
#include "config_xor.h"

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include <sys/types.h>

#ifdef USE_LOGFILE_MONITOR

#undef  FIL__
#define FIL__  _("sh_log_evalrule.c")

/* Debian/Ubuntu: libpcre3-dev */
#include <pcre.h>

#include "samhain.h"
#include "sh_pthread.h"
#include "sh_utils.h"
#include "sh_string.h"
#include "sh_log_check.h"
#include "sh_log_evalrule.h"
#include "zAVLTree.h"

/* #define DEBUG_EVALRULES */

#ifdef DEBUG_EVALRULES
void DEBUG(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap); /* flawfinder: ignore *//* we control fmt string */
  va_end(ap);
  return;
}
#else
void DEBUG(const char *fmt, ...)
{
  (void) fmt;
  return;
}
#endif

enum policies {
  EVAL_REPORT,
  EVAL_SUM
};

struct sh_ceval    /* Counter for summarizing    */
{
  sh_string   * hostname;
  sh_string   * counted_str;
  sh_string   * filename;
  unsigned long count;
  time_t        start;
  time_t        interval;
};

void sh_ceval_free(void * item)
{
  struct sh_ceval * counter = (struct sh_ceval *) item;
  if (!counter)
    return;
  sh_string_destroy(&(counter->hostname));
  sh_string_destroy(&(counter->counted_str));
  sh_string_destroy(&(counter->filename));
  SH_FREE(counter);
}

struct sh_qeval  /* Queue with definitions */
{
  sh_string       * label;
  enum policies     policy;
  int               severity;
  time_t            interval;        /* if EVAL_SUM, interval   */ 
  struct sh_qeval * next;
};

struct sh_geval  /* Group of rules (may be a single rule) */
{
  sh_string       * label;           /* label for this group    */
  pcre            * rule;            /* compiled regex for rule */
  pcre_extra      * rule_extra;
  int             * ovector;         /* captured substrings     */
  int               ovecnum;         /* how many captured       */
  int               captures;        /* (captures+1)*3 required */
  zAVLTree        * counterlist;     /* counters if EVAL_SUM    */
  struct sh_qeval * queue;           /* queue for this rule     */
  struct sh_geval * nextrule;        /* next rule in this group */
  struct sh_geval * next;            /* next group of rules     */
};

struct sh_heval  /* host-specific rules */
{
  pcre            * hostname;        /* compiled regex for hostname */
  pcre_extra      * hostname_extra;
  struct sh_geval * rulegroups;      /* list of group of rules      */
  struct sh_heval * next;
};

static struct sh_heval * hostlist  = NULL;
static struct sh_qeval * queuelist = NULL;
static struct sh_geval * grouplist = NULL;

/* These flags are set if we are within 
 * the define of a host/rule group.
 */
static struct sh_heval * host_open  = NULL;
static struct sh_geval * group_open = NULL;

int sh_eval_gend (const char * str)
{
  (void) str;
  if (group_open) {
    group_open = NULL;
    return 0;
  }
  return -1;
}

