#!/usr/bin/perl -w
#
# $Id: WithContext.pm,v 1.2 2002/03/29 13:13:10 fpmip Exp $
#
# Context-based class for correlation
#
# Author : Tycho Fruru <tycho@fruru.com>
#
# Copyright (C) 2001 Tycho Fruru <tycho@fruru.com>
# Copyright (C) 2001 Conostix S.A. Luxembourg - Tycho Fruru <tycho@conostix.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
#
# 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.
#
package IPFC::Correlate::WithContext;

use strict;
use warnings;

use IPFC::Log;
use IPFC::Event;
use IPFC::EventGroup;
use IPFC::EventCondition;

use Data::Dumper;

our @ISA=("IPFC::Correlate::Interface");

sub new() {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	
	my $self;
	
	$self->{'contexts'} = {};
	$self->{'config'} = {};
	$self->{'pending-actions'} = {};

	IPFC::Log::logger(10, "Creating new IPFC::Correlate::WithContext object");
	bless($self,$class);
	return $self;
}

sub correlate($) {
# tries to find correlations between the new event using the current config and contexts
	my $self = shift;
	my $event = shift;
	
	IPFC::Log::logger(10,"Entering correlate");
	
	if (! $event->isa("IPFC::Event")) {
		die "Called IPFC::Correlate::WithContext::correlate without IPFC::Event parameter !";
	}
	
	$self->correlate_one_chain($event,"main");
	
	IPFC::Log::logger(10,"Leaving correlate");
	return 1;
}

sub correlate_one_chain($$) {
#
	my $self = shift;
	my $event = shift;
	my $chain = shift;
	
	my $i = 0;
	my $continue = 1;
	
	IPFC::Log::logger(10,"Entering correlate_one_chain : $chain");
	
	while ($continue && defined($self->{'config'}->{$chain}->[$i])) {
		if (my ($newchain) = $self->correlate_one_rule($event,$chain,$i)) {
			$i++;
			
			if (!defined $newchain) {
				IPFC::Log::logger(10,"Abort found on chain $chain");
				$continue = 0;
			} elsif ($newchain ne $chain) {
				IPFC::Log::logger(10,"First processing $newchain");
				$self->correlate_one_chain($event,$newchain);
				IPFC::Log::logger(10,"Finished processing $newchain, continuing with $chain");
			}
			
		} else {
			IPFC::Log::logger(10,"Last rule in chain $chain found");
			$continue = 0;
		}
	}
	
	IPFC::Log::logger(10,"Leaving correlate_one_chain : $chain");
	return 1;
}
		

sub correlate_one_rule($$$) {
# just try to do the sensible thing for one correlation rule
	my $self = shift;
	my $event = shift;
	my $chain = shift;
	my $i = shift;
	
	IPFC::Log::logger(10,"Entering correlate_one_rule : $chain $i");
	
	my $rule = $self->{'config'}->{$chain}->[$i];
	
	
	if (defined($rule->{'eventcondition'})) {
	  my $eventcondition = IPFC::EventCondition->new($rule->{'eventcondition'});
	  my $contextcondition = $self->{'contextcondition'};
		
	  if (! $eventcondition->evaluate($event)) {
		IPFC::Log::logger(10,"Eventcondition was false for rule $chain $i");
		return $chain;
	  }
	}
	IPFC::Log::logger(10,"Eventcondition was true for rule $chain $i");
	
	# now do some things with context condition
	
	my $substitutions = $self->expand_variables($event,$chain,$i);
	if (defined($rule->{'contextcondition'})) {
	  if (! $self->context_evaluate($substitutions,$rule->{'contextcondition'})) {
		IPFC::Log::logger(10,"Contextcondition was false for rule $chain $i");
		return $chain;
	  }
	}
		
	IPFC::Log::logger(10,"Contextcondition was true for rule $chain $i");
	
	if ($rule->{'type'} eq "suppress") {
		return $chain;
	}
		
	elsif ($rule->{'type'} eq "single") {
		my $actions = $rule->{'action'};
		$self->execute_actions($substitutions,$event,$actions);
	} 
	
	elsif ($rule->{'type'} eq "singlewithsuppress") {
		my $actions = $rule->{'action'};
		my $contextname = $chain."-".$i."-".$self->substitute_variables($substitutions,$rule->{'contextname'});
		
		my $sup_context = $self->{'contexts'}->{$contextname};
		
		if (defined($sup_context)) {
			if ($sup_context->{'timeout'} < time) {
				delete $self->{'contexts'}->{$contextname};
				
			} else {
				return $chain; # suppressed event
			}
		}
		
		$self->{'contexts'}->{$contextname}->{'timeout'} = time + $rule->{'timeout'};
		print "Executing actions for singlewithsuppress $contextname\n";
		$self->execute_actions($substitutions,$event,$rule->{'actions'});
		return $chain
	}
	
	elsif ($rule->{'type'} eq "threshold") {
		my $thresholdname = $chain."-".$i."-".$self->substitute_variables($substitutions,$rule->{'thresholdname'});
		
		my $thr_context = $self->{'contexts'}->{$thresholdname};
		
		if (defined($thr_context)) {
			$thr_context->{'number'}++;
			
			while (defined($thr_context->{'end_times'}->[0]) && 
				$thr_context->{'end_times'}->[0] < time) {
				  shift @{$thr_context->{'end_times'}};
				  $thr_context->{'number'}--;
			}
			
			if ($thr_context->{'number'} >= $rule->{'threshold'}) {
				my $actions = $rule->{'action'};
				print "Executing actions for threshold $thresholdname\n";
				$self->execute_actions($substitutions,$event,$actions);
				delete $self->{'contexts'}->{$thresholdname};
			}
		} else {
			print "Creating a new threshold $thresholdname\n";
			$self->{'contexts'}->{$thresholdname}->{'number'} = 1;
			push @{$self->{'contexts'}->{$thresholdname}->{'end_times'}}, time + $rule->{'timeout'};
			$self->{'contexts'}->{$thresholdname}->{'timeout'} = time + $rule->{'timeout'};
		}
		
		return $chain;
	}
	
	elsif ($rule->{'type'} eq "goto") {
		my $newchain = $rule->{'chain'};
		
		return $newchain;
	}
	
	elsif ($rule->{'type'} eq "endchain") {
		return undef;
	}
	
	else { 
		IPFC::Log::logger(1,"Unknown rule type in rule $chain $i");
		die;
	}
	
}

