#!perl

=head1 NAME

lilith - Forward EVE log alerts to Postgresql as well as make it searchable.

=head1 SYNOPSIS

lilith [B<-c> <config>] B<-a> run

lilith [B<-c> <config>] B<-a> class_map

lilith [B<-c> <config>] B<-a> create_tables

lilith [B<-c> <config>] B<-a> dump_self

lilith [B<-c> <config>] B<-a> event [B<-t> <table>] B<--id> <row_id> [B<--raw>]
[[B<--pcap> <output file>] [B<--virani> <remote>] [B<--buffer> <buffer secodns>]]

lilith [B<-c> <config>] B<-a> event [B<-t> <table>] B<--event> <event_id> [B<--raw>]
[[B<--pcap> <output file>] [B<--virani> <remote>] [B<--buffer> <buffer secodns>]

lilith [B<-c> <config>] B<-a> extend [B<-Z>] [B<-m> <minutes>]

lilith [B<-c> <config>] B<-a> get_short_class_snmp_list

lilith [B<-c> <config>] B<-a> search [B<--output> <return>] [B<-t> <table>]
[B<-m> <minutes>] [B<--order> <clm>] [B<--limit> <int>] [B<--offset> <int>]
[B<--orderdir> <dir>] [B<--si> <src_ip>] [B<--di> <<dst_ip>] [B<--ip> <ip>]
[B<--sp> <<src_port>] [B<--dp> <<dst_port>] [B<--port> <<port>] [B<--host> <host>]
[B<--hostl>] [B<--hosN>] [B<--ih> <host>] [B<--ihl>] [B<--ihN>] [B<-i> <instance>]
[B<-il>] [B<-iN>] [B<-c> <class>] [B<--cl>] [B<--cN>] [B<-s> <sig>] [B<--sl>]
[B<--sN>] [B<--if> <if>] [B<--ifl>] [B<--ifN>] [B<--ap> <proto>] [B<--apl>] [B<--apN>]
[B<--gid> <gid>] [B<--sid> <sid>] [B<--rev> <rev>] [B<--subip> <subip>]
[B<--subhost> <subhost>] [B<--slug> <slug>] [B<--pkg> <pkg>] [B<--malscore> <malscore>]
[B<--size> <size>] [B<--target> <target>] [B<--task> <task>]

=head1 DESCRIPTION

This script runs various actions for Lilith, including search and the daemon.

=head1 GENERAL SWITCHES

=head2 -a <action>

The action to perform.

    - Default :: search

=head2 -c <config>

The config file to use.

    - Default :: /usr/local/etc/lilith.toml

=head2 -t <table>

Table to operate on.

    - Default :: suricata

=head1 ACTIONS

=head2 run

Start processing the EVE logs and daemonize.

=head2 class_map

Print a table of class mapping from long name to the short name used for display in the search results.

=head2 create_tables

Create the tables in the DB.

=head2 dump_self

Initiate Lilith and then dump it via Data::Dumper.

=head2 event

Fetches a event. The table to use can be specified via -t.

=head3 --id <row_id>

Fetch event via row ID.

=head3 --event <event_id>

Fetch the event via the event ID.

=head2 --raw

Do not decode the EVE JSON.

=head3 --pcap <file>

Fetch the remote PCAP via Virani and write it to the file. Only usable for with Suricata tables.

Default :: undef

=head3 --virani <conf>

Virani setting to pass to -r.

Default :: instance name in alert

=head3 --buffer <secs>

How many seconds to pad the start and end time with.

Default :: 60


=head2 extend

Prints a LibreNMS style extend.

=head3 -Z

Enable Gzip+Base64 LibreNMS style extend compression.

=head3 -m <minutes>

How far back to search. For the extend action, 5 minutes
is the default.

=head3 -d <dir>

The directory to write it out too.

=head2 get_short_class_snmp_list

Print a list of shorted class names for use with SNMP.

=head2 search

Search the DB. The table may be specified via -t.

The common option types for search are as below.

    - Integer :: A comma seperated list of integers to check for. Any number
                 prefixed with a ! will be negated.
    - String :: A string to check for. May be matched using like or negated via
                the proper options.
    - Complex :: A item to match.
    - IP :: An IP.

=head3 General Search Options

=head4 --output <return>

The output type.

    - Values :: table,json
    - Default :: table

=head4 -m <minute>

How far back to to in minutes.

    - Default :: 1440

    - Default, extend :: 5

=head4 --order <column>

Column to use for sorting by.

    - Default :: timestamp

    - Cape Default :: stop

=head4 --orderdir <direction>

Direction to order in.

    - Values :: ASC,DSC
    - Default :: ASC

=head3 IP Options

=head4 --si <src IP>

Source IP.

    - Default :: undef
    - Type :: IP

=head4  --di <dst IP>

Destination IP.

    - Default :: undef
    - Type :: IP

=head4  --ip <IP>

IP, either dst or src.

    - Default :: undef
    - Type :: complex IP

=head3  Port Options

=head4 --sp <src port>

Source port.

    - Default :: undef
    - Type :: integer

=head4  --dp <dst port>

Destination port.

    - Default :: undef
    - Type :: integer

=head4 -p <port>

Port, either dst or src.

    - Default :: undef
    - Type :: complex integer

=head3 Host Options

    Sagan :: Host is the sending system and instance host is the host the
             instance is running on.

    Suricata :: Host is the system the instance is running on. There is no
                instance host.

=head4 --host <host>

Host.

    - Default :: undef
    - Type :: string

=head4 --hostl

Use like for matching host.

    - Default :: undef
    - Type :: string

=head4 --hostN

Invert host matching.

    - Default :: undef
    - Type :: string

=head3 Instance Options

=head4 --ih <host>

Instance host.

    - Default :: undef
    - Type :: string

=head4 --ihl

Use like for matching instance host.

    - Default :: undef

=head4 --ihN

Invert instance host matching.

    - Default :: undef

=head3 Instance Options

=head4 -i  <instance>

Instance.

    - Default :: undef
    - Type :: string

=head4 --il

Use like for matching instance.

    - Default :: undef
    - Type :: string

=head4 --iN

Invert instance matching.

    - Default :: undef
    - Type :: string

=head3 Class Options

=head4 -c <class>

Classification.

    - Default :: undef
    - Type :: string

=head4 --cl

Use like for matching classification.

    - Default :: undef
    - Type :: string

=head4 --cN

Invert class matching.

    - Default :: undef
    - Type :: string

=head3 Signature Options

=head4 -s <sig>

Signature.

    - Default :: undef
    - Type :: string

=head4 --sl

Use like for matching signature.

    - Default :: undef
    - Type :: string

=head4 --sN

Invert signature matching.

    - Default :: undef
    - Type :: string

=head3 In Interface Options

=head4 --if <if>

Interface.

    - Default :: undef
    - Type :: string

=head4 --ifl

Use like for matching interface.

    - Default :: undef

=head4 --ifN

Invert interface matching.

    - Default :: undef

=head3 App Proto Options

=head4 --ap <proto>

App proto.

    - Default :: undef
    - Type :: string

=head4 --apl

Use like for matching app proto.

    - Default :: undef

=head4 --apN

Invert app proto matching.

    - Default :: undef

=head3 Rule Options

=head4 --gid <gid>

GID.

    - Default :: undef
    - Type :: integer

=head4 --sid <sid>

SID.

    - Default :: undef
    - Type :: integer

=head4 --rev <rev>

Rev.

    - Default :: undef
    - Type :: integer

=head3 CAPEv2 Options

=head4 --slug <slug>

The slug it was submitted with.

    - Default :: undef
    - Type :: string


=head4 --pkg <pkg>

The detopnation package used with CAPEv2.

    - Default :: undef
    - Type :: string

=head4 --malscore <malscore>

The malscore of the sample.

    - Default :: undef
    - Type :: integer

=head4 --size <size>

The size of the sample.

    - Default :: undef
    - Type :: integer

=head4 --target <target>

The the detonation target.

    - Default :: undef
    - Type :: string

=head4 --task <task>

The task ID of the run.

    - Default :: undef
    - Type :: integer

=head4 --subip <subip>

The IP the sample was submitted from.

    - Default :: undef
    - Type :: IP

=head4 --subhost <subhost>

The host the sample was submitted from.

    - Default :: undef
    - Type :: string

=head1 ENVIROMENTAL VARIABLES

=head2 Lilith_color_enable

Set to enable colorized output.

    - Default :: 0

=head2 Lilith_table_color

The L<Text::ANSITable> table color to use.

    - Default :: Text::ANSITable::Standard::NoGradation

=head2 Lilith_table_border

The L<Text::ANSITable> border type to use.

    - Default :: ASCII::None

=head2 Lilith_IP_color

Perl boolean for if IPs should be colored or not.

    - Default :: 1

=head2 Lilith_IP_private_color

ANSI color to use for private IPs.

    - Default :: bright_green

=head2 Lilith_IP_remote_color

ANSI color to use for remote IPs.

    - Default :: bright_yellow

=head2 Lilith_IP_local_color

ANSI color to use for local IPs.

    - Default :: bright_red

=head2 Lilith_timesamp_drop_micro

Perl boolean for if microseconds should be dropped or not.

    - Default :: 1

=head2 Lilith_instance_color

If the lilith instance colomn info should be colored.

    - Default :: 1

=head2 Lilith_instance_type_color

Color for the instance name.

    - Default :: bright_blue

=head2 Lilith_instance_slug_color

Color for the insance slug.

    - Default :: bright_magenta

=head2 Lilith_instance_loc_color

Color for the insance loc.

    - Default :: bright_cyan.

=head1 CONFIG FILE

The default config file is `/usr/local/etc/lilith.toml`.

    - dsn ::  A DSN connection string to be used by DBI

    - pass :: Password to use for the connection.

    - user :: User to use for the connetion.

    - class_ignore :: Array of classes to ignore.

Sub hashes are then treated as a instance. The following values are available for that.

    - eve :: The EVE file to follow.

    - type :: `sagan` or `suricata`, depending on which it is.

    - instance :: The name for the instance. If not specified the hash name is used.

Example...

    dsn="dbi:Pg:dbname=lilith;host=192.168.1.2"
    pass="WhateverYouSetAsApassword"
    user="lilith"
    # a handy one to ignore for the extend as it is spammy
    class_ignore=["Generic Protocol Command Decode"]
    
    # add a suricata instance to monitor
    [suricata-eve]
    instance="foo-pie"
    type="suricata"
    eve="/var/log/suricata/alert.json"
    
    # add a second suricata instance to monitor
    [another-eve]
    instance="foo2-pie"
    type="suricata"
    eve="/var/log/suricata/alert2.json"
    
    # add a sagan eve to monitor
    # instance name is 'foo-lae', given there is no value for instance
    [foo-lae]
    type="sagan"
    eve="/var/log/sagan/alert.json"

=cut

use strict;
use warnings;
use Getopt::Long           qw( GetOptions );
use Lilith                 ();
use TOML                   qw(from_toml to_toml);
use File::Slurp            qw(read_file);
use JSON                   qw( decode_json );
use Text::ANSITable        ();
use Term::ANSIColor        qw( color );
use Net::Server::Daemonize qw(daemonize);
use MIME::Base64           qw( encode_base64 );
use Gzip::Faster           qw( gzip );
use Data::Dumper           qw( Dumper );
use Sys::Syslog            qw( openlog syslog );
use Time::Piece::Guess     ();
use Pod::Usage             qw( pod2usage );

sub version {
	print 'lilith v. ' . $Lilith::VERSION . "\n";
}

sub help {
	&version;

	print "\n\n";

	pod2usage( -exitval => 255, -verbose => 2, -output => \*STDOUT );

}

# get the commandline options
my $help        = 0;
my $version     = 0;
my $config_file = '/usr/local/etc/lilith.toml';
my $action      = 'search';
my $src_ip;
my $dest_ip;
my $src_port;
my $dest_port;
my $alert_id;
my $table = 'suricata';
my $host;
my $host_not;
my $host_like;
my $instance_host;
my $instance_host_not;
my $instance_host_like;
my $instance;
my $instance_not;
my $instance_like;
my $class;
my $class_not;
my $class_like;
my $signature;
my $signature_not;
my $signature_like;
my $ip;
my $port;
my $go_back_minutes;
my $in_iface;
my $in_iface_not;
my $in_iface_like;
my $proto;
my $app_proto;
my $app_proto_not;
my $app_proto_like;
my $gid;
my $sid;
my $rev;
my $limit;
my $order_by;
my $order_dir;
my $offset;
my $search_output = 'table';
my $pretty;
my $columns;
my $column_set = 'default';
my $class_shortern;
my $debug;
my $event_id;
my $id;
my $decode_raw;
my $daemonize;
my $user  = 0;
my $group = 0;
my $librenms_compress;
my $dir;
my $raw;
my $virani;
my $pcap;
my $buffer = 60;
my $subip;
my $subhost;
my $slug;
my $pkg;
my $malscore;
my $size;
my $target;
my $task;
Getopt::Long::Configure('no_ignore_case');
Getopt::Long::Configure('bundling');
GetOptions(
	'version'     => \$version,
	'v'           => \$version,
	'help'        => \$help,
	'h'           => \$help,
	'config=s'    => \$config_file,
	'a=s'         => \$action,
	'si=s'        => \$src_ip,
	'sp=s'        => \$src_port,
	'di=s'        => \$dest_ip,
	'dp=s'        => \$dest_port,
	'id=s'        => \$id,
	't=s'         => \$table,
	'host=s'      => \$host,
	'hostN=s'     => \$host_not,
	'hostl'       => \$host_like,
	'ihl'         => \$instance_host_like,
	'ihN=s'       => \$instance_host_not,
	'ih=s'        => \$instance_host,
	'i=s'         => \$instance,
	'iN=s'        => \$instance_not,
	'il'          => \$instance_like,
	'c=s'         => \$class,
	'cN=s'        => \$class_not,
	'cl'          => \$class_like,
	's=s'         => \$signature,
	'sN=s'        => \$signature_not,
	'sl'          => \$signature_like,
	'ip=s'        => \$ip,
	'p=s'         => \$port,
	'm=s'         => \$go_back_minutes,
	'if=s'        => \$in_iface,
	'ifN=s'       => \$in_iface_not,
	'ifl'         => \$in_iface_like,
	'proto=s'     => \$proto,
	'ap=s'        => \$app_proto,
	'apN=s'       => \$app_proto_not,
	'apl'         => \$app_proto_like,
	'gid=s'       => \$gid,
	'sid=s'       => \$sid,
	'rev=s'       => \$rev,
	'limit=s'     => \$limit,
	'offset=s'    => \$offset,
	'order=s'     => \$order_by,
	'orderdir=s'  => \$order_dir,
	'output=s'    => \$search_output,
	'pretty'      => \$pretty,
	'columns=s'   => \$columns,
	'columnset=s' => \$column_set,
	'debug'       => \$debug,
	'event=s'     => \$event_id,
	'raw'         => \$decode_raw,
	'daemonize'   => \$daemonize,
	'user=s'      => \$user,
	'group=s'     => \$user,
	'Z'           => \$librenms_compress,
	'dir=s'       => \$dir,
	'pcap=s'      => \$pcap,
	'virani=s'    => \$virani,
	'buffer=s'    => \$buffer,
	'subip=s',    => \$subip,
	'subhost=s',  => \$subhost,
	'slug=s'      => \$slug,
	'pkg=s'       => \$pkg,
	'malscore=s'  => \$malscore,
	'size=s'      => \$size,
	'target=s'    => \$target,
	'task=s'      => \$task,
);

# print version or help if requested
if ($help) {
	&help;
	exit 42;
}
if ($version) {
	&version;
	exit 42;
}

if ( !$ENV{Lilith_color_enable} ) {
	$ENV{NO_COLOR} = 1;
}

if ( !defined( $ENV{Lilith_table_color} ) ) {
	$ENV{Lilith_table_color} = 'Text::ANSITable::Standard::NoGradation';
}

if ( !defined( $ENV{Lilith_table_border} ) ) {
	$ENV{Lilith_table_border} = 'ASCII::None';
}

if ( !defined( $ENV{Lilith_IP_color} ) ) {
	$ENV{Lilith_IP_color} = '1';
}

if ( !defined( $ENV{Lilith_IP_private_color} ) ) {
	$ENV{Lilith_IP_private_color} = 'bright_green';
}

if ( !defined( $ENV{Lilith_IP_remote_color} ) ) {
	$ENV{Lilith_IP_remote_color} = 'bright_yellow';
}

if ( !defined( $ENV{Lilith_IP_local_color} ) ) {
	$ENV{Lilith_IP_local_color} = 'bright_red';
}

if ( !defined( $ENV{Lilith_timesamp_drop_micro} ) ) {
	$ENV{Lilith_timestamp_drop_micro} = '0';
}

if ( !defined( $ENV{Lilith_timesamp_drop_offset} ) ) {
	$ENV{Lilith_timestamp_drop_offset} = '0';
}

if ( !defined( $ENV{Lilith_instance_color} ) ) {
	$ENV{Lilith_instance_color} = '1';
}

if ( !defined( $ENV{Lilith_instance_type_color} ) ) {
	$ENV{Lilith_instance_type_color} = 'bright_blue';
}

if ( !defined( $ENV{Lilith_instance_slug_color} ) ) {
	$ENV{Lilith_instance_slug_color} = 'bright_magenta';
}

if ( !defined( $ENV{Lilith_instance_loc_color} ) ) {
	$ENV{Lilith_instance_loc_color} = 'bright_cyan';
}

if ( !defined($action) ) {
	die('No action defined via -a');
}

# make sure the file exists
if ( !-f $config_file ) {
	die( '"' . $config_file . '" does not exist' );
}

if ( $buffer !~ /^\d+$/ ) {
	die( '--buffer is set to "' . $buffer . '" which is non-numeric' );
}

# read the in or die
my $toml_raw = read_file($config_file) or die 'Failed to read "' . $config_file . '"';

# read the specified config
my ( $toml, $err ) = from_toml($toml_raw);
unless ($toml) {
	die "Error parsing toml,'" . $config_file . "'" . $err;
}

my $lilith = Lilith->new(
	dsn                   => $toml->{dsn},
	user                  => $toml->{user},
	pass                  => $toml->{pass},
	debug                 => $debug,
	class_ignore          => $toml->{class_ignore},
	sid_ignore            => $toml->{sid_ignore},
	suricata_class_ignore => $toml->{suricata_class_ignore},
	suricata_sid_ignore   => $toml->{suricata_sid_ignore},
	sagan_class_ignore    => $toml->{sagan_class_ignore},
	sagan_sid_ignore      => $toml->{sagan_sid_ignore},
);

# create the tables if requested
if ( $action eq 'create_tables' ) {
	$lilith->create_tables();
	exit;
}

# dump self if asked
if ( $action eq 'dump_self' ) {
	print Dumper($lilith);
	exit 0;
}

my %files;
my @toml_keys = keys( %{$toml} );
my $int       = 0;
while ( defined( $toml_keys[$int] ) ) {
	my $item = $toml_keys[$int];

	if ( ref( $toml->{$item} ) eq "HASH" ) {

		# add the file in question
		$files{$item} = $toml->{$item};
	}

	$int++;
} ## end while ( defined( $toml_keys[$int] ) )

if ( $action eq 'run' ) {
	openlog( 'lilith', undef, 'daemon' );
	my $message = "Lilith starting...";
	syslog( 'info', $message );
	print $message. "\n";

	$message = "dsn: ";
	if ( defined( $toml->{dsn} ) ) {
		$message = $message . $toml->{dsn} . "\n";
	} else {
		$message = $message . "***undefined***\n";
	}
	syslog( 'info', $message );
	print $message. "\n";

	$message = "user: ";
	if ( defined( $toml->{user} ) ) {
		$message = $message . $toml->{user} . "\n";
	} else {
		$message = $message . "***undefined***\n";
	}
	syslog( 'info', $message );
	print $message. "\n";

	$message = "pass: ";
	if ( defined( $toml->{pass} ) ) {
		$message = $message . "***defined***\n";
	} else {
		$message = $message . "***undefined***\n";
	}
	syslog( 'info', $message );
	print $message. "\n\n";

	$message = "Configured Instances...";
	syslog( 'info', $message );
	print $message. "\n";

	foreach my $line ( split( /\n/, to_toml( \%files ) ) ) {
		syslog( 'info', $line );
		print $line. "\n";
	}

	print "\n\n";

	$message = "Calling Lilith->run now....";
	syslog( 'info', $message );
	print $message. "\n";

	if ($daemonize) {
		daemonize( $user, $group, '/var/run/lilith/pid' );
	}

	$lilith->run( files => \%files, );
} ## end if ( $action eq 'run' )

if ( $action eq 'extend' ) {

	if ( !defined($go_back_minutes) ) {
		$go_back_minutes = 5,;
	}

	my $to_return = $lilith->extend( go_back_minutes => $go_back_minutes, );
	my $json      = JSON->new;
	if ($pretty) {
		$json->canonical(1);
		$json->pretty(1);
	}

	my $raw_json = $json->encode($to_return);
	if ($librenms_compress) {
		my $compressed = encode_base64( gzip($raw_json) );
		$compressed =~ s/\n//g;
		$compressed = $compressed . "\n";
		print $compressed;
	} else {
		print $raw_json;
	}
	if ( !$pretty && !$librenms_compress ) {
		print "\n";
	}

	exit 0;
} ## end if ( $action eq 'extend' )

if ( $action eq 'search' ) {

	#
	# run the search
	#
	my $returned = $lilith->search(
		src_ip             => $src_ip,
		src_port           => $src_port,
		dest_ip            => $dest_ip,
		dest_port          => $dest_port,
		ip                 => $ip,
		port               => $port,
		alert_id           => $alert_id,
		table              => $table,
		host               => $host,
		host_not           => $host_not,
		host_like          => $host_like,
		instance_host      => $instance_host,
		instance_host_not  => $instance_host_not,
		instance_host_like => $instance_host_like,
		instance           => $instance,
		instance_not       => $instance_not,
		instance_like      => $instance_like,
		class              => $class,
		class_not          => $class_not,
		class_like         => $class_like,
		signature          => $signature,
		signature_not      => $signature_not,
		signature_like     => $signature_like,
		ip                 => $ip,
		port               => $port,
		app_proto          => $app_proto,
		app_proto_not      => $app_proto_not,
		app_proto_like     => $app_proto_like,
		proto              => $proto,
		gid                => $gid,
		sid                => $sid,
		rev                => $rev,
		order_by           => $order_by,
		order_dir          => $order_dir,
		limit              => $limit,
		offset             => $offset,
		go_back_minutes    => $go_back_minutes,
		subbed_from_ip     => $subip,
		subbed_from_host   => $subhost,
		slug               => $slug,
		pkg                => $pkg,
		malscore           => $malscore,
		size               => $size,
		target             => $target,
		task               => $task,
	);

	#
	# assemble the selected output
	#
	if ( $search_output eq 'json' ) {
		my $json = JSON->new;
		if ($pretty) {
			$json->canonical(1);
			$json->pretty(1);
		}
		print $json->encode($returned);
		if ( !$pretty ) {
			print "\n";
		}
		exit 0;
	} elsif ( $search_output eq 'table' ) {

		#
		# set the columns they had not been manually specified
		#
		if ( !defined($columns) ) {
			if ( $table eq 'suricata' ) {
				if ( $column_set eq 'default' ) {
					$columns
						= 'id,instance,in_iface,src_ip,src_port,dest_ip,dest_port,proto,app_proto,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_timestamp' ) {
					$columns
						= 'timestamp,instance,in_iface,src_ip,src_port,dest_ip,dest_port,proto,app_proto,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_event' ) {
					$columns
						= 'id,event_id,instance,in_iface,src_ip,src_port,dest_ip,dest_port,proto,app_proto,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_timestamp_event' ) {
					$columns
						= 'timestamp,event_id,instance,in_iface,src_ip,src_port,dest_ip,dest_port,proto,app_proto,signature,classification,rule_id';
				} else {
					die( '"' . $column_set . '" is not a known column set' );
				}
			} elsif ( $table eq 'sagan' ) {
				if ( $column_set eq 'default' ) {
					$columns
						= 'id,instance,src_ip,host,xff,facility,level,priority,program,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_timestamp' ) {
					$columns
						= 'timestamp,instance,src_ip,host,xff,facility,level,priority,program,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_event' ) {
					$columns
						= 'id,event_id,instance,src_ip,host,xff,facility,level,priority,program,signature,classification,rule_id';
				} elsif ( $column_set eq 'default_timestamp_event' ) {
					$columns
						= 'timestamp,event_id,instance,src_ip,host,xff,facility,level,priority,program,signature,classification,rule_id';
				} else {
					die( '"' . $column_set . '" is not a known column set' );
				}
			} elsif ( $table eq 'cape' ) {
				if ( $column_set eq 'default' ) {
					$columns = 'id,instance,slug,target,size,pkg,malscore,subbed_from_ip,subbed_from_host';
				}
			}
		} ## end if ( !defined($columns) )

		# friendly column names
		my $column_names = {
			'id'                  => 'id',
			'instance'            => 'instance',
			'host'                => 'host',
			'timestamp'           => 'timestamp',
			'event_id'            => 'event_id',
			'flow_id'             => 'flow_id',
			'in_iface'            => 'if',
			'src_ip'              => 'src_ip',
			'src_port'            => 'sport',
			'dest_ip'             => 'dest_ip',
			'dest_port'           => 'dport',
			'proto'               => 'proto',
			'app_proto'           => 'aproto',
			'flow_pkts_toserver'  => 'PtS',
			'flow_bytes_toserver' => 'BtS',
			'flow_pkts_toclient'  => 'PtC',
			'flow_bytes_toclient' => 'BtC',
			'flow_start'          => 'flow_start',
			'classification'      => 'class',
			'signature'           => 'signature',
			'gid'                 => 'gid',
			'sid'                 => 'sid',
			'rev'                 => 'rev',
			'rule_id'             => 'rule_id',
			'facility'            => 'facility',
			'level'               => 'level',
			'priority'            => 'priority',
			'program'             => 'program',
			'xff'                 => 'xff',
			'stream'              => 'stream',
			'event_id'            => 'event',
			'slug'                => 'slug',
			'target'              => 'target',
			'size'                => 'size',
			'pkg'                 => 'pkg',
			'malscore'            => 'malscore',
			'subbed_from_ip'      => 'subbed_from_ip',
			'subbed_from_host'    => 'subbed_from_host',
		};

		#
		# init the table
		#
		my $tb = Text::ANSITable->new;
		$tb->border_style( $ENV{Lilith_table_border} );
		$tb->color_theme( $ENV{Lilith_table_color} );

		my @columns_array = split( /,/, $columns );
		my $header_int    = 0;
		my $padding       = 0;
		my @headers;
		foreach my $header (@columns_array) {

			push( @headers, $column_names->{$header} );

			if   ( ( $header_int % 2 ) != 0 ) { $padding = 1; }
			else                              { $padding = 0; }

			$tb->set_column_style( $header_int, pad => $padding );

			$header_int++;
		} ## end foreach my $header (@columns_array)

		$tb->columns( \@headers );

		#
		# process each found row
		#
		my @td;
		foreach my $row ( @{$returned} ) {
			my @new_line;

			foreach my $column (@columns_array) {

				if ( $column eq 'rule_id' ) {
					$row->{rule_id} = $row->{gid} . ':' . $row->{sid} . ':' . $row->{rev};
				}

				if ( defined( $row->{$column} ) && $column eq 'rule_id' ) {
					push( @new_line, $row->{gid} . ':' . $row->{sid} . ':' . $row->{rev} );
				} elsif ( defined( $row->{$column} ) && $column eq 'classification' ) {
					push( @new_line, $lilith->get_short_class( $row->{$column} ) );
				} elsif ( defined( $row->{$column} ) && ( $column eq 'src_ip' || $column eq 'dest_ip' ) ) {
					if ( defined( $ENV{Lilith_IP_color} ) ) {
						if (   $row->{$column} =~ /^192\.168\./
							|| $row->{$column} =~ /^10\./
							|| $row->{$column} =~ /^172\.16/
							|| $row->{$column} =~ /^172\.17/
							|| $row->{$column} =~ /^172\.19/
							|| $row->{$column} =~ /^172\.19/
							|| $row->{$column} =~ /^172\.20/
							|| $row->{$column} =~ /^172\.21/
							|| $row->{$column} =~ /^172\.22/
							|| $row->{$column} =~ /^172\.23/
							|| $row->{$column} =~ /^172\.24/
							|| $row->{$column} =~ /^172\.25/
							|| $row->{$column} =~ /^172\.26/
							|| $row->{$column} =~ /^172\.26/
							|| $row->{$column} =~ /^172\.27/
							|| $row->{$column} =~ /^172\.28/
							|| $row->{$column} =~ /^172\.29/
							|| $row->{$column} =~ /^172\.30/
							|| $row->{$column} =~ /^172\.31/ )
						{
							$row->{$column} = color( $ENV{Lilith_IP_private_color} ) . $row->{$column} . color('reset');
						} elsif ( $row->{$column} =~ /^127\./ ) {
							$row->{$column} = color( $ENV{Lilith_IP_local_color} ) . $row->{$column} . color('reset');
						} else {
							$row->{$column} = color( $ENV{Lilith_IP_remote_color} ) . $row->{$column} . color('reset');
						}
					} ## end if ( defined( $ENV{Lilith_IP_color} ) )
					push( @new_line, $row->{$column} );
				} elsif ( defined( $row->{$column} ) && $column eq 'timestamp' ) {
					if ( $ENV{Lilith_timesamp_drop_micro} ) {
						$row->{$column} =~ s/\.[0-9]+//;
					}
					if ( $ENV{Lilith_timesamp_drop_offset} ) {
						$row->{$column} =~ s/\-[0-9]+$//;
					}
					push( @new_line, $row->{$column} );
				} elsif ( defined( $row->{$column} ) && $column eq 'instance' && $ENV{Lilith_instance_color} ) {
					my $color0 = color( $ENV{Lilith_instance_slug_color} );
					my $color1 = color('reset');
					my $color3 = color( $ENV{Lilith_instance_type_color} );
					my $color4 = color( $ENV{Lilith_instance_loc_color} );
					$row->{$column} =~ s/(^[A-Za-z0-9]+)\-/$color0$1$color1-/;
					$row->{$column} =~ s/\-(ids|pie|lae)$/-$color3$1$color1/;
					$row->{$column} =~ s/\-([A-Za-z0-9\-]+)\-/-$color4$1$color1/;
					push( @new_line, $row->{$column} );
				} elsif ( defined( $row->{$column} ) ) {
					push( @new_line, $row->{$column} );
				} else {
					push( @new_line, '' );
				}

			} ## end foreach my $column (@columns_array)

			push( @td, \@new_line );
		} ## end foreach my $row ( @{$returned} )

		#
		# print the table
		#
		$tb->add_rows( \@td );
		print $tb->draw;
		exit 0;
	} ## end elsif ( $search_output eq 'table' )

	# bad selection via --output
	die('No applicable output found');
} ## end if ( $action eq 'search' )

#
# gets a event
#

if ( $action eq 'event' ) {
	if ( defined($id) && defined($event_id) ) {
		die('Can not search via both --id and --event for fetching a event');
	}

	if ( !defined($id) && !defined($event_id) ) {
		die('Either --id and --event for fetching a event');
	}

	my $returned = $lilith->search(
		table    => $table,
		id       => $id,
		event_id => $event_id,
		debug    => $debug,
		no_time  => 1,
		limit    => 1,
	);

	if ( !defined( $returned->[0] ) ) {
		print "{}\n";
		exit 42;
	}

	if (   !$decode_raw
		&& defined( $returned->[0] )
		&& defined( $returned->[0]{raw} ) )
	{
		$returned->[0]{raw} = decode_json( $returned->[0]{raw} );
	}

	my $json = JSON->new;
	if ($pretty) {
		$json->canonical(1);
		$json->pretty(1);
	}
	my $raw_json = $json->encode( $returned->[0] );
	print $raw_json;
	if ( !$pretty ) {
		print "\n";
	}

	if ($pcap) {
		if ( $table ne 'suricata' ) {
			die '--pcap is only supported for Suricata';
		}

		print "\n";

		if ( !defined $returned->[0] ) {
			die('No such event, unable to fetch PCAP');
		}

		my $remote_arg = $virani;
		if ( !defined($virani) ) {
			$remote_arg = $returned->[0]{instance};
		}

		my $filter = 'host ' . $returned->[0]{src_ip} . ' and host ' . $returned->[0]{dest_ip};
		if (   defined( $returned->[0]{src_port} )
			&& defined( $returned->[0]{dest_port} )
			&& $returned->[0]{dest_port} =~ /^\d+$/
			&& $returned->[0]{src_port}  =~ /^\d+$/ )
		{
			$filter
				= $filter . ' and ( port ' . $returned->[0]{src_port} . ' or port ' . $returned->[0]{dest_port} . ' ) ';
		}

		my $start_obj;
		eval { $start_obj = Time::Piece::Guess->guess_to_object( $returned->[0]{flow_start}, 1 ); };
		if ( $@ || !defined($start_obj) ) {
			die( 'Failed to parse the start stamp,"' . $returned->[0]{flow_start} . '",' );
		}
		$start_obj = $start_obj - $buffer;

		my $end_obj;
		eval { $end_obj = Time::Piece::Guess->guess_to_object( $returned->[0]{timestamp}, 1 ); };
		if ( $@ || !defined($end_obj) ) {
			die( 'Failed to parse the timestamp,"' . $returned->[0]{timestamp} . '",' );
		}
		$end_obj = $end_obj + $buffer;

		system(
			'virani',          '-r', $remote_arg,     '-w', $pcap, '-s',
			$start_obj->epoch, '-e', $end_obj->epoch, '-f', $filter
		);
	} ## end if ($pcap)

	exit 0;
} ## end if ( $action eq 'event' )

#
# print the class_map
#

if ( $action eq 'class_map' ) {
	#
	# init the table
	#
	my $tb = Text::ANSITable->new;
	$tb->border_style( $ENV{Lilith_table_border} );
	$tb->color_theme( $ENV{Lilith_table_color} );

	my @columns = ( 'Class', 'Mapping' );

	my $header_int = 0;
	my $padding;
	my @headers;
	foreach my $header (@columns) {
		push( @headers, $header );

		if   ( ( $header_int % 2 ) != 0 ) { $padding = 1; }
		else                              { $padding = 0; }

		$tb->set_column_style( $header_int, pad => $padding );

		$header_int++;
	} ## end foreach my $header (@columns)

	$tb->columns( \@headers );

	#
	#
	#
	my @td;
	foreach my $key ( sort( keys( %{ $lilith->{class_map} } ) ) ) {
		my @row = ( $key, $lilith->{class_map}{$key} );
		push( @td, \@row );
	}

	#
	# print the table
	#
	$tb->add_rows( \@td );
	print $tb->draw;

	exit 0;
} ## end if ( $action eq 'class_map' )

#
# print short SNMP class names
#

if ( $action eq 'get_short_class_snmp_list' ) {
	my $class_list = $lilith->get_short_class_snmp_list;

	foreach my $item ( @{$class_list} ) {
		print $item. "\n";
	}

	exit 0;
}

#
# means we did not match anything
#
die( 'No matching action, -a, found for "' . $action . '"' );
