#! /usr/bin/perl
########################################################
#
# This script is intended to provide a means for
# detecting changes made to files, via a regular
# comparison of MD5 hashes and others properties
# to an established "database".
# In this respect, it is designed as a portable clone
# of tripwire or aide softwares.
#
# This script requires perl ,and some others perl modules
# which come in standard installation
#
###############################################################################
#    Copyright (C) 2002-2004 by Eric Gerbier
#    Bug reports to: eric.gerbier@tutanota.com
#    $Id$
#
#    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.
#
###############################################################################
# I use special naming for references :
# $r_name : for a ref to scalar
# $rh_name : for a ref to hashes
# $ra_name : for a ref to array
# global variables begin with an uppercase
###############################################################################
#                         perl modules and libraries
###############################################################################

use strict;
use warnings;
use Storable;
use English '-no_match_vars';
use Pod::Usage;
use Digest::MD5;                    # for md5 checksum
use MIME::Base64;                   # for checksum in md5sum mode
use Fcntl qw( :flock :DEFAULT );    # to get F_* and O_* LOCK_* constants
use FileHandle;                     # for database open mode
use Getopt::Long;                   # arg analysis
use Cwd 'abs_path';                 # convert to absolute path
use File::Glob ':bsd_glob';         # for jokers
use File::Basename;                 # for delete

# use diagnostics;
#use Data::Dumper;

# !! can also use Digest::SHA1 if exists: see below
# !!! Win32::FileSecurity is used too (on windows)

# we have to work with the kernel
# the first code use FAM : SGI::FAM
# but the last core revision is 25 Sep 1997
# the current code use Gamin
# but the code is going to be old ( 11 Nov 2005 )
#use Sys::Gamin;

# another way to explore will be Linux::Inotify2 (07 Oct 2008)

# afick library
my $abs_script_path = Cwd::abs_path($PROGRAM_NAME);
my $dirname = dirname($abs_script_path);
require $dirname . '/afick-common.pl';

###############################################################################
#                     global variables
###############################################################################
# almost all begin with a first upper-case character

# global objects
my $Config  = Afick::Cfg->new();
my $Backend = Afick::Backend->new();
my $Report  = Afick::Report->new();

# the scan will be dated with date of program begin
my $Date_ref     = $Config->get_dateref();      # reference date : begin of run
my $Date_ref_hum = $Config->get_dateref_h();    # same in strftime format

# real-time
#my $Real_time;          # real-time mode
#my %Delfiles = ();    # list of deleted files

