#!/usr/bin/perl
###############################################################################
#
# Copyright (c) 2008  Ramon Novoa  <rnovoa@artica.es>
# Copyright (c) 2008-2023 Pandora FMS.
#
# grep_log	Perl script to search log files for a matching pattern. The last
#           searched position is saved in an index file so that consecutive
#           runs do not return the same results. The log file inode number is
#           also saved to detect log rotation.
#
# 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; version 2 of the License.
#
# 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.
#
###############################################################################
use strict;
use File::Basename;
use Scalar::Util qw(looks_like_number);
BEGIN {
	eval {
		require MIME::Base64;
	};
}

# Output format (module or log_module).
my $Output = 'module';

# Be verbose
my $Verbose = 0;

# Index file storage directory, with a trailing '/'
my $Idx_dir=($^O =~ /win/i)?"$ENV{'TMP'}\\":"/tmp/";

# Log file
my $Log_file = '';

# Module name
my $Module_name = "default_log";

# Index file
my $Idx_file = '';

# Log file position index
my $Idx_pos = 0;

# Log file inode number
my $Idx_ino = '';

# Log file size
my $Idx_size = 0;

# Regular expression to be matched
my $Reg_exp = '';

# Flag to show or not summary module
my $summary_flag = 0;

my $nodatalist_flag = 0;

# Number of coincidences found
my $coincidences = 0;

if ( (defined ($ENV{GREP_LOG_TMP})) && (-d $ENV{GREP_LOG_TMP}) ) {
	$Idx_dir=$ENV{GREP_LOG_TMP};
}

# Define encode_base64 if it is not available via MIME::Base64.
my $encode_sub = defined(&MIME::Base64::encode_base64) ? \&MIME::Base64::encode_base64 : sub {
	my ($str, $endl) = @_;

	my @ALPHABET = ('A'..'Z', 'a'..'z', 0..9, '+', '/');
	my $str_len = length($str);
	my $str_base64 = '';
	
	for (my $i = 0; $i < $str_len; $i += 3) {
		my $chunk = substr($str, $i, 3);
		my $chunk_len = length($chunk);

		my $num = 0;
		$num |= ord(substr($chunk, 0, 1)) << 16 if ($chunk_len >= 1);
		$num |= ord(substr($chunk, 1, 1)) << 8  if ($chunk_len >= 2);
		$num |= ord(substr($chunk, 2, 1))       if ($chunk_len == 3);
		
		my $enc_1 = ($num & 0xfc0000) >> 18;
		my $enc_2 = ($num & 0x03f000) >> 12;
		my $enc_3 = ($num & 0x000fc0) >> 6;
		my $enc_4 = ($num & 0x00003f);
		
		$str_base64 .= $ALPHABET[$enc_1];
		$str_base64 .= $ALPHABET[$enc_2];
		$str_base64 .= $chunk_len >= 2 ? $ALPHABET[$enc_3] : '=';
		$str_base64 .= $chunk_len == 3 ? $ALPHABET[$enc_4] : '=';
	}
	
	return $str_base64;
};

########################################################################################
# Erase blank spaces before and after the string
########################################################################################
sub trim($){
	my $string = shift;
	if (empty ($string)){
		return "";
	}

	$string =~ s/\r//g;

	chomp ($string);
	$string =~ s/^\s+//g;
	$string =~ s/\s+$//g;

	return $string;
}

########################################################################################
# Empty
########################################################################################
sub empty($){
	my $str = shift;

	if (! (defined ($str)) ){
		return 1;
	}

	if(looks_like_number($str)){
		return 0;
	}

	if ($str =~ /^\ *[\n\r]{0,2}\ *$/) {
		return 1;
	}
	return 0;
}

###############################################################################
# SUB error_msg
# Print an error message and exit.
###############################################################################
sub error_msg ($) {
	my $err_msg = $_[0];

	if (! -z $err_msg) {
		print(stderr "[error] $err_msg.\n");
	}

	exit 1;
}