sub execute_actions($$$) {
	my $self = shift;
	my $substitutions = shift;
	my $event = shift;
	my $actions = shift;
	
	foreach my $action (@{$actions}) {
		IPFC::Log::logger(9,"Starting one action : $action->{'type'}");
		if ($action->{'type'} eq "createcontext") {
			my $contextname = substitute_variables($substitutions,$action->{'name'});
			my $time = time + $action->{'time'};
			
			my $timeoutactions = $action->{'timeoutactions'};

			delete $self->{'contexts'}->{$contextname};
						
			$self->{'contexts'}->{$contextname}->{'timeout'} = $time;
			$self->{'contexts'}->{$contextname}->{'messages'} = [];
			$self->{'contexts'}->{$contextname}->{'events'} = [ $event ];
			$self->{'contexts'}->{$contextname}->{'timeoutactions'} = $timeoutactions;
			
			IPFC::Log::logger(9,"Context $contextname created !");
		} elsif ($action->{'type'} eq "addtocontext") {
			my $contextname = substitute_variables($substitutions,$action->{'name'});
			my $message = substitute_variables($substitutions,$action->{'message'});
			push @{$self->{'contexts'}->{$contextname}->{'messages'}} , $message;
			IPFC::Log::logger(9,"Message $message added to context $contextname");
		} elsif ($action->{'type'} eq "deletecontext") {
			my $contextname = substitute_variables($substitutions,$action->{'name'});
			delete $self->{'contexts'}->{$contextname};
			IPFC::Log::logger(9,"Context $contextname deleted");
		} elsif ($action->{'type'} eq "generatealert") {
			IPFC::Log::logger(9,"Generating alert ...");
			my $alertmessage = substitute_variables($substitutions,$action->{'message'});
			IPFC::Log::logger(9,"message = $alertmessage");
		} elsif ($action->{'type'} eq "setcontext") {
			my $contextname = substitute_variables($substitutions,$action->{'name'});
			
			if (defined($action->{'time'})) {
				my $time = time + $action->{'time'};
				$self->{'contexts'}->{$contextname}->{'timeout'} = $time;
			}
			
			if (defined($action->{'timeoutactions'})) {
				$self->{'contexts'}->{$contextname}->{'timeoutactions'} = 
					$action->{'timeoutactions'};
			}
			
			IPFC::Log::logger(9,"Context $contextname updated");
		}
	}
	
	return 1;
}

sub expand_variables($$$$) {
# take an IPFC::Correlation::WithContext object
# an IPFC::Event
# and a number indicating the currently active rule in the configuration
# give back a hashref key = variable_name, value = value
#  with variable substitutions
	my $self = shift;
	my $event = shift;
	my $chain = shift;
	my $i = shift;
	my $conf_attr = $self->{'config'}->{$chain}->[$i]->{'attributes'};
	my $conf_patt = $self->{'config'}->{$chain}->[$i]->{'patterns'};
	
	my $result = {};
	
	foreach my $attr (@{$conf_attr}) {
		$result->{$attr} = $event->{$attr}->[0];
	}
	
	foreach my $string (keys %{$conf_patt}) {
	  my $sstring = substitute_variables($result,$string);
	  foreach my $pattern (keys %{$conf_patt->{$string}}) {
	    my @sub_variables = @{$conf_patt->{$string}->{$pattern}} ;	# will be defined
	    
	    $sstring =~ /$pattern/;
	    my $max = $#sub_variables;
	    
    # if anyone knows a better way to do this, i'd be grateful (don't try ${$x} ... we use strict !)
	    if ($max >= 0 ) { $result->{$sub_variables[0]} = $1 }
	    if ($max >= 1 ) { $result->{$sub_variables[1]} = $2 }
	    if ($max >= 2 ) { $result->{$sub_variables[2]} = $3 }
	    if ($max >= 3 ) { $result->{$sub_variables[3]} = $4 }
	    if ($max >= 4 ) { $result->{$sub_variables[4]} = $5 }
	    if ($max >= 5 ) { $result->{$sub_variables[5]} = $6 }
	    if ($max >= 6 ) { $result->{$sub_variables[6]} = $7 }
	    if ($max >= 7 ) { $result->{$sub_variables[7]} = $8 }
	    if ($max >= 8 ) { $result->{$sub_variables[8]} = $9 }
	    if ($max >= 9 ) { $result->{$sub_variables[9]} = $0 }    
	  }
	}
	
#	print Dumper($result);
	return $result;
	
}

