File Coverage

blib/lib/Lilith.pm
Criterion Covered Total %
statement 26 304 8.5
branch 0 142 0.0
condition 0 39 0.0
subroutine 9 19 47.3
pod 7 8 87.5
total 42 512 8.2


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