int sh_eval_gadd (const char * str)
{
  struct sh_geval * ng;
  struct sh_geval * tmp;
  pcre *  group;
  pcre_extra * group_extra;
  const char * error;
  int          erroffset;
  unsigned int nfields = 2;
  size_t       lengths[2];
  char *       new = sh_util_strdup(str);
  char **      splits = split_array(new, &nfields, ':', lengths);

  if (group_open)
    group_open = NULL;

  if (nfields != 2)
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }

  group = pcre_compile(splits[1], PCRE_NO_AUTO_CAPTURE, 
		       &error, &erroffset, NULL);
  if (!group)
    {
      sh_string * msg =  sh_string_new(0);
      sh_string_add_from_char(msg, _("Bad regex: "));
      sh_string_add_from_char(msg, splits[1]);
      
      SH_MUTEX_LOCK(mutex_thread_nolog);
      sh_error_handle(SH_ERR_ERR, FIL__, __LINE__, 0, MSG_E_SUBGEN,
		      sh_string_str(msg),
		      _("sh_eval_gadd"));
      SH_MUTEX_UNLOCK(mutex_thread_nolog);
      sh_string_destroy(&msg);
      
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }
  group_extra = NULL; /* pcre_study(group, 0, &error); */

  ng = SH_ALLOC(sizeof(struct sh_geval));
  ng->label       = sh_string_new_from_lchar(splits[0], lengths[0]);
  ng->rule        = group;
  ng->rule_extra  = group_extra;
  ng->ovector     = NULL;
  ng->ovecnum     = 0;
  ng->captures    = 0;
  ng->counterlist = NULL;
  ng->queue       = NULL;
  ng->nextrule    = NULL;
  ng->next        = NULL;

  if (!host_open)
    {
      if (0 != sh_eval_hadd("^.*"))
	{
	  pcre_free(group);
	  sh_string_destroy(&(ng->label));
	  SH_FREE(splits);
	  SH_FREE(new);
	  SH_FREE(ng);
	  return -1;
	}
    }

  /* 
   * Insert at end, to keep user-defined order 
   */ 

  if (host_open)
    {
      if (grouplist) 
	{
	  tmp = grouplist; 
	  while (tmp->next != NULL) { tmp = tmp->next; }
	  tmp->next = ng;
	} else {
	  grouplist = ng;
        }


      /* 
       * If there is an open host group, add it to its
       * rulegroups
       */

      if (host_open->rulegroups) 
	{
	  tmp = host_open->rulegroups; 
	  while (tmp->next != NULL) { tmp = tmp->next; }
	  tmp->next = ng;
	} else {
	  host_open->rulegroups = ng;
        }
    }

  group_open = ng;
  SH_FREE(splits);
  SH_FREE(new);
  return 0;
}

int sh_eval_hend (const char * str)
{
  (void) str;
  if (host_open) {
    host_open = NULL;
    return 0;
  }
  return -1;
}

int sh_eval_hadd (const char * str)
{
  struct sh_heval * nh;
  struct sh_heval * tmp;
  pcre *  host;
  pcre_extra * host_extra;
  const char * error;
  int          erroffset;

  if (host_open)
    host_open = NULL;

  host = pcre_compile(str, PCRE_NO_AUTO_CAPTURE, 
		      &error, &erroffset, NULL);
  if (!host)
    {
      sh_string * msg =  sh_string_new(0);
      sh_string_add_from_char(msg, _("Bad regex: "));
      sh_string_add_from_char(msg, str);
      
      SH_MUTEX_LOCK(mutex_thread_nolog);
      sh_error_handle(SH_ERR_ERR, FIL__, __LINE__, 0, MSG_E_SUBGEN,
		      sh_string_str(msg),
		      _("sh_eval_hadd"));
      SH_MUTEX_UNLOCK(mutex_thread_nolog);
      sh_string_destroy(&msg);

      return -1;
    }
  host_extra = NULL; /* pcre_study(host, 0, &error); */

  nh = SH_ALLOC(sizeof(struct sh_heval));
  nh->hostname = host;
  nh->hostname_extra = host_extra;
  nh->rulegroups = NULL;

  /* 
   * Insert at end, to keep user-defined order 
   */ 
  nh->next = NULL;
  if (hostlist) {
    tmp = hostlist; 
    while (tmp->next != NULL) { tmp = tmp->next; }
    tmp->next = nh;
  } else {
    hostlist = nh;
  }
  host_open = nh;

  return 0;
}

