new upstream release (3.3.0); modify package compatibility for Stretch
[ossec-hids.git] / contrib / ossec2mysqld.pl
1 #!/usr/bin/perl -w
2 use strict;
3 use Socket;
4 use POSIX 'setsid';
5 use Regexp::IPv6 qw($IPv6_re);
6 # ---------------------------------------------------------------------------
7 # Author: Meir Michanie (meirm@riunx.com)
8 # Co-Author: J.A.Senger (jorge@br10.com.br)
9 # $Id$
10 # $Revision$
11 ## ---------------------------------------------------------------------------
12 # http://www.riunx.com/
13 # ---------------------------------------------------------------------------
14 #
15 # ---------------------------------------------------------------------------
16 # About this script
17 # ---------------------------------------------------------------------------
18 #
19 # "Ossec to Mysql" records the OSSEC HIDS alert logs in MySQL database.
20 # It can run as a daemon (ossec2mysqld.pl), recording in real-time the logs in database or
21 # as a simple script (ossec2mysql.pl).
22 #
23 # ---------------------------------------------------------------------------
24 # Prerequisites
25 # ---------------------------------------------------------------------------
26 #
27 # MySQL Server
28 # Perl DBD::mysql module
29 # Perl DBI module
30 #
31 # ---------------------------------------------------------------------------
32 # Installation steps
33 # ---------------------------------------------------------------------------
34
35 # 1) Create new database
36 # 2a) Run ossec2mysql.sql to create MySQL tables in your database
37 # 2b) Create BASE tables with snort tables extention
38 # 3) Create a user to access the database;
39 # 4) Copy ossec2mysql.conf to /etc/ossec2mysql.conf with 0600 permissions
40 # 3) Edit /etc/ossec2mysql.conf according to your configuration:
41 #       dbhost=localhost
42 #       database=ossecbase
43 #       debug=5
44 #       dbport=3306
45 #       dbpasswd=mypassword
46 #       dbuser=ossecuser
47 #       daemonize=0
48 #       resolve=1
49 #       
50 #
51 # ---------------------------------------------------------------------------
52 # License
53 # ---------------------------------------------------------------------------
54 #
55 # This program is free software; you can redistribute it and/or
56 # modify it under the terms of the GNU General Public License
57 # as published by the Free Software Foundation; either version 2
58 # of the License, or (at your option) any later version.
59 #
60 # This program is distributed in the hope that it will be useful,
61 # but WITHOUT ANY WARRANTY; without even the implied warranty of
62 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
63 # GNU General Public License for more details.
64 #
65 # You should have received a copy of the GNU General Public License
66 # along with this program; if not, write to the Free Software
67 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
68 #
69 # ---------------------------------------------------------------------------
70 # About OSSEC HIDS
71 # ---------------------------------------------------------------------------
72 #
73 # OSSEC HIDS is an Open Source Host-based Intrusion Detection System.
74 # It performs log analysis and correlation, integrity checking,
75 # rootkit detection, time-based alerting and active response.
76 # http://www.ossec.net
77 #
78 # ---------------------------------------------------------------------------
79
80 # ---------------------------------------------------------------------------
81 # Parameters
82 # ---------------------------------------------------------------------------
83 $SIG{TERM} = sub { &gracefulend('TERM')};
84 $SIG{INT} = sub { &gracefulend('INT')};
85 my ($RUNASDAEMON)=0;
86 my ($DAEMONLOGFILE)='/var/log/ossec2mysql.log';
87 my ($DAEMONLOGERRORFILE) = '/var/log/ossec2mysql.err';
88 my ($LOGGER)='ossec2mysql';
89 use ossecmysql;
90
91 my %conf;
92 $conf{dbhost}='localhost';
93 $conf{database}='snort';
94 $conf{debug}=5;
95 $conf{dbport}='3306';
96 $conf{dbpasswd}='password';
97 $conf{dbuser}='user';
98 $conf{daemonize}=0;
99 $conf{sensor}='sensor';
100 $conf{hids_interface}='ossec';
101 $conf{resolve}=1;
102
103
104 my($OCT) = '(?:25[012345]|2[0-4]\d|1?\d\d?)';
105
106 my($IP) = $OCT . '\.' . $OCT . '\.' . $OCT . '\.' . $OCT . '\|' . $IPv6_re;
107
108 my $VERSION="0.4";
109 my $sig_class_id=1;
110 &help() unless @ARGV;
111 my $dump=0;
112 my ($hids_id,$hids,$hids_interface,$last_cid)=(undef, 'localhost', 'ossec',0);
113 my ($tempvar,$VERBOSE)=(0,0);
114 # ---------------------------------------------------------------------------
115 #  Arguments parsing
116 # ---------------------------------------------------------------------------
117  
118 while (@ARGV){
119         $_= shift @ARGV;
120         if (m/^-d$|^--daemon$/){
121                 $conf{daemonize}=1;
122         }elsif ( m/^-h$|^--help$/){
123                 &help();
124         }elsif ( m/^-n$|^--noname$/){
125                 $conf{'resolve'}=0;
126         }elsif ( m/^-v$|^--verbose$/){
127                 $VERBOSE=1;
128         }elsif ( m/^--interface$/){
129                 $conf{hids_interface}= shift @ARGV if @ARGV; # ossec-rt/ossec-feed
130         }elsif ( m/^--sensor$/){
131                 $conf{sensor}= shift @ARGV if @ARGV; # monitor
132         }elsif ( m/^--conf$/){
133                 $conf{conf}= shift @ARGV if @ARGV; # localhost
134                 &loadconf(\%conf);
135         }elsif ( m/^--dbhost$/){
136                 $conf{dbhost}= shift @ARGV if @ARGV; # localhost
137         }elsif ( m/^--dbport$/){
138                 $conf{dbport}= shift @ARGV if @ARGV; # localhost
139         }elsif ( m/^--dbname$/){
140                 $conf{database}= shift @ARGV if @ARGV; # snort
141         }elsif ( m/^--dbuser$/){
142                 $conf{dbuser}= shift @ARGV if @ARGV; # root
143         }elsif ( m/^--dbpass$/){
144                 $conf{dbpasswd}= shift @ARGV if @ARGV; # monitor
145         }
146
147 }
148 if ($conf{dbpasswd}=~ m/^--stdin$/){
149         print "dbpassword:";
150         $conf{dbpasswd}=<>;
151         chomp $conf{dbpasswd};
152 }
153 $hids=$conf{sensor} if exists($conf{sensor});
154 $hids_interface=$conf{hids_interface} if exists($conf{hids_interface});
155
156 &daemonize() if $conf{daemonize};
157 my $dbi= ossecmysql->new(%conf) || die ("Could not connect to $conf{dbhost}:$conf{dbport}:$conf{database} as $conf{dbpasswd}\n");
158 ####
159 # SQL vars;
160 my ($query,$numrows,$row_ref);
161 ####
162 #get sensor id
163 $query= 'select sid,last_cid from sensor where hostname=? and interface=?';
164 $numrows= $dbi->execute($query,$hids,$hids_interface);
165 if (1==$numrows){
166         $row_ref=$dbi->{sth}->fetchrow_hashref;
167         $hids_id=$row_ref->{sid};
168         $last_cid=$row_ref->{last_cid};
169 }else{
170         $query="INSERT INTO sensor ( sid , hostname , interface , filter , detail , encoding , last_cid )
171 VALUES (
172 NULL , ?, ? , NULL , ? , ?, ?
173 )";
174         $numrows= $dbi->execute($query,$hids,$hids_interface,1,2,0);
175         $hids_id=$dbi->lastid();
176 }
177 $dbi->{sth}->finish;
178 &forceprintlog ("SENSOR:$hids; feed:$hids_interface; id:$hids_id; last cid:$last_cid");
179 #exit ;
180
181 my $newrecord=0;
182 my %stats;
183 my %resolv;
184 my ($timestamp,$sec,$mail,$date,$alerthost,$alerthostip,$datasource,$rule,$level,$description,
185         $srcip,$dstip,$user,$text)=();
186 my $lasttimestamp=0;
187 my $delta=0;
188 ########################################################
189 my $datepath=`date "+%Y/%b/ossec-alerts-%d.log"`;
190 my $LOG='/var/ossec/logs/alerts/'. $datepath;
191 chomp $LOG;
192 &taillog($last_cid,$LOG);
193 ###############################################################
194 sub forceprintlog(){
195         $tempvar=$VERBOSE;
196         $VERBOSE=1;
197         &printlog (@_);
198         $VERBOSE=$tempvar;
199 }
200 sub taillog {
201    my ($last_cid,$LOG)=@_;
202    my($offset, $line, $stall) = '';
203
204    $offset = (-s $LOG); # Don't start at beginning, go to end
205
206    while (1==1) {
207        sleep(1);
208         %resolv=();
209        $| = 1;
210        $stall += 1;
211         $datepath=`date "+%Y/%b/ossec-alerts-%d.log"`;
212         $LOG='/var/ossec/logs/alerts/'. $datepath;
213         chomp $LOG;
214         unless ( -f $LOG){&forceprintlog ("Error -f $LOG"); next; }
215        if ((-s $LOG) < $offset) {
216            &forceprintlog ("Log shrunk, resetting..");
217            $offset = 0;
218        }
219
220         unless (open(TAIL, $LOG)){ &forceprintlog ("Error opening $LOG: $!\n");next ;}
221
222         if (seek(TAIL, $offset, 0)) {
223            # found offset, log not rotated
224        } else {
225            # log reset, follow
226            $offset=0;
227            seek(TAIL, $offset, 0);
228        }
229        while (<TAIL>) {
230         if (m/^$/){
231                 $newrecord=1;
232                 next unless $timestamp;
233                 $alerthostip=$alerthost if $alerthost=~ m/^$IP$/;
234                 if ($alerthostip){
235                         $dstip=$alerthostip;
236                         $resolv{$alerthost}=$dstip;
237                 }else{
238                         if (exists $resolv{$alerthost}){
239                                 $dstip=$resolv{$alerthost};
240                         }else{
241                                 if ($conf{'resolve'}){
242                                         $dstip=`host $alerthost 2>/dev/null | grep 'has address\|has IPv6 address' `;
243                                         if ($dstip =~m/($IP)/ ){
244                                                 $dstip=$1;
245                                         }else{
246                                                 $dstip=$srcip;
247                                         }
248                                 }else{
249                                         $dstip=$alerthost;
250                                 }
251                                 $resolv{$alerthost}=$dstip;
252                                 
253                         }
254                 }
255                 $last_cid= &prepair2basedata(
256                         $hids_id,
257                         $last_cid,
258                         $timestamp,
259                         $sec,
260                         $mail,
261                         $date,
262                         $alerthost,
263                         $datasource,
264                         $rule,
265                         $level,
266                         $description,
267                         $srcip,
268                         $dstip,
269                         $user,
270                         $text
271                 );
272                 ($timestamp,$sec,$mail,$date,$alerthost,$alerthostip,$datasource,$rule,$level,$description,
273                 $srcip,$dstip,$user,$text)=();
274                 next ;
275         }
276         if (m/^\*\* Alert ([0-9]+).([0-9]+):(.*)$/){
277                 $timestamp=$1;
278                 if ( $timestamp == $lasttimestamp){
279                         $delta++;
280                 }else{
281                         $delta=0;
282                         $lasttimestamp=$timestamp;
283                 }
284                 $sec=$2;
285                 $mail=$3;
286                 $mail=$mail ? $mail : 'nomail';
287 #2006 Aug 29 17:19:52 firewall -> /var/log/messages
288 #2006 Aug 30 11:52:14 192.168.0.45->/var/log/secure
289 #2006 Sep 12 11:12:16 92382-Snort1 -> 172.16.176.132
290 #
291         }elsif ( m/^([0-9]+\s\w+\s[0-9]+\s[0-9]+:[0-9]+:[0-9]+)\s+(\S+)\s*->(.*)$/){
292                 $date=$1;
293                 $alerthost=$2;
294                 $datasource=$3;
295                 if ($datasource=~ m/($IP)/){
296                         $alerthost=$1;
297                         $datasource="remoted";
298                 }
299
300 #2006 Aug 29 17:33:31 (recepcao) 10.0.3.154 -> syscheck
301         }elsif ( m/^([0-9]+\s\w+\s[0-9]+\s[0-9]+:[0-9]+:[0-9]+)\s+\((.*?)\)\s+(\S+)\s*->(.*)$/){
302                 $date=$1;
303                 $alerthost=$2;
304                 $alerthostip=$3;
305                 $datasource=$4;
306         }elsif ( m/^([0-9]+\s\w+\s[0-9]+\s[0-9]+:[0-9]+:[0-9]+)\s(.*?)$/){
307                 $date=$1;
308                 $alerthost='localhost';
309                 $datasource=$2;
310         }elsif ( m/Rule: ([0-9]+) \(level ([0-9]+)\) -> (.*)$/ ){
311                 $rule=$1;
312                 $level=$2;
313                 $description= $3;
314         }elsif ( m/Src IP:/){
315                 if ( m/Src IP: (\S+)/){
316                         $srcip=$1;
317                 }else{
318                         $srcip='';
319                 }
320         }elsif ( m/User: (.*)$/){
321                 $user=$1;
322         }elsif( m/(.*)$/){
323                 $text .=$1;
324         }
325                 
326
327        } # End while read line
328        $offset=tell(TAIL);
329        close(TAIL);
330    }
331 }
332
333
334 sub prepair2basedata(){
335         my (
336                 $hids_id,
337                 $last_cid,
338                 $timestamp,
339                 $sec,
340                 $mail,
341                 $date,
342                 $alerthost,
343                 $datasource,
344                 $rule,
345                 $level,
346                 $description,
347                 $srcip,
348                 $dstip,
349                 $user,
350                 $text
351         )=@_;
352         my ($count,$query,$row_ref,$sig_id);
353 ###
354 #
355 # Get/Set signature id
356         $query = "SELECT sig_id FROM signature where sig_name=? and sig_class_id=? and sig_priority=? and sig_rev=? and sig_sid=? and sig_gid is NULL";
357         $dbi->execute($query,$description,1,$level,0,$rule);
358         $count=$dbi->{sth}->rows;
359         if ($count){
360                 $row_ref=$dbi->{sth}->fetchrow_hashref;
361                 $sig_id=$row_ref->{sig_id};
362                 &printlog ("REUSING SIGNATURE\n");
363         }else{
364                 $query="INSERT INTO signature ( sig_id , sig_name , sig_class_id , sig_priority , sig_rev , sig_sid , sig_gid )
365 VALUES (
366 NULL ,?, ? , ? , ? , ?, NULL
367 )";
368                 $dbi->execute($query,$description,1,$level,0,$rule);
369                 $sig_id = $dbi->lastid();
370         }
371 $dbi->{sth}->finish;
372 &printlog ("SIGNATURE: $sig_id\n");
373 #######
374 #
375 # Set event
376         $query="INSERT INTO event ( sid , cid , signature , timestamp )
377 VALUES (
378 ? , ? , ? ,? 
379 )";
380         $last_cid++;
381         $dbi->execute($query,$hids_id,$last_cid,$sig_id,&fixdate2base($date));
382
383 &printlog ("EVENT: ($query,$hids_id,$last_cid,$sig_id,&fixdate2base($date)\n");
384 $dbi->{sth}->finish;
385 #########
386 #
387 # Set acid_event
388         $query=" INSERT INTO acid_event ( sid , cid , signature , sig_name , sig_class_id , sig_priority , timestamp , ip_src , ip_dst , ip_proto , layer4_sport , layer4_dport )
389 VALUES (
390 ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ?, ?
391 ) ";
392         $dbi->execute($query,$hids_id,$last_cid,$sig_id,$description,1,$level,&fixdate2base($date),$srcip,$dstip,undef,undef,undef);
393 &printlog ("ACID_EVENT: ($query,$hids_id,$last_cid,$sig_id,$description,1,$level,&fixdate2base($date),$srcip,$dstip,undef,undef)\n");
394 $dbi->{sth}->finish;
395
396 #########
397 #
398 #
399 # Set data
400         $text = "** Alert $timestamp.$sec:\t$mail\n$date $alerthost -> $datasource\nRule: $rule (level $level) -> $description\nSrc IP: ($srcip)\nUser: $user\n$text";
401         $query=" INSERT INTO data ( sid , cid , data_payload ) 
402 VALUES (
403 ?,?,?)";
404         $dbi->execute($query,$hids_id,$last_cid,$text);
405 &printlog ("DATA: ($query,$hids_id,$last_cid,$text)\n");
406 $dbi->{sth}->finish;
407 ##########
408 #
409         $query="UPDATE sensor SET last_cid=? where sid=? limit 1";
410         $numrows= $dbi->execute($query,$last_cid,$hids_id);
411 # end sub
412 $dbi->{sth}->finish;
413 return $last_cid;
414 }
415
416 sub fixdate2base(){
417         my ($date)=@_;
418         $date=~ s/ Jan /-01-/;
419         $date=~ s/ Feb /-02-/;
420         $date=~ s/ Mar /-03-/;
421         $date=~ s/ Apr /-04-/;
422         $date=~ s/ May /-05-/;
423         $date=~ s/ Jun /-06-/;
424         $date=~ s/ Jul /-07-/;
425         $date=~ s/ Aug /-08-/;
426         $date=~ s/ Sep /-09-/;
427         $date=~ s/ Oct /-10-/;
428         $date=~ s/ Nov /-11-/;
429         $date=~ s/ Dec /-12-/;
430         $date=~ s/\s$//g;
431         return $date;
432 }
433 sub version(){
434         print "OSSEC report tool $VERSION\n";
435         print "Licensed under GPL\n";
436         print "Contributor Meir Michanie\n";
437 }
438
439 sub help(){
440         &version();
441         print "This tool helps you import into base the alerts generated by ossec."
442         . " More info in the doc directory .\n";
443         print "Usage:\n";
444         print "$0 [-h|--help] # This text you read now\n";
445         print "Options:\n";
446         print "\t--dbhost <hostname>\n";
447         print "\t--dbname <database>\n";
448         print "\t--dbport <[0-9]+>\n";
449         print "\t--dbpass <dbpasswd>\n";
450         print "\t--dbuser <dbuser>\n";
451         print "\t-d|--daemonize\n";
452         print "\t-n|--noname\n";
453         print "\t-v|--verbose\n";
454         print "\t--conf <ossec2based-config>\n";
455         print "\t--sensor <sensor-name>\n";
456         print "\t--interface <ifname>\n";
457         
458         exit 0;
459 }
460
461
462 sub daemonize {
463         chdir '/'               or die "Can't chdir to /: $!";
464         open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
465         open STDOUT, ">>$DAEMONLOGFILE"
466                                or die "Can't write to $DAEMONLOGFILE: $!";
467         defined(my $pid = fork) or die "Can't fork: $!";
468         if ($pid){
469                 open (PIDFILE , ">/var/run/ossec2base2.pid") ;
470                 print PIDFILE "$pid\n";
471                 close (PIDFILE);
472                 exit 0;
473         }
474         setsid                  or die "Can't start a new session: $!";
475         open STDERR, ">>$DAEMONLOGERRORFILE" or die "Can't write to $DAEMONLOGERRORFILE: $!";
476 }
477
478 sub gracefulend(){
479         my ($signal)=@_;
480         &forceprintlog ("Terminating upon signal $signal");
481         close TAIL;
482         &forceprintlog ("Daemon halted");
483         close STDOUT;
484         close STDERR;
485         exit 0;
486 }
487
488 sub printlog(){
489         return  unless $VERBOSE;
490         my (@lines)=@_;
491         foreach my $line(@lines){
492                 chomp $line;
493                 my ($date)=scalar localtime;
494                 $date=~ s/^\S+\s+(\S+.*\s[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}).*$/$1/;
495                 print "$date $LOGGER: $line\n";
496         }
497 }
498
499
500 sub loadconf(){
501         my ($hash_ref)=@_;
502         my $conf=$hash_ref->{conf};
503         unless (-f $conf) { &printlog ("ERROR: I can't find config file $conf"); exit 1;}
504         unless (open ( CONF , "$conf")){ &printlog ("ERROR: I can't open file $conf");exit 1;}
505         while (<CONF>){
506                 next if m/^$|^#/;
507                 if ( m/^(\S+)\s?=\s?(.*?)$/) {
508                         $hash_ref->{$1} = $2;
509                 }
510         }
511         close CONF;
512 }
513