###############################################################################
#                     subroutines
###############################################################################
# a general low-level methode to access database data
sub get_data($$) {

	my $name  = shift @_;    # file name
	my $field = shift @_;    # field name, ex mtime

	my $res;
	if ( $Backend->exist($name) ) {
		my $rec = $Backend->get($name);
		$res = $rec->$field();
	}
	elsif ( $Report->is_newval($name) ) {
		my $rec = $Report->get_newval($name);
		$res = $rec->$field();
	}
	else {
		$res = Afick::Constant->EMPTY;
	}
	return $res;
}
#######################################################
# get file mode from database
sub get_filemode($) {
	my $name = shift @_;    # file name

	return get_data( $name, 'filemode' );
}
#######################################################
# get inode date from database
sub get_ctime($) {
	my $name = shift @_;    # file name

	return get_data( $name, 'ctime' ) || 0;
}
#######################################################
# get inode date from database
sub get_mtime($) {
	my $name = shift @_;    # file name

	return get_data( $name, 'mtime' ) || 0;
}
#######################################################
# get file_type from database
sub file_type($) {
	my $name = shift @_;    # file name

	my $mode = get_filemode($name);
	return file_type_h($mode);
}
#######################################################
# high-level sub for summary messages
sub report_summary($$) {
	my $status   = shift @_;    # new, change, deleted, dangling ...
	my $filename = shift @_;    # file name

	my $filetype = file_type($filename);

	Afick::Msg->report( "$status $filetype : $filename", Afick::Msg->Info );
	return;
}
#######################################################
# to write on history file
sub history($) {
	my $txt = shift @_;         # text to be added
	chomp $txt;

	my $history = $Config->get_directive('history');
	if ($history) {

		# add a summary on history file
		if ( open my $fh_hist, '>>', $history ) {
			print {$fh_hist} "$Date_ref_hum  $txt" . Afick::Constant->LF;
			close $fh_hist
			  or Afick::Msg->warning(
				"(history) can not close $history history file : $ERRNO");
		}
		else {
			Afick::Msg->warning(
				"(history) can not write to $history history file : $ERRNO");
		}
	}
	return;
}
#######################################################
# will help exit as clean as possible
sub signal_handler {
	my ($sig) = @_;    # 1st argument is signal name

	# first close database "clean"
	$Backend->close_database($Config);

	# then exit with a warning
	Afick::Msg->my_die("Caught a SIG$sig--shutting down");

	# dummy return
	return;
}
#######################################################
# we need to have the full database name for checksum
sub get_database_list($) {
	my $ignore_case = shift @_;

	# bugfix : the bsd_glob is not a valid choice
	# because on init, no database exists, so
	# the first update, or any compare action will show new files !
	# @list  = bsd_glob( $Database . '*' );
	# so we have a list, but we cannot add control file (does not exist)

	my @list = $Backend->list_database_control();

	# add control file
	my $ctr = Afick::Control->new( $Backend->get_database() );
	push @list, $ctr->get_ctr();

	# convert to full path
	if ( !$Config->get_directive('allow_relativepath') ) {
		foreach my $elem (@list) {
			$elem = to_abspath($elem);
		}
	}

	# apply case
	if ($ignore_case) {
		foreach my $elem (@list) {
			$elem = lc $elem;
		}
	}
	return @list;
}
#######################################################
# auto-control on database files
# only check permissions
sub auto_control_prepdb() {

	my $check;

	# should test read-only mode
	if ( $Config->get_directive('allow_relativepath') ) {
		$check = is_microsoft() ? 'n' : 'p+n';
	}
	else {
		$check = 'u+g+p+n';
	}

	my @list = get_database_list( $Config->get_directive('ignore_case') );

	foreach my $elem (@list) {
		$Config->add_control_file( $elem, $check );
	}
	return;
}
#######################################################
# parcours File::Find clone
#######################################################
# is called from create_database or update_database
sub top_parcours($) {
	my $config = shift @_;    # Cfg object

	my @listerep = $config->rules_byorder();
	foreach my $elem (@listerep) {
		my $rel_name = remove_chroot($elem);
		Afick::Msg->debug( "(top_parcours) $elem ($rel_name)", 4 );
		parcours( $elem, Afick::Constant->EMPTY, 0 );
	}
	return;
}
#######################################################
# scan a directory
# we use SLASH separator on any system (unix and windows) to
#   match normalizisation (cf reg_name)
sub parcours_directory($$$) {
	my $rep   = shift @_;    # directory
	my $masq  = shift @_;    # herited masq
	my $level = shift @_;    # level

	# equal : do not scan into the directory
	return if ( $Config->is_onlythis($rep) );

	if ( -l $rep ) {

		# do not follow links on directory
		Afick::Msg->debug( "(parcours) $rep skipped (link on directory)", 2 );
	}

	# search for files in this directory
	elsif ( opendir my $fh_dir, $rep ) {

		# read an array because of recursive algo
		# to avoid reach the limit of open files
		my @liste = readdir $fh_dir;
		foreach my $elem (@liste) {
			next if ( is_special_dir($elem) );

			# build full path
			my $f_elem =
			  ( is_fsroot($rep) )
			  ? $rep . $elem
			  : $rep . Afick::Constant->SLASH . $elem;

			# equal : do not scan sub-dir
			next if ( ( $Config->is_onlydir($rep) ) and ( -d $f_elem ) );

			parcours( $f_elem, $masq, $level + 1 );

		}
		closedir $fh_dir
		  or Afick::Msg->warning("can not close directory $rep : $ERRNO");
	}
	else {

		# should never happen ... except on windows (junction directories)
		if ( is_microsoft() ) {
			Afick::Msg->debug(
				"(parcours) can not open directory $rep : $ERRNO", 3 );
		}
		else {
			Afick::Msg->warning(
				"(parcours) can not open directory $rep : $ERRNO");
		}
	}    # opendir error

	return;
}
#######################################################
# the general fonction to scan an element
sub parcours($$$) {
	my $elem  = shift @_;    # file or directory to scan
	my $masq  = shift @_;    # herited masq
	my $level = shift @_;    # level

	Afick::Msg->debug( "(parcours) $elem masq = $masq level = $level", 3 );

	# key will be used for all internals indexes
	my $key = ( $Config->get_directive('ignore_case') ) ? lc $elem : $elem;

	# stop as soon as possible
	return if ( $Config->is_exception($key) );
	return if ( $Report->is_finded($key) );

	# is there a special masq ?
	$masq = $Config->get_masq( $key, $masq );

	my ( $fic, undef, undef ) = fileparse($key);
	if ( -d $key ) {

		# directory inode
		wanted_update( $key, $masq );

		# files in directory
		parcours_directory( $key, $masq, $level );
	}
	elsif ( -l $key ) {
		return if ( $Config->file_exceptions( $key, $fic ) );
		my $ret = test_dangling($key);
		$Report->set_dangling( $key, $ret ) if ($ret);
		wanted_update( $key, $masq );
	}
	else {
		return if ( $Config->file_exceptions( $key, $fic ) );
		wanted_update( $key, $masq );
	}

	return;
}
#######################################################
# create a new database (empty the existing one if exists)
# sub sister is update_database
sub create_database($$$) {
	my $backend      = shift @_;
	my $config       = shift @_;    # Cfg object
	my $date_ref_hum = shift @_;

	$backend->create_database_init( $config, $date_ref_hum,
		get_afick_version() );

	# add database controls
	auto_control_prepdb();

	$Report->set_update(0);
	top_parcours($config);
	my $nbscan = $Report->nb_finded();
	$Report->print_dangling( 1, $Config );
	$backend->close_database($config);

	Afick::Msg->crlf();
	Afick::Msg->info(
		"Hash database created successfully. $nbscan files entered.");
	history("init : $nbscan files entered");

	return;
}
#######################################################
# analysis
#######################################################
# compare 2 file infos and print the difference
#######################################################
# detect file changes : new, deleted, changed
sub analyse_change($$$$) {
	my $name     = shift @_;    # file name
	my $rel_name = shift @_;    # name for database
	my $entry    = shift @_;    # current fileinfo data
	my $masq     = shift @_;    # attribute masq

	if ( $Backend->exist($rel_name) ) {

		# we have an old enty
		my $old_entry = $Backend->get($rel_name);
		if ( $old_entry->is_changed( $entry, $masq ) ) {

			# keep values to compare
			$Report->set_oldval( $name, $old_entry );
			$Report->set_newval( $name, $entry );

			# keep masq for display_changes sub
			$Report->set_masq( $name, $masq );

			# for file : print result immediatly
			if ( $Config->is_filemode() ) {
				report_summary( 'changed', $name );
				$old_entry->display_changes( $entry, $Config, $Report );
			}

			# update database
			$Backend->set( $rel_name, $entry ) if $Backend->is_update();
		}
		elsif ( $old_entry ne $entry ) {

			# dummy change (not in masq): update database
			$Backend->set( $rel_name, $entry ) if $Backend->is_update();
		}
		else {

			# no change
		}
	}
	else {

		# new entry
		# Newfiles will be used in print_new for report_full_newdel
		# if the parent directory is not in Newfiles list
		# we have a first level (1)
		# else increment level
		my ( undef, $rep, undef ) = fileparse($rel_name);
		remove_trailing_slash( \$rep );

		#if ( $Report->is_newfiles($rep) ) {
		#	$Report->set_newfiles( $rel_name, $Report->get_newfiles($rep) + 1 );
		#}
		#else {
		$Report->set_newfiles( $rel_name, 1 );

		#}
		Afick::Msg->debug(
			"(analyse_change) new $rel_name ($rep) "
			  . $Report->get_newfiles($rel_name),
			4
		);

		$Report->set_newval( $rel_name, $entry );

		# for file : print result immediatly
		if ( $Config->is_filemode() ) {
			report_summary( 'new', $name );
		}
		$Backend->set( $rel_name, $entry ) if $Backend->is_update();
	}
	return;
}
#######################################################
# process a file in update mode
sub wanted_update($$) {
	my $name = shift @_;    # file name
	my $masq = shift @_;    # attribute masq

	# store file name without chroot prefix
	my $rel_name = remove_chroot($name);
	$Report->set_finded($rel_name);
	my $entry =
	  Afick::Object->file_info( $name, $masq, $Date_ref, $Config, $Report,
		$Backend->get_dbm() );

	if ( $Report->is_update() ) {

		# compare / update
		analyse_change( $name, $rel_name, $entry, $masq );
	}
	else {
		# create mode
		$Backend->set( $rel_name, $entry );
	}

	return;
}
#######################################################
# display statistics about all detected changes
# and compute the return code
sub statistics($$$$) {
	my $action   = shift @_;    # create, update, compare ...
	my $nbnew    = shift @_;    # number of new files/directories
	my $nbdelete = shift @_;    # number of deleted files/directories
	my $nbmod    = shift @_;    # number of modified files/directories

	my $nbscan   = $Report->nb_finded();
	my $dangling = $Report->nb_dangling();
	my $nbmasked = $Report->nb_masked_sysupdate();
	my $nbchange = $nbnew + $nbdelete + $nbmod;

	my $nb_exclude_re    = $Config->nb_exclude_re();
	my $nb_exclude_prefx = $Config->nb_exclude_prefx();
	my $nb_exclude_sufx  = $Config->nb_exclude_sufx();
	my $text =
"$nbscan files scanned, $nbchange changed (new : $nbnew; delete : $nbdelete; changed : $nbmod; dangling : $dangling; exclude_suffix : $nb_exclude_sufx; exclude_prefix : $nb_exclude_prefx; exclude_re : $nb_exclude_re; masked : $nbmasked; degraded : "
	  . $Report->get_nb_degraded() . ')';
	if ( $Backend->is_update() ) {
		Afick::Msg->info("Hash database updated successfully : $text");
	}
	else {
		Afick::Msg->info("Hash database : $text");
	}
	history("$action : $text");

	# status
	## no critic (ProhibitParensWithBuiltins)
	return oct(
		join(
			Afick::Constant->EMPTY,
			'0b', map { $_ ? 1 : 0; } ( $nbnew, $nbdelete, $nbmod, $dangling )
		)
	);
}
#######################################################
# compare or update a database with the system
# sub sister is create_database
sub update_database($$$) {
	my $backend      = shift @_;
	my $config       = shift @_;    # Cfg object
	my $date_ref_hum = shift @_;

	my $configfile = $config->get_configfile();
	my $database   = $config->get_directive('database');

	#first check if database exists
	my $ctr = Afick::Control->new($database);
	if ( $ctr->exists_ctr() ) {
		Afick::Msg->debug( "(update) $database $configfile", 3 );
	}
	else {

		# no control file
		# switch to init, to allow cron job to start
		# (allow to suppress init from install)
		Afick::Msg->warning(
			'(update) no database found : change action to init');
		create_database( $backend, $config, $date_ref_hum );
		return 0;
	}

	my $action = ( $backend->is_update() ) ? 'update' : 'compare';
	$backend->open_database( $config, $action, $date_ref_hum,
		get_afick_version() );

	# add database controls
	auto_control_prepdb();

	# check if afick was changed
	auto_control_check();

	# for afick-tk progress bar
	# guess the number of file does not change too much
	my $total = scalar $backend->getkeys();
	Afick::Msg->progress("total $total");

	# scan file list
	$Report->set_update(1);
	top_parcours($config);

	Afick::Msg->debug( '(update) begin analysis', 1 );

	# analysis
	my $nbdelete = 0;
	my $nbnew    = 0;

	# summary section
	if (    ( $config->get_directive('report_summary') )
		and ( !$config->is_filemode() ) )
	{
		Afick::Msg->info('summary changes');
		$nbnew    = $Report->print_new( 0, $Config );
		$nbdelete = $Report->print_delete( 0, $Config, $Backend );
		$Report->print_changed( 0, $Config );
		$Report->print_dangling( 0, $Config );
	}

	# details section
	Afick::Msg->crlf();
	if ( !$config->is_filemode() ) {
		Afick::Msg->info('detailed changes');
		$nbnew    = $Report->print_new( 1, $Config );
		$nbdelete = $Report->print_delete( 1, $Config, $Backend );
		$Report->print_changed( 1, $Config );
		$Report->print_dangling( 1, $Config );
	}
	my $nbmod    = $Report->nb_oldval();
	my $nbchange = $nbmod;
	$nbchange += $nbnew;
	$nbchange += $nbdelete;

	$backend->close_database($config);

	Afick::Msg->crlf();

	my $ret = statistics( $action, $nbnew, $nbdelete, $nbmod );

	# return a status
	return $ret;
}    # end update
#######################################################
# get a forced ctime to display inode date for new files
sub ctimef($) {
	my $key   = shift @_;
	my $ctime = get_ctime($key) || ( stat $key )[10] || 0;

	# force scalar env
	my $date = get_time( $ctime, $Config->get_directive('utc_time') );
	return $date;
}
#######################################################
# get parent mtime
sub parent_date($) {
	my $parent = shift @_;
	my $mtime  = get_mtime($parent) || ( stat $parent )[9] || 0;

	# force scalar env
	my $date = get_time( $mtime, $Config->get_directive('utc_time') );
	return $date;
}
#######################################################
#  print program version
sub version() {

	my $version = get_afick_version();
	print "afick : another file integrity checker\nversion $version\n";
	return;
}
#######################################################
# auto-control
#######################################################
# compare old and entry for afick main components
sub control($) {
	my $name = shift @_;    # file name to control

	# get old entry
	my $old_entry = $Backend->get($name);
	$Report->set_finded($name);

	# compute the new entry
	my $masq = $Config->to_scan($name);
	my $new_entry =
	  Afick::Object->file_info( $name, $masq, $Date_ref, $Config, $Report,
		$Backend->get_dbm() );

	# special warnings for control files
	if ( defined $old_entry ) {
		if ( $old_entry->is_changed( $new_entry, $masq ) ) {
			Afick::Msg->warning(
				"(control) afick internal change : $name (see below)");

			# debug
			Afick::Msg->debug( "(control) old_entry=$old_entry", 4 );
			Afick::Msg->debug( "(control) new_entry=$new_entry", 4 );
		}
	}
	else {
		# first run
		Afick::Msg->warning("(control) detect a new afick file : $name");
	}
	analyse_change( $name, $name, $new_entry, $masq );
	return;
}
#######################################################
# check if afick internals changes since last run
sub auto_control_check() {
	Afick::Msg->debug( '(control) begin', 4 );

	# get files from Control to not forget anyone
	my %control = $Config->control();
	foreach my $elem ( keys %control ) {
		control($elem) if ( !$Config->is_exception($elem) );
	}
	Afick::Msg->debug( '(control) end', 4 );

	return;
}
#############################################################
#                          main
#############################################################
# for file type macros : the POSIX module contains all standard defines macros
# others (links, sockets) may be loaded from Fcntl (tag :mode), cf man perlfunc
# but are not all defined on windows for example