int sh_eval_qadd (const char * str)
{
  struct sh_qeval * nq;
  int     severity;
  unsigned int nfields = 4; /* label:interval:(report|sum):severity */
  size_t  lengths[4];
  char *  new = sh_util_strdup(str);
  char ** splits = split_array(new, &nfields, ':', lengths);

  if (nfields != 4)
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }

  if (strcmp(splits[2], _("sum")) && strcmp(splits[2], _("report")))
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }

  if (!strcmp(splits[2], _("sum")) && atoi(splits[1]) < 0)
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }
  
  severity = sh_error_convert_level (splits[3]);
  if (severity < 0)
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }

  nq = SH_ALLOC(sizeof(struct sh_qeval));
  nq->label = sh_string_new_from_lchar(splits[0], lengths[0]);

  DEBUG("debug: splits[2] = %s, policy = %d\n",splits[2],nq->policy); 
  if (0 == strcmp(splits[2], _("report"))) {
    nq->policy   = EVAL_REPORT;
    nq->interval = 0;
  }
  else {
    nq->policy   = EVAL_SUM;
    nq->interval = (time_t) atoi(splits[1]);
  }

  nq->severity = severity;
  nq->next     = queuelist;
  queuelist    = nq;

  SH_FREE(splits);
  SH_FREE(new);
  return 0;
}

static struct sh_qeval * find_queue(const char * str)
{
  struct sh_qeval * retval = queuelist;

  if (!str)
    return NULL;

  while (retval)
    {
      if (0 == strcmp(str, sh_string_str(retval->label)))
	break;
      retval = retval->next;
    }
  return retval;
}

int sh_eval_radd (const char * str)
{
  struct sh_geval * nr;
  struct sh_geval * tmp;
  struct sh_qeval * queue = NULL;
  pcre *  rule;
  pcre_extra * rule_extra;
  const char * error;
  int          erroffset;
  int          captures = 0;
  unsigned int nfields = 2; /* queue:regex */
  size_t       lengths[2];
  char *       new = sh_util_strdup(str);
  char **      splits = split_array(new, &nfields, ':', lengths);

  if (nfields != 2)
    {
      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }

  if (0 != strcmp(splits[0], _("trash")))
      {
	queue = find_queue(splits[0]);
	if (!queue)
	  {
	    SH_FREE(splits);
	    SH_FREE(new);
	    return -1;
	  }
      }

  rule = pcre_compile(splits[1], 0, 
		      &error, &erroffset, NULL);
  if (!rule)
    {
      sh_string * msg =  sh_string_new(0);
      sh_string_add_from_char(msg, _("Bad regex: "));
      sh_string_add_from_char(msg, splits[1]);
      
      SH_MUTEX_LOCK(mutex_thread_nolog);
      sh_error_handle(SH_ERR_ERR, FIL__, __LINE__, 0, MSG_E_SUBGEN,
		      sh_string_str(msg),
		      _("sh_eval_radd"));
      SH_MUTEX_UNLOCK(mutex_thread_nolog);
      sh_string_destroy(&msg);

      SH_FREE(splits);
      SH_FREE(new);
      return -1;
    }
  rule_extra = NULL; /* pcre_study(rule, 0, &error); */
  pcre_fullinfo(rule, rule_extra, PCRE_INFO_CAPTURECOUNT, &captures);

  DEBUG("adding rule: |%s| with %d captures\n", splits[1], captures);

  SH_FREE(splits);
  SH_FREE(new);

  nr = SH_ALLOC(sizeof(struct sh_geval));
  nr->label       = NULL;
  nr->rule        = rule;
  nr->rule_extra  = rule_extra;
  nr->captures    = captures;
  nr->ovector     = SH_ALLOC(sizeof(int) * (captures+1) * 3);
  nr->ovecnum     = 0;
  nr->counterlist = NULL;
  nr->queue       = queue;
  nr->nextrule    = NULL;
  nr->next        = NULL;

  /* 
   * If there is an open group, add it to its
   * rules
   */
  if (group_open)
    {
      if (group_open->nextrule) 
	{
	  tmp = group_open->nextrule; 
	  while (tmp->nextrule != NULL) { tmp = tmp->nextrule; } /* next -> nextrule */
	  tmp->nextrule = nr;                                    /* next -> nextrule */
	} else {
	  group_open->nextrule = nr;
	}
    }

  /* 
   * ..else, add it to the currently open host (open the
   * default host, if there is no open one)
   */
  else
    {
      if (!host_open)
	{
	  if (0 != sh_eval_hadd("^.*"))
	    {
	      SH_FREE(nr->ovector);
	      SH_FREE(nr);
	      return -1;
	    }
	}

      if (host_open)
	{
	  /* 
	   * Add rule as member to grouplist, to facilitate cleanup
	   */
#if 0
	  if (grouplist) 
	    {
	      tmp = grouplist; 
	      while (tmp->next != NULL) { tmp = tmp->next; }
	      tmp->next = nr;
	    } else {
	      grouplist = nr;
	    }
#endif

	  /* 
	   * Add rule to host rulegroups
	   */
	  if (host_open->rulegroups) 
	    {
	      /* Second, third, ... rule go to host_open->rulegroups->nextrule,
	       * since test_rules() iterates over nextrules
	       */
	      tmp = host_open->rulegroups; 
	      while (tmp->nextrule != NULL) { tmp = tmp->nextrule; }
	      tmp->nextrule = nr;
	    } 
	  else 
	    {
	      /* First rule goes to host_open->rulegroups */
	      host_open->rulegroups = nr;
	    }
	}
      else
	{
	  SH_FREE(nr->ovector);
	  SH_FREE(nr);
	  return -1;
	}
    }

  return 0;
}