###############################################################################
# SUB print_help
# Print a help message.
###############################################################################
sub print_help () {
	print "Usage: $0 <log_file> <module_name> <pattern> <up_lines_extra> <bot_lines_extra> [--summary] [--nodatalist]\n\n";
	print "Options:\n";
	print "\t<log_file>\t\tPath to the log file to be monitored\n";
	print "\t<module_name>\t\tName of the module that will be created\n";
	print "\t<pattern>\t\tRegex string to be matched in log file\n";
	print "\t<up_lines_extra>\tShows NUM lines before matching lines to provide context\n";
	print "\t<bot_lines_extra>\tShows NUM lines after matching lines to provide context\n";
	print "\t--summary\t\tCreates a module with the total number of matches\n";
	print "\t--nodatalist\t\tInserts all coincidences in a single data output instead of a data per line\n";
}

###############################################################################
# SUB log_msg
# Print a log message.
###############################################################################
sub log_msg ($) {
	my $log_msg = $_[0];

	if (! -z $log_msg && $Verbose == 1) {
		print(stdout "[log] $log_msg.\n");
	}
}

###############################################################################
# SUB load_idx
# Load index file.
###############################################################################
sub load_idx () {
	my $line;
	my $current_ino;
	my $current_size;

	log_msg("Loading index file $Idx_file");

	open(IDXFILE, $Idx_file) || error_msg("Error opening file $Idx_file: " .
	                                         $!);

	# Read position and date
	$line = <IDXFILE>;
	($Idx_pos, $Idx_ino, $Idx_size) = split(' ', $line);

	close(IDXFILE);

	# Reset the file index if the file has changed
	$current_ino = (stat($Log_file))[1];
	$current_size = -s "$Log_file";
	if ($current_ino != $Idx_ino || $current_size < $Idx_size) {
		log_msg("File changed, resetting index");

		$Idx_pos = 0;
		$Idx_ino = $current_ino;
	}
	$Idx_size = $current_size;

	return;
}

###############################################################################
# SUB save_idx
# Save index file.
###############################################################################
sub save_idx () {

	log_msg("Saving index file $Idx_file");

	open(IDXFILE, "> $Idx_file") || error_msg("Error opening file $Idx_file: "
	                                          . $!);
	print (IDXFILE $Idx_pos . " " . $Idx_ino . " " . $Idx_size);
	close(IDXFILE);

	return;
}

###############################################################################
# SUB create_idx
# Create index file.
###############################################################################
sub create_idx () {
	my $first_line;

	log_msg("Creating index file $Idx_file");

	open(LOGFILE, $Log_file) || error_msg("Error opening file $Log_file: " .
	                                     $!);

	# Go to EOF and save the position
	seek(LOGFILE, 0, 2);
	$Idx_pos = tell(LOGFILE);

	close(LOGFILE);

	# Save the file inode number
	$Idx_ino = (stat($Log_file))[1];

	# Save the index file
	save_idx();

	return;
}

###############################################################################
# SUB parse_log
# Parse log file starting from position $Idx_pos.
###############################################################################
sub parse_log (;$$) {
	my ($up_lines,$bot_lines) = @_;
	my $line;

	log_msg("Parsing log file $Log_file");

	# Open log file for reading
	open(LOGFILE, $Log_file) || error_msg("Error opening file $Log_file: " .
	                                     $!);

	# Go to starting position.
	seek(LOGFILE, $Idx_pos, 0);

	# Parse log file
	my %data;

	# Matched line id
	my $matched_line = 0;

	if ( (defined($up_lines)) || (defined($bot_lines)) ){
		# Detailed workmode
		my @lines;
		my $nl = 0;
		my @nl_found;
		while ($line = <LOGFILE>) {
			push @lines, $line;
			if ($line =~ m/$Reg_exp/i) {
				push @nl_found, $nl;
				$coincidences++;
			}
			$nl++;
		}
		# Return all coincidences with the desired margin
		foreach my $curr_line (@nl_found){
			my $flag = 0; # avoid repetition of current line
			if (defined($up_lines)){
				$flag = 1;
				# Push upper lines
				for (my $i = ($curr_line-$up_lines); $i<=$curr_line; $i++){
					if ($i < 0) {next;}
					if (defined ($lines[$i])) {
						push (@{$data{$matched_line}}, $lines[$i]);
					}

				}
			}
			if (defined($bot_lines)){
				# Push bottom lines
				for (my $i = ($curr_line+$flag); $i<=($curr_line+$bot_lines); $i++){
					if (defined ($lines[$i])) {
						push (@{$data{$matched_line}}, $lines[$i]);
					}
				}
			}
			$matched_line++;
		}
	}
	else { # Standar workmode
		while ($line = <LOGFILE>) {
			if ($line =~ m/$Reg_exp/i) {
				push (@{$data{$matched_line++}}, $line);
			}
		}
	}

	$Idx_pos = tell(LOGFILE);
	close(LOGFILE);

	# Save the index file
	save_idx();

	return \%data;
}