init_file_macro();

# buffer
$OUTPUT_AUTOFLUSH = 1;

# var for get options
my %opt = ();

Getopt::Long::Configure('no_ignore_case');
if (
	!GetOptions(
		\%opt,

		# directives
		'allow_overload|o!',
		'allow_relativepath!',
		'archive|A=s',
		'database|D=s',
		'debug|d=i',
		'exclude_prefix|X=s',
		'exclude_suffix|x=s',
		'exclude_re|R=s',
		'follow_symlinks|Y!',
		'history|y=s',
		'ignore_case|a!',
		'max_checksum_size|S=i',
		'only_suffix=s',
		'quiet|q!',
		'report_full_newdel|full_newdel|f!',
		'report_context!',
		'report_url=s',
		'report_summary!',
		'report_syslog!',
		'mask_sysupdate!',
		'running_files|r!',
		'utc_time!',
		'timing|t!',
		'verbose|v!',
		'warn_dead_symlinks|dead_symlinks|s!',
		'warn_missing_file|missing_files|m!',

		# config
		'config_file|c=s',
		'progress|P',

		# actions
		'check_config|C',
		'check_update|U!',
		'clean_config|G',
		'help|?',
		'man',
		'init|i',
		'compare|k',
		'list|l=s@',
		'search=s',
		'print|p',
		'csv',
		'export',
		'import',
		'print_config',
		'print_directive',
		'print_macro',
		'print_alias',
		'print_rule',
		'version|V',
		'update|u',

		#'real_time',

		# plugins
		'stat_ext',
		'stat_secu',
		'stat_size',
		'stat_date',
		'duplicates',
	)
  )
{
	pod2usage(2);
}

