File Coverage

blib/lib/Lilith.pm
Criterion Covered Total %
statement 29 316 9.1
branch 0 142 0.0
condition 0 39 0.0
subroutine 10 20 50.0
pod 7 8 87.5
total 46 525 8.7


line stmt bran cond sub pod time code
1             package Lilith;
2              
3 1     1   113611 use 5.006;
  1         4  
4 1     1   8 use strict;
  1         2  
  1         19  
5 1     1   4 use warnings;
  1         3  
  1         39  
6 1     1   1432 use POE qw(Wheel::FollowTail);
  1         37526  
  1         6  
7 1     1   107496 use JSON;
  1         13811  
  1         6  
8 1     1   128 use Sys::Hostname;
  1         2  
  1         47  
9 1     1   1897 use DBI;
  1         18419  
  1         70  
10 1     1   613 use Digest::SHA qw(sha256_base64);
  1         3274  
  1         103  
11 1     1   545 use File::ReadBackwards;
  1         1183  
  1         29  
12 1     1   624 use Sys::Syslog;
  1         8786  
  1         4224  
13              
14             =head1 NAME
15              
16             Lilith - Work with Suricata/Sagan EVE logs and PostgreSQL.
17              
18             =head1 VERSION
19              
20             Version 0.1.0
21              
22             =cut
23              
24             our $VERSION = '0.1.0';
25              
26             =head1 SYNOPSIS
27              
28             my $toml_raw = read_file($config_file) or die 'Failed to read "' . $config_file . '"';
29             my ( $toml, $err ) = from_toml($toml_raw);
30             unless ($toml) {
31             die "Error parsing toml,'" . $config_file . "'" . $err;
32             }
33              
34             my $lilith=Lilith->new(
35             dsn=>$toml->{dsn},
36             sagan=>$toml->{sagan},
37             suricata=>$toml->{suricata},
38             user=>$toml->{user},
39             pass=>$toml->{pass},
40             );
41              
42              
43             $lilith->create_table(
44             dsn=>$toml->{dsn},
45             sagan=>$toml->{sagan},
46             suricata=>$toml->{suricata},
47             user=>$toml->{user},
48             pass=>$toml->{pass},
49             );
50              
51             my %files;
52             my @toml_keys = keys( %{$toml} );
53             my $int = 0;
54             while ( defined( $toml_keys[$int] ) ) {
55             my $item = $toml_keys[$int];
56              
57             if ( ref( $toml->{$item} ) eq "HASH" ) {
58             # add the file in question
59             $files{$item} = $toml->{$item};
60             }
61              
62             $int++;
63             }
64              
65             $ilith->run(
66             files=>\%files,
67             );
68              
69             =head1 FUNCTIONS
70              
71             =head1 new
72              
73             Initiates it.
74              
75             my $lilith=Lilith->run(
76             dsn=>$toml->{dsn},
77             sagan=>$toml->{sagan},
78             suricata=>$toml->{suricata},
79             user=>$toml->{user},
80             pass=>$toml->{pass},
81             );
82              
83             The args taken by this are as below.
84              
85             - dsn :: The DSN to use for with DBI.
86              
87             - sagan :: Name of the table for Sagan alerts.
88             Default :: sagan_alerts
89              
90             - suricata :: Name of the table for Suricata alerts.
91             Default :: suricata_alerts
92              
93             - user :: Name for use with DBI for the DB connection.
94             Default :: lilith
95              
96             - pass :: pass for use with DBI for the DB connection.
97             Default :: undef
98              
99             - sid_ignore :: Array of SIDs to ignore for Suricata and Sagan
100             for the extend.
101             Default :: undef
102              
103             - class_ignore :: Array of classes to ignore for the
104             extend for Suricata and Sagan
105             Default :: undef
106              
107             - suricata_sid_ignore :: Array of SIDs to ignore for Suricata
108             for the extend.
109             Default :: undef
110              
111             - suricata_class_ignore :: Array of classes to ignore for the
112             extend for Suricata.
113             Default :: undef
114              
115             - sagan_sid_ignore :: Array of SIDs to ignore for Sagan for
116             the extend.
117             Default :: undef
118              
119             - sagan_class_ignore :: Array of classes to ignore for the
120             extend for Sagan.
121             Default :: undef
122              
123             =cut
124              
125             sub new {
126 0     0 0   my ( $blank, %opts ) = @_;
127              
128 0 0         if ( !defined( $opts{dsn} ) ) {
129 0           die('"dsn" is not defined');
130             }
131              
132 0 0         if ( !defined( $opts{user} ) ) {
133 0           $opts{user} = 'lilith';
134             }
135              
136 0 0         if ( !defined( $opts{sagan} ) ) {
137 0           $opts{sagan} = 'sagan_alerts';
138             }
139              
140 0 0         if ( !defined( $opts{suricata} ) ) {
141 0           $opts{suricata} = 'suricata_alerts';
142             }
143              
144 0 0         if ( !defined( $opts{sid_ignore} ) ) {
145 0           my @empty_array;
146 0           $opts{sid_ignore} = \@empty_array;
147             }
148              
149 0 0         if ( !defined( $opts{class_ignore} ) ) {
150 0           my @empty_array;
151 0           $opts{class_ignore} = \@empty_array;
152             }
153              
154 0 0         if ( !defined( $opts{suricata_sid_ignore} ) ) {
155 0           my @empty_array;
156 0           $opts{suricata_sid_ignore} = \@empty_array;
157             }
158              
159 0 0         if ( !defined( $opts{suricata_class_ignore} ) ) {
160 0           my @empty_array;
161 0           $opts{suricata_class_ignore} = \@empty_array;
162             }
163              
164 0 0         if ( !defined( $opts{sagan_sid_ignore} ) ) {
165 0           my @empty_array;
166 0           $opts{sagan_sid_ignore} = \@empty_array;
167             }
168              
169 0 0         if ( !defined( $opts{sagan_class_ignore} ) ) {
170 0           my @empty_array;
171 0           $opts{sagan_class_ignore} = \@empty_array;
172             }
173              
174             my $self = {
175             sid_ignore => $opts{sid_ignore},
176             suricata_sid_ignore => $opts{suricata_sid_ignore},
177             sagan_sid_ignore => $opts{sagan_sid_ignore},
178             class_ignore => $opts{class_ignore},
179             suricata_class_ignore => $opts{suricata_class_ignore},
180             sagan_class_ignore => $opts{sagan_class_ignore},
181             dsn => $opts{dsn},
182             user => $opts{user},
183             pass => $opts{pass},
184             sagan => $opts{sagan},
185             suricata => $opts{suricata},
186             debug => $opts{debug},
187 0           class_map => {
188             'Not Suspicious Traffic' => '!SusT',
189             'Unknown Traffic' => 'Unknown T',
190             'Attempted Information Leak' => '!IL',
191             'Information Leak' => 'IL',
192             'Large Scale Information Leak' => 'LrgSclIL',
193             'Attempted Denial of Service' => 'ADoS',
194             'Denial of Service' => 'DoS',
195             'Attempted User Privilege Gain' => 'AUPG',
196             'Unsuccessful User Privilege Gain' => '!SucUsrPG',
197             'Successful User Privilege Gain' => 'SucUsrPG',
198             'Attempted Administrator Privilege Gain' => '!SucAdmPG',
199             'Successful Administrator Privilege Gain' => 'SucAdmPG',
200             'Decode of an RPC Query' => 'DRPCQ',
201             'Executable code was detected' => 'ExeCode',
202             'A suspicious string was detected' => 'SusString',
203             'A suspicious filename was detected' => 'SusFilename',
204             'An attempted login using a suspicious username was detected' => '!LoginUsername',
205             'A system call was detected' => 'Syscall',
206             'A TCP connection was detected' => 'TCPconn',
207             'A Network Trojan was detected' => 'NetTrojan',
208             'A client was using an unusual port' => 'OddClntPrt',
209             'Detection of a Network Scan' => 'NetScan',
210             'Detection of a Denial of Service Attack' => 'DOS',
211             'Detection of a non-standard protocol or event' => 'NS PoE',
212             'Generic Protocol Command Decode' => 'GPCD',
213             'access to a potentially vulnerable web application' => 'PotVulWebApp',
214             'Web Application Attack' => 'WebAppAtk',
215             'Misc activity' => 'MiscActivity',
216             'Misc Attack' => 'MiscAtk',
217             'Generic ICMP event' => 'GenICMP',
218             'Inappropriate Content was Detected' => '!AppCont',
219             'Potential Corporate Privacy Violation' => 'PotCorpPriVio',
220             'Attempt to login by a default username and password' => '!DefUserPass',
221             'Targeted Malicious Activity was Detected' => 'TargetedMalAct',
222             'Exploit Kit Activity Detected' => 'ExpKit',
223             'Device Retrieving External IP Address Detected' => 'RetrExtIP',
224             'Domain Observed Used for C2 Detected' => 'C2domain',
225             'Possibly Unwanted Program Detected' => 'PotUnwantedProg',
226             'Successful Credential Theft Detected' => 'CredTheft',
227             'Possible Social Engineering Attempted' => 'PosSocEng',
228             'Crypto Currency Mining Activity Detected' => 'Mining',
229             'Malware Command and Control Activity Detected' => 'MalC2act',
230             'Potentially Bad Traffic' => 'PotBadTraf',
231             'Unsuccessful Admin Privilege' => 'SucAdmPG',
232             'Exploit Attempt' => 'ExpAtmp',
233             'Program Error' => 'ProgErr',
234             'Suspicious Command Execution' => 'SusProgExec',
235             'Network event' => 'NetEvent',
236             'System event' => 'SysEvent',
237             'Configuration Change' => 'ConfChg',
238             'Spam' => 'Spam',
239             'Attempted Access To File or Directory' => 'FoDAccAtmp',
240             'Suspicious Traffic' => 'SusT',
241             'Configuration Error' => 'ConfErr',
242             'Hardware Event' => 'HWevent',
243             '' => 'blankC',
244             },
245             lc_class_map => {},
246             rev_class_map => {},
247             lc_rev_class_map => {},
248             snmp_class_map => {},
249             };
250 0           bless $self;
251              
252 0           my @keys = keys( %{ $self->{class_map} } );
  0            
253 0           foreach my $key (@keys) {
254 0           my $lc_key = lc($key);
255 0           $self->{lc_class_map}{$lc_key} = $self->{class_map}{$key};
256 0           $self->{rev_class_map}{ $self->{class_map}{$key} } = $key;
257 0           $self->{lc_rev_class_map}{ lc( $self->{class_map}{$key} ) } = $key;
258 0           $self->{snmp_class_map}{$lc_key} = $self->{class_map}{$key};
259 0           $self->{snmp_class_map}{$lc_key} = $self->{class_map}{$key};
260 0           $self->{snmp_class_map}{$lc_key} =~ s/^\!/not\_/;
261             }
262              
263 0           return $self;
264             }
265              
266             =head2 run
267              
268             Start processing. This method is not expected to return.
269              
270             $lilith->run(
271             files=>{
272             foo=>{
273             type=>'suricata',
274             instance=>'foo-pie',
275             eve=>'/var/log/suricata/alerts-pie.json',
276             },
277             'foo-lae'=>{
278             type=>'sagan',
279             eve=>'/var/log/sagan/alerts-lae.json',
280             },
281             },
282             );
283              
284             One argument named 'files' is taken and it is hash of
285             hashes. The keys are below.
286              
287             - type :: Either 'suricata' or 'sagan', depending
288             on the type it is.
289              
290             - eve :: Path to the EVE file to read.
291              
292             - instance :: Instance name. If not specified the key
293             is used.
294              
295             =cut
296              
297             sub run {
298 0     0 1   my ( $self, %opts ) = @_;
299              
300 0           my $dbh;
301 0           eval { $dbh = DBI->connect_cached( $self->{dsn}, $self->{user}, $self->{pass} ); };
  0            
302 0 0         if ($@) {
303 0           warn($@);
304 0           openlog( 'lilith', undef, 'daemon' );
305 0           syslog( 'LOG_ERR', $@ );
306 0           closelog;
307             }
308              
309             # process each file
310 0           my $file_count = 0;
311 0           foreach my $item_key ( keys( %{ $opts{files} } ) ) {
  0            
312 0           my $item = $opts{files}->{$item_key};
313 0 0         if ( !defined( $item->{instance} ) ) {
314 0           warn( 'No instance name specified for ' . $item_key . ' so using that as the instance name' );
315 0           $item->{instance} = $item_key;
316             }
317              
318 0 0         if ( !defined( $item->{type} ) ) {
319 0           die( 'No type specified for ' . $item->{instance} );
320             }
321              
322 0 0         if ( !defined( $item->{eve} ) ) {
323 0           die( 'No file specified for ' . $item->{instance} );
324             }
325              
326             # create each POE session out for each EVE file we are following
327             POE::Session->create(
328             inline_states => {
329             _start => sub {
330             $_[HEAP]{tailor} = POE::Wheel::FollowTail->new(
331             Filename => $_[HEAP]{eve},
332 0     0     InputEvent => "got_log_line",
333             );
334             },
335             got_log_line => sub {
336 0     0     my $self = $_[HEAP]{self};
337 0           my $json;
338 0           eval { $json = decode_json( $_[ARG0] ) };
  0            
339 0 0         if ($@) {
340 0           return;
341             }
342              
343 0           my $dbh;
344 0           eval { $dbh = DBI->connect_cached( $self->{dsn}, $self->{user}, $self->{pass} ); };
  0            
345 0 0         if ($@) {
346 0           warn($@);
347 0           openlog( 'lilith', undef, 'daemon' );
348 0           syslog( 'LOG_ERR', $@ );
349 0           closelog;
350             }
351              
352 0           eval {
353 0 0 0       if ( defined($json)
      0        
354             && defined( $json->{event_type} )
355             && $json->{event_type} eq 'alert' )
356             {
357             # put the event ID together
358             my $event_id
359             = sha256_base64( $_[HEAP]{instance}
360             . $_[HEAP]{host}
361             . $json->{timestamp}
362             . $json->{flow_id}
363 0           . $json->{in_iface} );
364              
365             # handle if suricata
366 0 0         if ( $_[HEAP]{type} eq 'suricata' ) {
    0          
367             my $sth
368             = $dbh->prepare( 'insert into '
369             . $self->{suricata}
370 0           . ' ( instance, host, timestamp, flow_id, event_id, in_iface, src_ip, src_port, dest_ip, dest_port, proto, app_proto, flow_pkts_toserver, flow_bytes_toserver, flow_pkts_toclient, flow_bytes_toclient, flow_start, classification, signature, gid, sid, rev, raw ) '
371             . ' VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? );'
372             );
373             $sth->execute(
374             $_[HEAP]{instance}, $_[HEAP]{host},
375             $json->{timestamp}, $json->{flow_id},
376             $event_id, $json->{in_iface},
377             $json->{src_ip}, $json->{src_port},
378             $json->{dest_ip}, $json->{dest_port},
379             $json->{proto}, $json->{app_proto},
380             $json->{flow}{pkts_toserver}, $json->{flow}{bytes_toserver},
381             $json->{flow}{pkts_toclient}, $json->{flow}{bytes_toclient},
382             $json->{flow}{start}, $json->{alert}{category},
383             $json->{alert}{signature}, $json->{alert}{gid},
384             $json->{alert}{signature_id}, $json->{alert}{rev},
385 0           $_[ARG0]
386             );
387             }
388              
389             #handle if sagan
390             elsif ( $_[HEAP]{type} eq 'sagan' ) {
391             my $sth
392             = $dbh->prepare( 'insert into '
393             . $self->{sagan}
394 0           . ' ( instance, instance_host, timestamp, event_id, flow_id, in_iface, src_ip, src_port, dest_ip, dest_port, proto, facility, host, level, priority, program, proto, xff, stream, classification, signature, gid, sid, rev, raw) '
395             . ' VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? );'
396             );
397             $sth->execute(
398             $_[HEAP]{instance}, $_[HEAP]{host},
399             $json->{timestamp}, $event_id,
400             $json->{flow_id}, $json->{in_iface},
401             $json->{src_ip}, $json->{src_port},
402             $json->{dest_ip}, $json->{dest_port},
403             $json->{proto}, $json->{facility},
404             $json->{host}, $json->{level},
405             $json->{priority}, $json->{program},
406             $json->{proto}, $json->{xff},
407             $json->{stream}, $json->{alert}{category},
408             $json->{alert}{signature}, $json->{alert}{gid},
409             $json->{alert}{signature_id}, $json->{alert}{rev},
410 0           $_[ARG0],
411             );
412             }
413             }
414 0 0         if ($@) {
415 0           warn( 'SQL INSERT issue... ' . $@ );
416 0           openlog( 'lilith', undef, 'daemon' );
417 0           syslog( 'LOG_ERR', 'SQL INSERT issue... ' . $@ );
418 0           closelog;
419             }
420             }
421              
422             },
423             },
424             heap => {
425             eve => $item->{eve},
426             type => $item->{type},
427             host => hostname,
428             instance => $item->{instance},
429 0           self => $self,
430             },
431             );
432              
433             }
434              
435 0           POE::Kernel->run;
436             }
437              
438             =head2 create_tables
439              
440             Just creates the required tables in the DB.
441              
442             $lilith->create_tables;
443              
444             =cut
445              
446             sub create_tables {
447 0     0 1   my ( $self, %opts ) = @_;
448              
449 0           my $dbh = DBI->connect_cached( $self->{dsn}, $self->{user}, $self->{pass} );
450              
451             my $sth
452             = $dbh->prepare( 'create table '
453 0           . $self->{suricata} . ' ('
454             . 'id bigserial NOT NULL, '
455             . 'instance varchar(255) NOT NULL,'
456             . 'host varchar(255) NOT NULL,'
457             . 'timestamp TIMESTAMP WITH TIME ZONE NOT NULL, '
458             . 'event_id varchar(64) NOT NULL, '
459             . 'flow_id bigint, '
460             . 'in_iface varchar(255), '
461             . 'src_ip inet, '
462             . 'src_port integer, '
463             . 'dest_ip inet, '
464             . 'dest_port integer, '
465             . 'proto varchar(32), '
466             . 'app_proto varchar(255), '
467             . 'flow_pkts_toserver integer, '
468             . 'flow_bytes_toserver integer, '
469             . 'flow_pkts_toclient integer, '
470             . 'flow_bytes_toclient integer, '
471             . 'flow_start TIMESTAMP WITH TIME ZONE, '
472             . 'classification varchar(1024), '
473             . 'signature varchar(2048),'
474             . 'gid int, '
475             . 'sid bigint, '
476             . 'rev bigint, '
477             . 'raw json NOT NULL, '
478             . 'PRIMARY KEY(id) );' );
479 0           $sth->execute();
480              
481             $sth
482             = $dbh->prepare( 'create table '
483 0           . $self->{sagan} . ' ('
484             . 'id bigserial NOT NULL, '
485             . 'instance varchar(255) NOT NULL, '
486             . 'instance_host varchar(255) NOT NULL, '
487             . 'timestamp TIMESTAMP WITH TIME ZONE, '
488             . 'event_id varchar(64) NOT NULL, '
489             . 'flow_id bigint, '
490             . 'in_iface varchar(255), '
491             . 'src_ip inet, '
492             . 'src_port integer, '
493             . 'dest_ip inet, '
494             . 'dest_port integer, '
495             . 'proto varchar(32), '
496             . 'facility varchar(255), '
497             . 'host varchar(255), '
498             . 'level varchar(255), '
499             . 'priority varchar(255), '
500             . 'program varchar(255), '
501             . 'xff inet, '
502             . 'stream bigint, '
503             . 'classification varchar(1024), '
504             . 'signature varchar(2048),'
505             . 'gid int, '
506             . 'sid bigint, '
507             . 'rev bigint, '
508             . 'raw json NOT NULL, '
509             . 'PRIMARY KEY(id) );' );
510 0           $sth->execute();
511             }
512              
513             =head2 extend
514              
515             my $return=$lilith->extend(
516             go_back_minutes=>5,
517             );
518              
519             =cut
520              
521             sub extend {
522 0     0 1   my ( $self, %opts ) = @_;
523              
524 0 0         if ( !defined( $opts{go_back_minutes} ) ) {
525 0           $opts{go_back_minutes} = 5;
526             }
527              
528             #
529             # put together the hashes of items to ignore
530             #
531              
532 0           my $suricata_sid_ignore = {};
533 0           my $suricata_class_ignore = {};
534 0           my $sagan_class_ignore = {};
535 0           my $sagan_sid_ignore = {};
536              
537 0 0         if ( defined( $self->{suricata_sid_ignore}[0] ) ) {
538 0           foreach my $item ( @{ $self->{suricata_sid_ignore} } ) {
  0            
539 0           $suricata_sid_ignore->{$item} = 1;
540             }
541             }
542              
543 0 0         if ( defined( $self->{sagan_sid_ignore}[0] ) ) {
544 0           foreach my $item ( @{ $self->{sagan_sid_ignore} } ) {
  0            
545 0           $sagan_sid_ignore->{$item} = 1;
546             }
547             }
548              
549 0 0         if ( defined( $self->{sagan_class_ignore}[0] ) ) {
550 0           foreach my $item ( @{ $self->{sagan_class_ignore} } ) {
  0            
551 0           my $lc_item = lc($item);
552 0 0         if ( defined( $self->{rev_class_map}{$item} ) ) {
    0          
553 0           $sagan_class_ignore->{ $self->{rev_class_map}{$item} } = 1;
554             }
555             elsif ( defined( $self->{lc_rev_class_map}{$item} ) ) {
556 0           $sagan_class_ignore->{ $self->{lc_rev_class_map}{$item} } = 1;
557             }
558             else {
559 0           $sagan_class_ignore->{$item} = 1;
560             }
561             }
562             }
563              
564 0 0         if ( defined( $self->{suricata_class_ignore}[0] ) ) {
565 0           foreach my $item ( @{ $self->{suricata_class_ignore} } ) {
  0            
566 0           my $lc_item = lc($item);
567 0 0         if ( defined( $self->{rev_class_map}{$item} ) ) {
    0          
568 0           $suricata_class_ignore->{ $self->{rev_class_map}{$item} } = 1;
569             }
570             elsif ( defined( $self->{lc_rev_class_map}{$item} ) ) {
571 0           $suricata_class_ignore->{ $self->{lc_rev_class_map}{$item} } = 1;
572             }
573             else {
574 0           $suricata_class_ignore->{$item} = 1;
575             }
576             }
577             }
578              
579             #
580             # basic initial stuff
581             #
582              
583             # librenms return hash
584 0           my $to_return = {
585             data => { count => {}, sagan => [], suricata => [] },
586             version => 1,
587             error => '0',
588             errorString => '',
589             };
590              
591             # shove the snmp class names into the count array
592 0           my $class_list = $self->get_short_class_snmp_list;
593 0           foreach my $item ( @{$class_list} ) {
  0            
594 0           $to_return->{data}{count}{$item} = 0;
595             }
596              
597             #
598             # Do the search in eval incase of failure
599             #
600              
601 0           my $sagan_found = ();
602 0           my $suricata_found = ();
603 0           eval {
604 0           my $dbh;
605 0           eval { $dbh = DBI->connect_cached( $self->{dsn}, $self->{user}, $self->{pass} ); };
  0            
606 0 0         if ($@) {
607 0           die( 'DBI->connect_cached failure.. ' . $@ );
608             }
609              
610             #
611             # suricata SQL bit
612             #
613              
614             my $sql
615             = 'select * from '
616             . $self->{suricata}
617             . " where timestamp >= CURRENT_TIMESTAMP - interval '"
618             . $opts{go_back_minutes}
619 0           . " minutes' and host ='"
620             . hostname . "'";
621              
622 0           $sql = $sql . ';';
623 0 0         if ( $self->{debug} ) {
624 0           warn( 'SQL search "' . $sql . '"' );
625             }
626 0           my $sth = $dbh->prepare($sql);
627 0           $sth->execute();
628              
629 0           while ( my $row = $sth->fetchrow_hashref ) {
630 0           push( @{$suricata_found}, $row );
  0            
631             }
632              
633             #
634             # Sagan SQL bit
635             #
636              
637             $sql
638             = 'select * from '
639             . $self->{sagan}
640             . " where timestamp >= CURRENT_TIMESTAMP - interval '"
641             . $opts{go_back_minutes}
642 0           . " minutes' and instance_host = '"
643             . hostname . "'";
644              
645 0           $sql = $sql . ';';
646 0 0         if ( $self->{debug} ) {
647 0           warn( 'SQL search "' . $sql . '"' );
648             }
649 0           $sth = $dbh->prepare($sql);
650 0           $sth->execute();
651              
652 0           while ( my $row = $sth->fetchrow_hashref ) {
653 0           push( @{$sagan_found}, $row );
  0            
654             }
655              
656             };
657 0 0         if ($@) {
658 0           $to_return->{error} = 1;
659 0           $to_return->{errorString} = $@;
660             }
661              
662 0           foreach my $row ( @{$suricata_found} ) {
  0            
663 0           push( @{ $to_return->{data}{suricata} }, $row->{event_id} );
  0            
664 0           my $snmp_class = $self->get_short_class_snmp( $row->{classification} );
665 0           $to_return->{data}{count}{$snmp_class}++;
666             }
667              
668 0           foreach my $row ( @{$sagan_found} ) {
  0            
669 0           push( @{ $to_return->{data}{sagan} }, $row->{event_id} );
  0            
670 0           my $snmp_class = $self->get_short_class_snmp( $row->{classification} );
671 0           $to_return->{data}{count}{$snmp_class}++;
672             }
673              
674 0           return $to_return;
675             }
676              
677             =head2 get_short_class
678              
679             Get SNMP short class name for a class.
680              
681             my $short_class_name=$lilith->get_short_class($class);
682              
683             =cut
684              
685             sub get_short_class {
686 0     0 1   my ( $self, $class ) = @_;
687              
688 0 0         if ( !defined($class) ) {
689 0           return ('undefC');
690             }
691              
692 0 0         if ( defined( $self->{lc_class_map}->{ lc($class) } ) ) {
693 0           return $self->{lc_class_map}->{ lc($class) };
694             }
695              
696 0           return ('unknownC');
697             }
698              
699             =head2 get_short_class_snmp
700              
701             Get SNMP short class name for a class. This
702             is the same as the short class name, but with /^\!/
703             replaced with 'not_'.
704              
705             my $snmp_class_name=$lilith->get_short_class_snmp($class);
706              
707             =cut
708              
709             sub get_short_class_snmp {
710 0     0 1   my ( $self, $class ) = @_;
711              
712 0 0         if ( !defined($class) ) {
713 0           return ('undefC');
714             }
715              
716 0 0         if ( defined( $self->{snmp_class_map}->{ lc($class) } ) ) {
717 0           return $self->{snmp_class_map}->{ lc($class) };
718             }
719              
720 0           return ('unknownC');
721             }
722              
723             =head2 get_short_class_snmp_list
724              
725             Gets a list of short SNMP class names.
726              
727             my $snmp_classes=$lilith->get_short_class_snmp_list;
728              
729             foreach my $item (@{ $snmp_classes }){
730             print $item."\n";
731             }
732              
733             =cut
734              
735             sub get_short_class_snmp_list {
736 0     0 1   my ($self) = @_;
737              
738 0           my $snmp_classes = [ 'undefC', 'unknownC' ];
739 0           foreach my $item ( keys( %{ $self->{snmp_class_map} } ) ) {
  0            
740 0           push( @{$snmp_classes}, $self->{snmp_class_map}{$item} );
  0            
741             }
742              
743 0           return $snmp_classes;
744             }
745              
746             =head2 search
747              
748             Searches the specified table and returns a array of found rows.
749              
750             - table :: 'suricata' or 'sagan' depending on the desired table to
751             use. Will die if something other is specified. The table
752             name used is based on what was passed to new(if not the
753             default).
754             Default :: suricata
755              
756             - go_back_minutes :: How far back to search in minutes.
757             Default :: 1440
758              
759             - limit :: Limit on how many to return.
760             Default :: undef
761              
762             - offset :: Offset for when using limit.
763             Default :: undef
764              
765             - order_by :: Column to order by.
766             Default :: timetamp
767              
768             - order_dir :: Direction to order.
769             Default :: ASC
770              
771             Below are simple search items that if given will be matched via a basic equality.
772              
773             - src_ip
774             - dest_ip
775             - event_id
776              
777             # will become "and src_ip = '192.168.1.2'"
778             src_ip => '192.168.1.2',
779              
780             Below are a list of numeric items. The value taken is a array and anything
781             prefixed '!' with add as a and not equal.
782              
783             - src_port
784             - dest_port
785             - gid
786             - sid
787             - rev
788             - id
789              
790             # will become "and src_port = '22' and src_port != ''512'"
791             src_port => ['22', '!512'],
792              
793             Below are a list of string items. On top of these variables,
794             any of those with '_like' or '_not' will my modified respectively.
795              
796             - host
797             - instance_host
798             - instance
799             - class
800             - signature
801             - app_proto
802             - in_iface
803              
804             # will become "and host = 'foo.bar'"
805             host => 'foo.bar',
806              
807             # will become "and class != 'foo'"
808             class => 'foo',
809             class_not => 1,
810              
811             # will become "and instance like '%foo'"
812             instance => '%foo',
813             instance_like => 1,
814              
815             # will become "and instance not like '%foo'"
816             instance => '%foo',
817             instance_like => 1,
818             instance_not => 1,
819              
820             Below are complex items.
821              
822             - ip
823             - port
824              
825             # will become "and ( src_ip != '192.168.1.2' or dest_ip != '192.168.1.2' )"
826             ip => '192.16.1.2'
827              
828             # will become "and ( src_port != '22' or dest_port != '22' )"
829             port => '22'
830              
831             =cut
832              
833             sub search {
834 0     0 1   my ( $self, %opts ) = @_;
835              
836             #
837             # basic requirements sanity checking
838             #
839              
840 0 0         if ( !defined( $opts{table} ) ) {
841 0           $opts{table} = 'suricata';
842             }
843             else {
844 0 0 0       if ( $opts{table} ne 'suricata' && $opts{table} ne 'sagan' ) {
845 0           die( '"' . $opts{table} . '" is not a known table type' );
846             }
847             }
848              
849 0 0         if ( !defined( $opts{go_back_minutes} ) ) {
850 0           $opts{go_back_minutes} = '1440';
851             }
852             else {
853 0 0         if ( $opts{go_back_minutes} !~ /^[0-9]+$/ ) {
854 0           die( '"' . $opts{go_back_minutes} . '" for go_back_minutes is not numeric' );
855             }
856             }
857              
858 0 0 0       if ( defined( $opts{limit} ) && $opts{limit} !~ /^[0-9]+$/ ) {
859 0           die( '"' . $opts{limit} . '" is not numeric and limit needs to be numeric' );
860             }
861              
862 0 0 0       if ( defined( $opts{offset} ) && $opts{offset} !~ /^[0-9]+$/ ) {
863 0           die( '"' . $opts{offset} . '" is not numeric and offset needs to be numeric' );
864             }
865              
866 0 0 0       if ( defined( $opts{order_by} ) && $opts{order_by} !~ /^[\_a-zA-Z]+$/ ) {
867 0           die( '"' . $opts{order_by} . '" is set for order_by and it does not match /^[\_a-zA-Z]+$/' );
868             }
869              
870 0 0 0       if ( defined( $opts{order_dir} ) && $opts{order_dir} ne 'ASC' && $opts{order_dir} ne 'DESC' ) {
    0 0        
871 0           die( '"' . $opts{order_dir} . '" for order_dir must by either ASC or DESC' );
872             }
873             elsif ( !defined( $opts{order_dir} ) ) {
874 0           $opts{order_dir} = 'ASC';
875             }
876              
877 0 0         if ( !defined( $opts{order_by} ) ) {
878 0           $opts{order_by} = 'timestamp';
879             }
880              
881 0           my $table = $self->{suricata};
882 0 0         if ( $opts{table} eq 'sagan' ) {
883 0           $table = $self->{sagan};
884             }
885              
886             #
887             # make sure all the set variables are not dangerous or potentially dangerous
888             #
889              
890 0           my @to_check = (
891             'src_ip', 'src_port', 'dest_ip', 'dest_port', 'ip', 'port',
892             'host', 'host', 'instance_host', 'instance_host', 'instance', 'instance',
893             'class', 'class_like', 'signature', 'signature', 'app_proto', 'app_proto_like',
894             'proto', 'gid', 'sid', 'rev', 'id', 'event_id',
895             'in_iface'
896             );
897              
898 0           foreach my $var_to_check (@to_check) {
899 0 0 0       if ( defined( $opts{$var_to_check} ) && $opts{$var_to_check} =~ /[\\\']/ ) {
900 0           die( '"' . $opts{$var_to_check} . '" for "' . $var_to_check . '" matched /[\\\']/' );
901             }
902             }
903              
904             #
905             # makes sure order_by is sane
906             #
907              
908 0           my @order_by = (
909             'src_ip', 'src_port', 'dest_ip', 'dest_port', 'host', 'host_like',
910             'instance_host', 'instance_host', 'instance', 'instance', 'class', 'class',
911             'signature', 'signature', 'app_proto', 'app_proto', 'proto', 'gid',
912             'sid', 'rev', 'timestamp', 'id', 'in_iface'
913             );
914              
915 0           my $valid_order_by;
916              
917 0           foreach my $item (@order_by) {
918 0 0         if ( $item eq $opts{order_by} ) {
919 0           $valid_order_by = 1;
920             }
921             }
922              
923 0 0         if ( !$valid_order_by ) {
924 0           die( '"' . $opts{order_by} . '" is not a valid column name for order_by' );
925             }
926              
927             #
928             # assemble
929             #
930              
931 0           my $host = hostname;
932              
933 0           my $dbh = DBI->connect_cached( $self->{dsn}, $self->{user}, $self->{pass} );
934              
935 0           my $sql = 'select * from ' . $table . ' where';
936 0 0 0       if ( defined( $opts{no_time} ) && $opts{no_time} ) {
937 0           $sql = $sql . ' id >= 0';
938             }
939             else {
940              
941 0           $sql = $sql . " timestamp >= CURRENT_TIMESTAMP - interval '" . $opts{go_back_minutes} . " minutes'";
942             }
943              
944             #
945             # add simple items
946             #
947              
948 0           my @simple = ( 'src_ip', 'dest_ip', 'proto', 'event_id' );
949              
950 0           foreach my $item (@simple) {
951 0 0         if ( defined( $opts{$item} ) ) {
952 0           $sql = $sql . " and " . $item . " = '" . $opts{$item} . "'";
953             }
954             }
955              
956             #
957             # add numeric items
958             #
959              
960 0           my @numeric = ( 'src_port', 'dest_port', 'gid', 'sid', 'rev', 'id' );
961              
962 0           foreach my $item (@numeric) {
963 0 0         if ( defined( $opts{$item} ) ) {
964              
965             # remove and tabs or spaces
966 0           $opts{$item} =~ s/[\ \t]//g;
967 0           my @arg_split = split( /\,/, $opts{$item} );
968              
969             # process each item
970 0           foreach my $arg (@arg_split) {
971              
972             # match the start of the item
973 0 0         if ( $arg =~ /^[0-9]+$/ ) {
    0          
    0          
    0          
    0          
    0          
    0          
974 0           $sql = $sql . " and " . $item . " = '" . $arg . "'";
975             }
976             elsif ( $arg =~ /^\<\=[0-9]+$/ ) {
977 0           $arg =~ s/^\<\=//;
978 0           $sql = $sql . " and " . $item . " <= '" . $arg . "'";
979             }
980             elsif ( $arg =~ /^\<[0-9]+$/ ) {
981 0           $arg =~ s/^\<//;
982 0           $sql = $sql . " and " . $item . " < '" . $arg . "'";
983             }
984             elsif ( $arg =~ /^\>\=[0-9]+$/ ) {
985 0           $arg =~ s/^\>\=//;
986 0           $sql = $sql . " and " . $item . " >= '" . $arg . "'";
987             }
988             elsif ( $arg =~ /^\>[0-9]+$/ ) {
989 0           $arg =~ s/^\>\=//;
990 0           $sql = $sql . " and " . $item . " > '" . $arg . "'";
991             }
992             elsif ( $arg =~ /^\![0-9]+$/ ) {
993 0           $arg =~ s/^\!//;
994 0           $sql = $sql . " and " . $item . " != '" . $arg . "'";
995             }
996             elsif ( $arg =~ /^$/ ) {
997              
998             # only exists for skipping when some one has passes something starting
999             # with a ,, ending with a,, or with ,, in it.
1000             }
1001             else {
1002             # if we get here, it means we don't have a valid use case for what ever was passed and should error
1003 0           die( '"' . $arg . '" does not appear to be a valid item for a numeric search for the ' . $item );
1004             }
1005             }
1006             }
1007             }
1008              
1009             #
1010             # handle string items
1011             #
1012              
1013 0           my @strings = ( 'host', 'instance_host', 'instance', 'class', 'signature', 'app_proto', 'in_iface' );
1014              
1015 0           foreach my $item (@strings) {
1016 0 0         if ( defined( $opts{$item} ) ) {
1017 0 0 0       if ( defined( $opts{ $item . '_like' } ) && $opts{ $item . '_like' } ) {
1018 0 0 0       if ( defined( $opts{$item} . '_not' ) && !$opts{ $item . '_not' } ) {
1019 0           $sql = $sql . " and " . $item . " like '" . $opts{$item} . "'";
1020             }
1021             else {
1022 0           $sql = $sql . " and " . $item . " not like '" . $opts{$item} . "'";
1023             }
1024             }
1025             else {
1026 0 0 0       if ( defined( $opts{$item} . '_not' ) && !$opts{ $item . '_not' } ) {
1027 0           $sql = $sql . " and " . $item . " = '" . $opts{$item} . "'";
1028             }
1029             else {
1030 0           $sql = $sql . " and " . $item . " != '" . $opts{$item} . "'";
1031             }
1032             }
1033             }
1034             }
1035              
1036             #
1037             # more complex items
1038             #
1039              
1040 0 0         if ( defined( $opts{ip} ) ) {
1041 0           $sql = $sql . " and ( src_ip = '" . $opts{ip} . "' or dest_ip = '" . $opts{ip} . "' )";
1042             }
1043              
1044 0 0         if ( defined( $opts{port} ) ) {
1045 0           $sql = $sql . " and ( src_port = '" . $opts{port} . "' or dest_port = '" . $opts{port} . "' )";
1046             }
1047              
1048             #
1049             # finalize the SQL query... ORDER, LIMIT, and OFFSET
1050             #
1051              
1052 0 0         if ( defined( $opts{order_by} ) ) {
1053 0           $sql = $sql . ' ORDER BY ' . $opts{order_by} . ' ' . $opts{order_dir};
1054             }
1055              
1056 0 0         if ( defined( $opts{linit} ) ) {
1057 0           $sql = $sql . ' LIMIT ' . $opts{limit};
1058             }
1059              
1060 0 0         if ( defined( $opts{offset} ) ) {
1061 0           $sql = $sql . ' OFFSET ' . $opts{offset};
1062             }
1063              
1064             #
1065             # run the query
1066             #
1067              
1068 0           $sql = $sql . ';';
1069 0 0         if ( $self->{debug} ) {
1070 0           warn( 'SQL search "' . $sql . '"' );
1071             }
1072 0           my $sth = $dbh->prepare($sql);
1073 0           $sth->execute();
1074              
1075 0           my $found = ();
1076 0           while ( my $row = $sth->fetchrow_hashref ) {
1077 0           push( @{$found}, $row );
  0            
1078             }
1079              
1080 0           $dbh->disconnect;
1081              
1082 0           return $found;
1083             }
1084              
1085             =head1 AUTHOR
1086              
1087             Zane C. Bowers-Hadley, C<< <vvelox at vvelox.net> >>
1088              
1089             =head1 BUGS
1090              
1091             Please report any bugs or feature requests to C<bug-lilith at rt.cpan.org>, or through
1092             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Lilith>. I will be notified, and then you'll
1093             automatically be notified of progress on your bug as I make changes.
1094              
1095              
1096              
1097              
1098             =head1 SUPPORT
1099              
1100             You can find documentation for this module with the perldoc command.
1101              
1102             perldoc Lilith
1103              
1104              
1105             You can also look for information at:
1106              
1107             =over 4
1108              
1109             =item * RT: CPAN's request tracker (report bugs here)
1110              
1111             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Lilith>
1112              
1113             =item * CPAN Ratings
1114              
1115             L<https://cpanratings.perl.org/d/Lilith>
1116              
1117             =item * Search CPAN
1118              
1119             L<https://metacpan.org/release/Lilith>
1120              
1121             =back
1122              
1123              
1124             =head1 ACKNOWLEDGEMENTS
1125              
1126              
1127             =head1 LICENSE AND COPYRIGHT
1128              
1129             This software is Copyright (c) 2022 by Zane C. Bowers-Hadley.
1130              
1131             This is free software, licensed under:
1132              
1133             The Artistic License 2.0 (GPL Compatible)
1134              
1135              
1136             =cut
1137              
1138             1; # End of Lilith