void sh_eval_cleanup()
{
  struct sh_geval * tmp;

  struct sh_geval * gtmp;
  struct sh_qeval * qtmp;
  struct sh_heval * htmp;

  while (grouplist)
    {
      gtmp      = grouplist;
      grouplist = gtmp->next;

      if (gtmp->label)      sh_string_destroy(&(gtmp->label));
      if (gtmp->rule_extra) (*pcre_free)(gtmp->rule_extra);
      if (gtmp->rule)       (*pcre_free)(gtmp->rule);
      if (gtmp->counterlist)
	zAVLFreeTree(gtmp->counterlist, sh_ceval_free);
      if (gtmp->ovector)
	SH_FREE(gtmp->ovector);

      while (gtmp->nextrule)
	{
	  tmp            = gtmp->nextrule;
	  gtmp->nextrule = tmp->nextrule;

	  if (tmp->rule_extra) (*pcre_free)(tmp->rule_extra);
	  if (tmp->rule)       (*pcre_free)(tmp->rule);
	  if (tmp->counterlist)
	    zAVLFreeTree(tmp->counterlist, sh_ceval_free);
	  if (tmp->ovector)
	    SH_FREE(tmp->ovector);
	  SH_FREE(tmp);
	}

      SH_FREE(gtmp);
    }

  qtmp = queuelist;
  while (qtmp)
    {
      if (qtmp->label)      sh_string_destroy(&(qtmp->label));
      queuelist = qtmp->next;
      SH_FREE(qtmp);
      qtmp = queuelist;
    }

  htmp = hostlist;
  while (htmp)
    {
      if (htmp->hostname_extra) (*pcre_free)(htmp->hostname_extra);
      if (htmp->hostname)       (*pcre_free)(htmp->hostname);
      hostlist = htmp->next;
      SH_FREE(htmp);
      htmp = hostlist;
    }

}

/**********************************************************************
 *
 * Actual rule processing
 *
 **********************************************************************/ 

/* Test a list of rules against msg; return matched rule, with ovector 
 * filled in
 */
static struct sh_geval * test_rule (struct sh_geval * rule, sh_string *msg)
{
  int res, count;

  if (!rule)
    DEBUG("debug: (NULL) rule\n");

  if (rule && sh_string_len(msg) < (size_t)INT_MAX)
    {
      count = 1;
      do {
	DEBUG("debug: check rule %d for <%s>\n", count, msg->str);
	res = pcre_exec(rule->rule, rule->rule_extra, 
			sh_string_str(msg), (int)sh_string_len(msg), 0,
			0, rule->ovector, (3*(1+rule->captures)));
	if (res >= 0)
	  {
	    rule->ovecnum = res;
	    DEBUG("debug: rule %d matches, result = %d\n", count, res);
	    break; /* return the matching rule; ovector is filled in */
	  }
	DEBUG("debug: rule %d did not match\n", count);
	rule = rule->nextrule; ++count;
      } while (rule);
    }
  if (!rule)
    DEBUG("debug: no match found\n");
  /* If there was no match, this is NULL */
  return rule;
}
  