# debug
#Afick::Msg->info( Dumper( \%opt ), 1 );

# update conf from program args
$Backend->set_update( $opt{'update'} );
Afick::Msg->set_progress( $opt{'progress'} ) if ( exists $opt{'progress'} );

# set directives from command line options
$Config->overload_directives( \%opt );

## no critic (ProhibitCascadingIfElse)
if ( exists $opt{'help'} ) {

	# -h : help
	pod2usage(1);
	exit;
}
elsif ( exists $opt{'man'} ) {
	pod2usage( -verbose => 2 );
}
elsif ( exists $opt{'version'} ) {

	# -V : version
	version();
	exit;
}
elsif ( exists $opt{'check_update'} ) {

	# -U : check if new version
	check_update( 'afick', get_afick_version() );
	exit;
}

my $opt_configfile;    # config file name
if ( exists $opt{'config_file'} ) {

	# convert in absolute path
	$opt_configfile = to_abspath( $opt{'config_file'} );
	Afick::Msg->debug( "config file : $opt_configfile", 2 );
	$Config->set_configfile($opt_configfile);
}
elsif ( -e $Config->get_configfile() ) {
	$opt_configfile = $Config->get_configfile();
}
else {

	pod2usage('missing config_file name (-c flag) and default config file');
}

# list of files from config file
my $nb_pbs = $Config->read_configuration( $opt{'clean_config'} );
if ( ( exists $opt{'check_config'} ) or ( exists $opt{'clean_config'} ) ) {
	if ($nb_pbs) {
		Afick::Msg->warning(
			"found $nb_pbs errors in config file $opt_configfile");
	}
	else {
		Afick::Msg->info("config file $opt_configfile ok");
	}
	exit $nb_pbs;
}