sub substitute_variables($$) {
# takes a hashref as produced by expand_variables
# and a parameter (string to be substituted)
# returns the substituted string
	my $substitutions = shift;
	my $string = shift;
	
	study $string;
	
	while (my ($variable,$value) = each %{$substitutions}) {
		$string =~ s/\$\{$variable\}/$value/;
	}
	
	return $string;
}

sub context_evaluate($$);

sub context_evaluate($$) {
	my $self = shift;
	my $substitutions = shift;
	my $condition = shift;
	
	if ((!defined($condition->{'op'})) or ($condition->{'op'} = 'exists')) {
		my $name = $condition->{'name'};
		$name = substitute_variables($substitutions,$name);
		
		if (defined($self->{'contexts'}->{$name}) && ($self->{'contexts'}->{$name}->{'timeout'} < time)) {
			delete $self->{'contexts'}->{$name};
		}
		
		return defined($self->{'contexts'}->{$name});
	} elsif ($condition->{'op'} eq 'not') {
		my $name = $condition->{'name'};
		$name = substitute_variables($substitutions,$name);
		
		if ($self->{'contexts'}->{$name}->{'timeout'} < time) {
			delete $self->{'contexts'}->{$name};
		}
		
		return !exists($self->{'contexts'}->{$name});
	} elsif ($condition->{'op'} eq 'and') {
		my $term1 = $condition->{'t1'};
		my $term2 = $condition->{'t2'};
		
		return context_evaluate($substitutions,$term1) && context_evaluate($substitutions,$term2);
	} elsif ($condition->{'op'} eq 'or') {
		my $term1 = $condition->{'t1'};
		my $term2 = $condition->{'t2'};
		
		return context_evaluate($substitutions,$term1) || context_evaluate($substitutions,$term2);
	}
}

sub read_config($) {
# read the configuration file named in the parameter
# for now we hardcode a configuration here (will be changed)
	my $self = shift;
	my $config_file = shift;
	
	$self->{'contexts'} = {};
	$self->{'pending-actions'} = {};
	
	$self->{'config'}->{'main'} = [];
	my $main = $self->{'config'}->{'main'};
	
	
	$main->[0]->{'type'} = "single";
	$main->[0]->{'continue'} = 1;    # 1 = continue after this rule matches, 0 = don't continue
	$main->[0]->{'eventcondition'} = infix(attr("agent_id"),"==",const("5"));
	$main->[0]->{'attributes'} = [ "agent_id", "message", "brolleke" ];
	$main->[0]->{'patterns'}->{'${agent_id} ${message}'}->{'5 Interface (\w+) ch'} = [ 'interfacename' ];  
#	$self->{'config'}->{'main'}->[0]->{'contextcondition'} = { 'op' => 'exists', name => 'interface_${interfacename}_${agent_id}'};
	$main->[0]->{'action'}->[0] = { 'type' => 'alert' ,
						    'class' => 'IPFC::Alert',
						    'level' => 10,
						    'summary' => 'Alert ${agent_id}',
						    'fullalert' => 'Agent ${agent_id} generated an alert !'
						  } ;
	$main->[0]->{'action'}->[1] = { 'type' => 'createcontext' ,
						    'name' => 'interface_${interfacename}_${agent_id}',
						    'time' => 20
						  } ;
	
	$main->[1]->{'type'} = "single";
	$main->[1]->{'continue'} = 1;    # 1 = continue after this rule matches, 0 = don't continue
	$main->[1]->{'attributes'} = [ "agent_id", "message", "brolleke" ];
	$main->[1]->{'patterns'}->{'${agent_id} ${message}'}->{'5 Interface (\w+) ch'} = [ 'interfacename' ];  
	$main->[1]->{'contextcondition'} = { 'op' => 'exists', name => 'interface_${interfacename}_${agent_id}'};
	$main->[1]->{'action'}->[0] = { 'type' => 'alert' ,
						    'class' => 'IPFC::Alert',
						    'level' => 10,
						    'summary' => 'Alert ${agent_id}',
						    'fullalert' => 'Agent ${agent_id} generated an alert !'
						  } ;
	$main->[1]->{'action'}->[1] = { 'type' => 'createcontext' ,
						    'name' => 'interface_${interfacename}_${agent_id}',
						    'time' => 20
						  } ;

	
}
				

sub cleanup() {
# clean up, potentially erasing old contexts etc
# for now, do nothing
	return 1;
}


return 1;