/* Test a (struct sh_geval *), which may be single rule or a group of rules,
 * against msg (if it's a group of rules, test against prefix first).
 */
static struct sh_geval * test_grules (struct sh_heval * host, 
				      sh_string *prefix, sh_string *msg)
{
  struct sh_geval * result = NULL;
  struct sh_geval * rules = host->rulegroups;

  if (rules && sh_string_len(prefix) < (size_t)INT_MAX)
    {
      DEBUG("debug: if rules\n");
      do {
	if(rules->label != NULL) 
	  {
	    /* this is a rule group; only groups have labels */

	    DEBUG("debug: if rules->label %s\n", rules->label->str);
	    if (pcre_exec(rules->rule, rules->rule_extra, 
			  sh_string_str(prefix), (int) sh_string_len(prefix),
			  0, 0, NULL, 0) >= 0)
	      {
		result = test_rule(rules->nextrule, msg);
		if (result)
		  break;
	      }
	  }
	else
	  {
	    /* First rule is in host->rulegroups */

	    DEBUG("debug: else (single rule)\n");
	    result = test_rule(rules, msg);
	    if (result)
	      break;
	  }
	rules = rules->next; /* next group of rules */
      } while (rules);
    }
  return result;
}

/* Top-level find_rule() function
 */
static struct sh_geval * find_rule (sh_string *host, 
				    sh_string *prefix, sh_string *msg)
{
  struct sh_geval * result = NULL;
  struct sh_heval * hlist  = hostlist;

  if (hlist && sh_string_len(host) < (size_t)INT_MAX)
    {
      do {
	if (pcre_exec(hlist->hostname, hlist->hostname_extra, 
		      sh_string_str(host), (int) sh_string_len(host), 
		      0, 0, NULL, 0) >= 0)
	  {
	    /* matching host, check rules/groups of rules */
	    result = test_grules(hlist, prefix, msg);
	    if (result)
	      break;
	  }
	hlist = hlist->next;
      } while (hlist);
    }
  return result;
}

static void msg_report(int severity, struct sh_logrecord * record)
{
  char * tmp;
  char * msg;
  char * ttt;
  SH_MUTEX_LOCK(mutex_thread_nolog);
  tmp = sh_util_safe_name (record->filename);
  msg = sh_util_safe_name_keepspace (sh_string_str(record->message));
  ttt = sh_util_safe_name_keepspace (sh_string_str(record->timestr));
  sh_error_handle (severity, FIL__, __LINE__, 0, MSG_LOGMON_REP,
		   msg,
		   ttt,
		   sh_string_str(record->host),
		   tmp);
  SH_FREE(ttt);
  SH_FREE(msg);
  SH_FREE(tmp);
  SH_MUTEX_UNLOCK(mutex_thread_nolog);
}

static void sum_report(int severity, sh_string * host, sh_string * message, sh_string * path)
{
  char * tmp;
  char * msg;
  SH_MUTEX_LOCK(mutex_thread_nolog);
  tmp = sh_util_safe_name (sh_string_str(path));
  msg = sh_util_safe_name_keepspace (sh_string_str(message));
  sh_error_handle (severity, FIL__, __LINE__, 0, MSG_LOGMON_SUM,
		   msg,
		   sh_string_str(host), 
		   tmp);
  SH_FREE(msg);
  SH_FREE(tmp);
  SH_MUTEX_UNLOCK(mutex_thread_nolog);
}

static zAVLKey sh_eval_getkey(void const *item)
{
  return ((struct sh_ceval *)item)->hostname->str;
}

/* Find the counter, or initialize one if there is none already
 */