Afick::Object->set_utc_time( $Config->get_directive('utc_time') );

# change priority
if ( not is_microsoft() ) {
	my $nice = $Config->get_macro('NICE');

	# test for NICE macro
	if ( !$nice ) {
		setpriority 0, 0, $nice;
		Afick::Msg->debug( "change priority to $nice", 2 );
	}
}

# print
#######
my $opt_print_directive = $opt{'print_directive'};
my $opt_print_macro     = $opt{'print_macro'};
my $opt_print_alias     = $opt{'print_alias'};
my $opt_print_rule      = $opt{'print_rule'};
if ( exists $opt{'print_config'} ) {
	$opt_print_directive = 1;
	$opt_print_macro     = 1;
	$opt_print_alias     = 1;
	$opt_print_rule      = 1;
}

if (   $opt_print_directive
	or $opt_print_macro
	or $opt_print_alias
	or $opt_print_rule )
{

	# force print all and exit
	$Config->print_config( $opt_print_directive, $opt_print_macro,
		$opt_print_alias, $opt_print_rule, 1 );
	exit;
}
elsif ( Afick::Msg->get_msg_level() != 0 ) {

	# print in debug mode only
	$Config->print_config( 1, 1, 1, 1, 1 );
}

# no we should have a database name
my $database = $Config->get_directive('database');
if ( !$database ) {

	pod2usage(
"missing database name in options (-D) and in config file $opt_configfile"
	);
}

# signal trapping
$SIG{'INT'}  = \&signal_handler;
$SIG{'QUIT'} = \&signal_handler;
$SIG{'TERM'} = \&signal_handler;
$SIG{'HUP'}  = 'IGNORE';

my $return_value = 0;

# actions
## no critic (ProhibitCascadingIfElse)
if ( exists $opt{'list'} ) {

	# -l : list
	my @opt_list = @{ $opt{'list'} };
	$Config->set_file_list( get_from_command_line(@opt_list) );
	$return_value = update_database( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'csv'} ) {

	# -csv : export in csv
	print_common( $Backend, $Config, $Date_ref_hum, 'csv', $opt{'search'} );
}
elsif ( exists $opt{'export'} ) {

	# portable export
	print_common( $Backend, $Config, $Date_ref_hum, 'export', $opt{'search'} );
}
elsif ( exists $opt{'import'} ) {

	# portable export
	import_raw( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'print'} ) {

	# -p : print
	print_common( $Backend, $Config, $Date_ref_hum, 'print', $opt{'search'} );
}
elsif ( exists $opt{'search'} ) {

	# -p : print
	print_common( $Backend, $Config, $Date_ref_hum, 'print', $opt{'search'} );
}
elsif ( exists $opt{'init'} ) {

	# -i : init
	$Backend->set_update(1);    # update mode
	Afick::Msg->set_archive( $Config->get_directive('archive'), $Date_ref );
	create_database( $Backend, $Config, $Date_ref_hum );
}

#elsif ( exists $opt{'real_time'} ) {
#	Afick::Msg->set_archive( $Archive, $Date_ref );
#	$return_value = real_time( $database, $Config );
#}
elsif ( exists $opt{'update'} or exists $opt{'compare'} ) {

	# -u : update and -k : check
	Afick::Msg->set_archive( $Config->get_directive('archive'), $Date_ref );
	$return_value = update_database( $Backend, $Config, $Date_ref_hum );
}

