#!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>]

lilith [B<-c> <config>] B<-a> event [B<-t> <table>] B<--event> <event_id> [B<--raw>]

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

lilith [B<-c> <config>] B<-a> generate_baphomet_yamls B<--dir> <dir>

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>]

=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 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.

=head2 -a generate_baphomet_yamls

Generate the YAMLs for Baphomet.

=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 wit 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.

=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

=head4 --orderdir <direction>

Direction to order in.

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

=head3 IP Options

=head4 --si <src IP>

Source IP.

    - Default :: undef
    - Type :: string

=head4  --di <dst IP>

Destination IP.

    - Default :: undef
    - Type :: string

=head4  --ip <IP>

IP, either dst or src.

    - Default :: undef
    - Type :: complex

=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

=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

=head4 --hostN

Invert host matching.

    - Default :: undef

=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

=head4 --iN

Invert instance matching.

    - Default :: undef

=head3 Class Options

=head4 -c <class>

Classification.

    - Default :: undef
    - Type :: string

=head4 --cl

Use like for matching classification.

    - Default :: undef

=head4 --cN

Invert class matching.

    - Default :: undef

=head3 Signature Options

=head4 -s <sig>

Signature.

    - Default :: undef
    - Type :: string

=head4 --sl

Use like for matching signature.

    - Default :: undef

=head4 --sN

Invert signature matching.

    - Default :: undef

=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

=head1 ENVIROMENTAL VARIABLES

=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.

=cut

use strict;
use warnings;
use Getopt::Long;
use Lilith;
use TOML        qw(from_toml to_toml);
use File::Slurp qw(read_file);
use JSON;
use Text::ANSITable;
use Term::ANSIColor;
use Net::Server::Daemonize qw(daemonize);
use MIME::Base64;
use Gzip::Faster;
use Data::Dumper;
use Sys::Syslog;

sub version {
	print "lilith v. 0.2.0\n";
}

sub help {
	&version;

	print '

--config <ini>     Config INI file.
                   Default :: /usr/local/etc/lilith.ini

-a <action>        Action to perform.
                   Default :: search

Action :: run
Description :: Runs the ingestion loop.

Action :: class_map
Description :: Display class to short class mappings.

Action :: create_tables
Description :: Creates the tables in PostgreSQL.

Action :: dump_self
Description :: Dumps $lilith via Data::Dumper

Action :: event
Description :: Fetch the information for the specified event.
               Either --id or --event is needed.

--id <row id>  Row ID to fetch.
               Default :: undef

--event <id>   Event ID to fetch.
               Default :: undef

--raw          Print the raw EVE JSON.
               Default :: undef

Action :: extend
Description :: LibreNMS style SNMP extend.

-m <minutes>    How far backt to go in minutes.
                Default :: 5

-Z              LibreNMS style compression, gzipped and
                then base64 encoded.
                Default :: undef

Action :: generate_baphomet_yamls
Description :: Generate Baphomet rule YAMLs.

--dir <dir>     Dir to output to.

Action :: search
Description :: Searches the specified table returns the results.

--ouput <return>   Return type. Either table or json.
                   Default :: table

-t <table>         Table to search. suricata/sagan
                   Default :: suricata

-m <minutes>       How far backt to go in minutes.
                   Default :: 1440

--order <clm>      Column to order by.
                   Default :: timestamp

--limit <int>      Limit to return.
                   Default :: undef

--offset <int>     Offset for a limited return.
                   Default :: undef

--orderdir <dir>   Direction to order in.
                   Default :: ASC


* IP Options


--si <src ip>    Source IP.
                 Default :: undef
                 Type :: string

--di <dst ip>    Destination IP.
                 Default :: undef
                 Type :: string

--ip <ip>        IP, either dst or src.
                 Default :: undef
                 Type :: complex


* Port Options

--sp <src port>  Source port.
                 Default :: undef
                 Type :: integer

--dp <dst port>  Destination port.
                 Default :: undef
                 Type :: integer

-p <port>        Port, either dst or src.
                 Default :: undef
                 Type :: complex


* 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.

--host <host>   Host.
                Default :: undef
                Type :: string

--hostl         Use like for matching host.
                Default :: undef

--hostN         Invert host matching.
                Default :: undef

--ih <host>     Instance host.
                Default :: undef
                Type :: string

--ihl           Use like for matching instance host.
                Default :: undef

--ihN           Invert instance host matching.
                Default :: undef


* Instance Options

-i  <instance>  Instance.
                Default :: undef
                Type :: string

--il            Use like for matching instance.
                Default :: undef

--iN            Invert instance matching.
                Default :: undef


* Class Options

-c <class>      Classification.
                Default :: undef
                Type :: string

--cl            Use like for matching classification.
                Default :: undef

--cN            Invert class matching.
                Default :: undef


* Signature Options

-s <sig>        Signature.
                Default :: undef
                Type :: string

--sl            Use like for matching signature.
                Default :: undef

--sN            Invert signature matching.
                Default :: undef


* In Interface Options

--if <if>       Interface.
                Default :: undef
                Type :: string

--ifl           Use like for matching interface.
                Default :: undef

--ifN           Invert interface matching.
                Default :: undef


* App Proto Options

--ap <proto>    App proto.
                Default :: undef
                Type :: string

--apl           Use like for matching app proto.
                Default :: undef

--apN           Invert app proto matching.
                Default :: undef


* Rule Options

--gid <gid>     GID.
                Default :: undef
                Type :: integer

--sid <sid>     SID.
                Default :: undef
                Type :: integer

--rev <rev>     Rev.
                Default :: undef
                Type :: integer

* Types

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.
';
}

# 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;
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'        => \$alert_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,
	'id=s'        => \$id,
	'event=s'     => \$event_id,
	'raw'         => \$decode_raw,
	'daemonize'   => \$daemonize,
	'user=s'      => \$user,
	'group=s'     => \$user,
	'Z'           => \$librenms_compress,
	'dir=s'       => \$dir,
);

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

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' );
}

# 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},
	sagan                 => $toml->{sagan},
	suricata              => $toml->{suricata},
	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++;
}

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 = "sagan: ";
	if ( defined( $toml->{sagan} ) ) {
		$message = $message . $toml->{sagan} . "\n";
	}
	else {
		$message = $message . "***undefined***\n";
	}
	syslog( 'info', $message );
	print $message. "\n";

	$message = "suricata: ";
	if ( defined( $toml->{suricata} ) ) {
		$message = $message . $toml->{suricata} . "\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, );
}

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;
}

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,
	);

	#
	# 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' );
				}
			}
			else {
				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' );
				}
			}
		}

		# 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',
		};

		#
		# 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++;
		}

		$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');
						}
					}
					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, '' );
				}

			}

			push( @td, \@new_line );
		}

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

	# bad selection via --output
	die('No applicable output found');
}

#
# 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');
	}

	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";
	}
	exit 0;
}

#
# 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++;
	}

	$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;
}

#
# 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;
}

#
# generate Baphomet YAMLs
#

if ( $action eq 'generate_baphomet_yamls' ) {
	$lilith->generate_baphomet_yamls($dir);

	exit 0;
}

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