static struct sh_ceval * find_counter(struct sh_geval * rule, 
				      sh_string * host, time_t interval)
{
  struct sh_ceval * counter;

  if (!(rule->counterlist))
    {
      DEBUG("debug: allocate new counterlist AVL tree\n");
      rule->counterlist = zAVLAllocTree(sh_eval_getkey);
    }

  counter = (struct sh_ceval *) zAVLSearch (rule->counterlist, 
					    sh_string_str(host));

  if (!counter)
    {
      DEBUG("debug: no counter found\n");

      counter = SH_ALLOC(sizeof(struct sh_ceval));
      counter->hostname    = sh_string_new_from_lchar(sh_string_str(host), 
						      sh_string_len(host));
      counter->counted_str = NULL;
      counter->filename    = NULL;
      counter->count       = 0;
      counter->start       = time(NULL);
      counter->interval    = interval;

      zAVLInsert(rule->counterlist, counter);
    }
  return counter;
		       
}

/* copy the message and replace captured substrings with '___'
 */
static sh_string * replace_captures(const sh_string * message, 
				    int * ovector, int ovecnum)
{
  sh_string * retval = sh_string_new_from_lchar(sh_string_str(message), 
						sh_string_len(message));

  if (ovecnum > 1)
    {
      retval = sh_string_replace(retval, &(ovector[2]), (ovecnum-1), "___", 3);
    }
  return retval;
}


/* process the counter for a SUM rule
 */
static int  process_counter(struct sh_ceval * counter, 
			    struct sh_geval * rule,  struct sh_logrecord * record)
{
  int retval = -1;
  time_t  now;

  if (!(counter->counted_str))
    {
      counter->counted_str = replace_captures(record->message, rule->ovector, 
					      rule->ovecnum);
      counter->filename    = sh_string_new_from_lchar(record->filename,
						      strlen(record->filename));
      DEBUG("debug: counted_str after replace: %s\n", 
	    sh_string_str(counter->counted_str)); 
    }

  ++(counter->count);
  now = time(NULL); now -= counter->start;
  DEBUG("debug: count %lu, interval %lu, time %lu\n", 
	counter->count, counter->interval, now);
  if (now >= counter->interval)
    {
      DEBUG("debug: report count\n");
      sum_report(rule->queue->severity, counter->hostname, 
		 counter->counted_str, counter->filename);
      counter->start = time(NULL);
      counter->count = 0;
    }
  return retval;
}

/* Process a rule
 */
static int  process_rule(struct sh_geval * rule, struct sh_logrecord * record)
{
  int retval = -1;
  struct sh_qeval * queue = rule->queue;

  if (queue)
    {
      DEBUG("debug: queue policy = %d found\n", queue->policy);
      if (queue->policy == EVAL_REPORT)
	{
	  DEBUG("debug: EVAL_REPORT host: %s, prefix: %s, message: %s\n",
		 sh_string_str(record->host), 
		 sh_string_str(record->prefix), 
		 sh_string_str(record->message));
	  msg_report(queue->severity, record);
	  retval = 0;
	}
      else if (queue->policy == EVAL_SUM)
	{
	  
	  struct sh_ceval * counter = 
	    find_counter(rule, record->host, queue->interval);
	  DEBUG("debug: EVAL_SUM host: %s, prefix: %s, message: %s\n",
		 sh_string_str(record->host), 
		 sh_string_str(record->prefix), 
		 sh_string_str(record->message));
	  if (counter)
	    {
	      DEBUG("debug: counter found\n");
	      retval = process_counter(counter, rule, record);
	    }
	}
    }
  else
    {
      DEBUG("debug: no queue found -- trash\n");
      /* No queue means 'trash' */
      retval = 0;
    }
  return retval;
}

#define DEFAULT_SEVERITY (-1)

int sh_eval_process_msg(struct sh_logrecord * record)
{
  static unsigned long i = 0;
  if (record)
    {
      struct sh_geval * rule = find_rule (record->host, record->prefix, 
					  record->message);

      if (rule)
	{
	  DEBUG("debug: (%lu) rule found\n", i); ++i;
	  return process_rule(rule, record);
	}
      else
	{
	  DEBUG("debug: (%lu) no rule found\n", i); ++i;
	  msg_report(DEFAULT_SEVERITY, record);
	}
      return 0;
    }
  return -1;
}

#endif