# plugins
elsif ( exists $opt{'stat_ext'} ) {
	stat_ext( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'stat_secu'} ) {
	stat_secu( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'stat_size'} ) {
	stat_size( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'stat_date'} ) {
	stat_date( $Backend, $Config, $Date_ref_hum );
}
elsif ( exists $opt{'duplicates'} ) {
	duplicates( $Backend, $Config, $Date_ref_hum );
}
else {

	# problem : print doc
	pod2usage('no action to do (-i, -u, -k, -l, -p)');
}
## use critic

# timing info
if ( $Config->get_directive('timing') ) {
	my ( $user, $system, $cuser, $csystem ) = times;
	Afick::Msg->info( "user time : $user; system time : $system; real time : "
		  . ( time - $Date_ref ) );
}

exit $return_value;

__END__

=head1 NAME

afick - Another File Integrity Checker

=head1 DESCRIPTION

The goal of this program is to monitor what change on your host : new/deleted/modified files.
So it can be used as an intrusion detection system ( by integrity checking ).
It is designed to be a portable clone of aide (Advanced Intrusion Detection Environment), or Tripwire software.

For the better security, you should launch it regularly (for exemple by a batch task)

This is a command-line program, you can use C<afick-tk.pl> if you
prefer a graphical interface.

A web interface is also privided by a webmin module.

=head1 SYNOPSIS

afick [L<action|/ACTIONS>] [L<options|/OPTIONS>]

afick use posix syntax, which allow many possibilities : 

=over 4

=item *
long (--) options

=item *
short (-) options

=item *
negative (--no) options

=back

Mandatory action (one and only one must be used) : 

 -i|--init                    initialize the hash.dbm database
 -C|--check_config	      only check config file and exit
 -G|--clean_config            check and clean configuration, then exit
 -U|--check_update            check if a software update is available
 -k|--compare                 compare the hash.dbm database
 -l|--list "fic1,fic2,.."     check the files given in arg (separeted by comma)
 -u|--update                  compare and update the hash.dbm database
 -p|--print                   print content of database
 --search filter              print content of database, filtered (see man of html doc for exemples)
 --csv                        export the database in csv format
 --export                     export database in portable text format
 --import                     import a database from an exported file
 --print_config               display all internals variables after arguments and config file parsing.
 			      it is the same as the 4 followings options, concatenated
 				(for debugging purposes)
 --print_directive        display directives (after config file and command line parsing)
 --print_macro            display macros (after config file parsing)
 --print_alias            display aliases (after config file parsing)
 --print_rule             display rules (after config file parsing)
 --stat_ext               display list of file extension, sorted by number (usefull for windows)
 --stat_secu              display some dangerous files (suid, sgid, group writable, world writable )
 --stat_size              display statistics on file size
 --stat_date              display chronological list of changed files
 --duplicates             display duplicates files