###############################################################################
# SUB print_summary
# Print module summary to stdout.
###############################################################################
sub print_summary() {
	my $output = "<module>\n";
	$output .= "<name><![CDATA[" . $Module_name . " coincidences]]></name>\n";
	$output .= "<type><![CDATA[async_data]]></type>\n";
	$output .= "<datalist>\n";
	$output .= "<data><value><![CDATA[$coincidences]]></value></data>\n";
	$output .= "</datalist>\n";
	$output .= "</module>\n";
	print stdout $output;
}

###############################################################################
# SUB parse_log
# Print log data to stdout.
###############################################################################
sub print_log ($) {
	my $data = shift;

	# No data
	my @kdata = keys (%{$data});
	if ($#kdata < 0) {
		print_summary() if ($summary_flag == 1);
		return;

	}

	# Log module
	if ($Output eq 'log_module') {
		my $output = "<log_module>\n";
		$output .= "<source><![CDATA[" . $Module_name . "]]></source>\n";
		$output .= "<data><![CDATA[";
		my $tmp = '';
		foreach my $line (@kdata) {
			foreach my $content (@{$data->{$line}}) {
				$tmp .= $content;
			}
		}
		$output .= &$encode_sub($tmp, '');
		$output .= "]]></data>";
		$output .= "</log_module>\n";

		print stdout $output;
	}
	# Regular module
	else {
		my $output;
		print_summary() if ($summary_flag == 1);
		$output = "<module>\n";
		$output .= "<name><![CDATA[" . $Module_name . "]]></name>\n";
		$output .= "<type><![CDATA[async_string]]></type>\n";
			if ($nodatalist_flag == 1){
				$output .= "<data><![CDATA[";
                		foreach my $line (@kdata) {
                        		foreach my $content (@{$data->{$line}}) {
                                		my $processed_line = $content;
						$processed_line =~ "\n";
                                		$output .= $processed_line;
                        		}
                		}
			$output .= "]]></data>\n";
			}
			else {	
		$output .= "<datalist>\n";
		foreach my $line (@kdata) {
			$output .= "<data><value><![CDATA[";
			foreach my $content (@{$data->{$line}}) {
				my $processed_line = $content;
				$processed_line =~ s/\]\]/]]]]><![CDATA[/g;
				$output .= $processed_line;
			}
			$output .= "]]></value></data>\n";
		}
		$output .= "</datalist>\n";
			}
		$output .= "</module>\n";

		print stdout $output;
	}

}

###############################################################################
###############################################################################
## Main
###############################################################################
###############################################################################

# Check command line parameters
if ($#ARGV < 2) {
	print_help();
	exit 1;
}

$Log_file     = trim($ARGV[0]);
$Module_name  = trim($ARGV[1]);
$Reg_exp      = trim($ARGV[2]);
my $up_lines  = trim($ARGV[3]);
my $bot_lines = trim($ARGV[4]);
my $sum_flag  = trim($ARGV[5]);
my $nodatalist = trim($ARGV[6]);

if ( grep { /--summary/ } @ARGV )
{
        $summary_flag = 1;
}

if ( grep { /--nodatalist/ } @ARGV )
{
        $nodatalist_flag = 1;
}

# Create index file storage directory
if ( ! -d $Idx_dir) {
	mkdir($Idx_dir) || error_msg("Error creating directory $Idx_dir: "
	                             . $!);
}

# Check that log file exists
if (! -e $Log_file) {
	error_msg("File $Log_file does not exist");
}

# Create index file if it does not exist
$Idx_file=$Idx_dir . $Module_name . "_" . basename($Log_file) . ".idx";
if (! -e $Idx_file) {
	create_idx();
	exit 0;
}

# Load index file
load_idx();

# Parse log file
my $data = parse_log($up_lines,$bot_lines);

# Print output to stdout
print_log ($data);

exit 0;