Other options

 -a|--ignore_case             helpful on Windows platforms, dangerous on Unix ones
 				reverse : --noignore_case
 -c|--config_file file        name of config file to use
 -D| --database file          force the database name    
 -d|--debug level	      set a level of debugging messages, from 0 (none) to 4 (full)
 -f|--report_full_newdel      report full information for new or deleted directories
			       reverse : --noreport_full_newdel 
 -m|--warn_missing_file           warn about files declared in config files 
                               which do not exist, 
			       reverse : --nowarn_missing_file
 -o|--allow_overload          allow rule overload : the last rule wins
                               reverse: --noallow_overload
 --allow_relativepath         control files are stored with a relative path
                               reverse: --noallow_overload
 -r|--running_files           warn about "running" files : modified since program begin
                               reverse: --norunning_files
 -s|--warn_dead_symlinks           warn about dead symlinks 
                               reverse: --nowarn_dead_symlinks
 -Y|--follow_symlinks         checksum on links target file (yes) or checksum on target name (no)
                               reverse: --nofollow_symlinks
 -S|--max_checksum_size	size  maximum cheksum size (bytes) : for bigger file, just compute checksum on begin of file
 				0 means no limit
 -t|--timing		      Print timing statistics
				reverse : --notiming
 --utc_time                   display report's dates in utc time, else in local time
				reverse : --noutc_time
 -v|--verbose                 toggle verbose mode (identical to full debug);
 			       reverse : --noverbose
 -q|--quiet		      toggle quiet mode (print only if there's a change to report)
				reverse : --noquiet
 -P|--progress		      display the name of scanned files, to be used only by afick-tk
 -h|--help                    show this help page
 --man			      full help
 -V|--version                 show afick version
 -x|--exclude_suffix "ext1 ext2"        list of file/dir suffixes to ignore
 -X|--exclude_prefix "pre1 pre2"        list of files/dir prefixes to ignore
 -R|--exclude_re "patern1 patern2"      list of files/dir patterns (regular expressions) to ignore
 --only_suffix "ext1 ext2"              list of suffix to scan (just this ones)
 -y|--history file	      history file of all runs with summary
 -A|--archive directory	      directory where archive files are stored
 --report_url output	      where to send afick report.default is stdout
 --report_syslog 	      send afick report to sylog. reverser : --noreport_syslog
 --report_summary             report changes in summary section
 --report_context             report all attributes changes (not only those from rules)
 --mask_sysupdate             mask package updates

=head1 REQUIRED ARGUMENTS

you have to give afick an action to do. See below :

=head1 ACTIONS

You have to use one this mandatory action :

=over 4

=item B<--init|-i>

initiate the database.

=item B<--check_config|-C>

only check config file syntax and exit with the number of errors

=item B<--check_update|-U>

check if a new software version is available on web server

=item B<--clean_config|-G>

check config file syntax, clean (comments) bad line, and exit with the number of errors

=item B<--compare|-k>

compare the file system with the database.

=item B<--list|-l "file1,file2,...,filen">

compare the specified files with the database. The files have to separeted by a comma.
You can also use : -lfile1 -lfile2 (if a file name contains space character, it must be quoted)

=item B<--csv>

export the database in csv format
the first line give the column title

=item B<--print|-p>

print the content of the database.

=item B<--export>

export database in portable text format to stdout.
to be used with --import option (see below)

=item B<--import>

import a database from stdin. The source file must be a result from --export command.
The database is empty before import.

=item B<--search your_filter>

print the content of the database, filtered by your_filter filter.

filters are to be written with column keywords and perl operators, and should be quoted

keywords are :  filetype, name, md5, sha1, sha256, sha512, checksum, device, inode, filemode, links, uid, acl, gid, filesize, blocs, atime, mtime, ctime

for examples :

"filetype =~ m/symbolic/"  : filter on file type

"filesize E<lt>  5000000" : filter on file size

"filemode & 04000" : extract suid files

"(filesize E<gt>  5000) and (name =~ m/urpmi/)" : you can combine filters

=item B<--print_config>

display all internals variables after command line and config file parsing (for debugging purposes).
It is the same as the 4 following options : --print_directive --print_macro --print_alias --print_rule

=item B<--print_directive>

display directives (after config file and command line parsing)

=item B<--print_macro>

display macros (after config file parsing)

=item B<--print_alias>

display aliases (after config file parsing)

=item B<--print_rule>

display rules (after config file parsing)

=item B<--update|-u>

compare and update the database.

=item B<--stat_ext>

display list of file extension, sorted by number (usefull for windows)

=item B<--stat_secu>

display from databases some dangerous files (suid, sgid, group writable, world writable )

=item B<--stat_size>

display from databases statistics on file size

can help to configure the max_checksum_size option

=item B<--stat_date>

display chronological list of changed files

be carefull, the system only keeps the last change date

=item B<--duplicates>

display a list of duplicates files : files with same contents. It uses the checksum
to compare files.

=back

=head1 OPTIONS

You can use any number of the following options :

=over 4

=item B<--archive|-A directory>

write reports to "directory".

=item B<--config_file|-c configfile>

read the configuration in config file named "configfile".

=item B<--database|-D name>

name of database to use.

=item B<--debug|-d level>

set a level of debugging messages, from 0 (none) to 4 (full)

=item B<--report_full_newdel|-f,(--noreport_full_newdel)>

(do not) report full information on new and deleted directories.

=item B<--help|-h>

Output summary help information and exit.

=item B<--man>

Output full help information and exit.

=item B<--history|-y historyfile>

write session status to history file

=item B<--ignore_case|-a>

ignore case for file names. Can be helpful on Windows operating systems, but is dangerous on Unix ones.

=item B<--max_checksum_size|-S size>

fix a maximum size (bytes) for checksum. on bigger files, compute checksum only on first 'size' bytes.
( 0 means no limit)

=item B<--warn_missing_file|-m,(--nowarn_missing_file)>

(do not) warn about files declared in config files which does not exist.

=item B<--warn_dead_symlinks|-s,(--nowarn_dead_symlinks)>

(do not) warn about dead symlinks.

=item B<--follow_symlinks,(--nofollow_symlinks)>

if set, do checksum on target file, else do checksum on target file name.

=item B<--allow_overload,(--noallow_overload)>

if set, allow rule overload (the last rule wins), else put a warning and keep the first rule.

=item B<--allow_relativepath,(--noallow_relativepath)>

if set, auto-control files (afick scripts, config and database) are stored as relative path.

=item B<--progress|-P>

display the name of scanned files, to be used only by afick-tk

=item B<--running_files|-r,(--norunning_files)>

(do not) warn about "running" files : modified since program begin.

=item B<--timing|-t,(--notiming)>

(do not) Print timing statistics.

=item B<--version|-V>

Output version information and exit.

=item B<--quiet|-q,(--noquiet)>

(not in) quiet mode : print only if there's a change to report. Do not use it with stat_* options or your 
output will be empty !. This mode is not recommanded, as it does not allow to check if an afick log was removed 
to mask a system change (with afick_archive.pl --check).

=item B<--verbose|-v,(--noverbose)>

(not in) verbose mode (obsolete).

=item B<--only_suffix|-x "ext1 ext2 ... extn">

list of suffix to scan (just this ones)

=item B<--exclude_suffix|-x "ext1 ext2 ... extn">

list of suffixes (files/directories ending in .ext1 or .ext2 ...) to ignore

=item B<--exclude_prefix|-X "pre1 pre2 ... pren">

list of prefix (files/directories beginning with pre1 or pre2 ...) to ignore

=item B<--exclude_re|-R "pre1 pre2 ... pren">

list of patterns (regular expressions) to ignore files or directories

=item B<--report_url output>

output can stdout, stderr or null (which mean no output)

=item B<--report_syslog (--noreport_syslog)>

send (or not) afick's report to syslog. the priority is 'notice' for info messages,
'warning' for warning messages. The facility is 'user'

=item B<--report_summary,(--noreport_summary)>

If true, report in the summary section, one ligne by file change

=item B<--report_context,(--noreport_context)>

If true, display all attributes changes, not only those selected by rule.
To make a difference, attributes from rules will have a "w_" prefix (warning),
and other attributes will have a 'i_' prefix (info).

=item B<--mask_sysupdate,(--nomask_sysupdate)>

each package update produces changes, which can be seen as false positives
and the package manager can be used to check changes.
this experimental option is set to false by default.
If set to true, files which are not modified according the package manager, are masked
linux rpm and deb package manager are available to use for now

=item B<--utc_time,(--noutc_time)>

if set, display report's dates in utc time, else in local time

=back

=head1 CONFIGURATION

The configuration file can be given by the -c option.
Else it can be given by the AFICK_CONFIG environment variable.
Else on windows, it search for a file named F<windows.conf> in the install directory.
Else (on Unix/linux), it first search for F</etc/afick.conf>, then for F<afick.conf> in install directory.

for config file syntax see afick.conf(5)

=head1 FILES

afick can write several kinds of file

=over 4

=item database file

the database is used to store data between afick's run. name and path 
are set by the database directive

=item control file

it is used to check afick integrity.
It has the name of the database, with '.ctr' suffix.

=item history file

(optionnal but recommended) : it is used to keep an history of all
report's summary. name and path are set by the history directive

=item archive's files

(optionnal but recommended) : it is used to keep afick's reports. the path 
is set by the archive directive. the file name contains the afick run date
in AAAAMMJJhhmmss format

=item log file

unix log file (on /var/log) are created when using the afick_cron script

=back

=head1 DATABASE

until release 2.9, the database backend was SDBM, because
it was the only one available on every operating system.

Coming with 2.10 release, afick can use other database backend : the 'best'
available one will be detected on init.
There is no way to migrate an existing SDBM base to a new format. The only way 
is to re-run afick in init mode (caution : changes since last update will be "lost")

=head1 USAGE

To use this program, you must

first adjust the config file to your needs :
see afick.conf(5) for the syntax)

then initiate the database with :
C<afick -c afick.conf --init>

then you can compare with
C<afick -c afick.conf -k>

or compare and update with
C<afick -c afick.conf --update>

then the best way is to set a batch task to have regular check,
you can use afick_cron script on unix/linux systems or afick_planning.pl on windows

=head1 ENVIRONMENT

=over 4

=item AFICK_CONFIG

The config file can be set with AFICK_CONFIG environment variable.

=item AFICK_CHROOT

this define the chroot directory for files declared with a '@' ( afick.conf(5) ).

example :

AFICK_CHROOT=/usr/local

@software all

will scan for files in /usr/local/software

=item in configuration file

if the config file contain references to environment variables
( syntaxe : ${name} ), they are expanded at the begin of config analysis.

=back

=head1 EXIT STATUS

An exit status of 0 means no differences were found, and no dangling links 
(if the warn_dead_symlinks option is set) , non-zero means
some differences were found or some dangling links. 
The non-zero value is a bitmap representing the type of difference found:

=over 4

=item Bit 0 ( value : 1)

Dangling

=item Bit 1 (value : 2)

Changed

=item Bit 2 (value : 4)

Deleted

=item Bit 3 (value : 8)

New

=back

=head1 SECURITY

For a better security, afick not only check the rules from configuration file,
but try to check it-self : perl scripts, configuration file, database, and warn
if something change.

=head1 SEE ALSO

=for html
<a href="afick.conf.5.html">afick.conf(5)</a> for the configuration file syntax
<br>
<a href="afick-tk.1.html">afick-tk(1)</a> for the graphical interface
<br>
<a href="afick.1.html">afick(1)</a> for the command-line interface
<br>
<a href="afickonfig.1.html">afickonfig(1)</a> for a tool to change afick's configuration file
<br>
<a href="afick_archive.1.html">afick_archive(1)</a> for a tool to manage archive's reports
<br>
<a href="afick_learn.1.html">afick_learn(1)</a> for a learning tool

=for man
\fIafick.conf\fR\|(5) for the configuration file syntaxe
.PP
\fIafick\-tk\fR\|(1) for the graphical interface
.PP
\fIafick\fR\|(1) for the command-line interface
.PP
\fIafickonfig\fR\|(1) for a tool to change afick's configuration file
.PP
\fIafick_archive\fR\|(1) for a tool to manage archive's reports
.PP
\fIafick_learn\fR\|(1) for a learning tool

=head1 DIAGNOSTICS

for diagnostics, you can run afick in debug mode,
with the --debug 4 command line option

=head1 DEPENDENCIES

this program only use perl and its standard modules.

=head1 INCOMPATIBILITIES

none known

=head1 BUGS AND LIMITATIONS

afick works on files, it is not a Version Control System,
and it does not show changes in registry for windows users

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2002 Eric Gerbier
All rights reserved.

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.

=head1 AUTHOR

Eric Gerbier

you can report any bug or suggest to eric.gerbier@tutanota.com
