# Warden.pm # # Copyright (C) 2011-2015 Cesnet z.s.p.o # # Use of this source is governed by a BSD-style license, see LICENSE file. package Warden; use strict; use warnings; use DBI; use DBD::mysql; use Sys::Syslog qw(:DEFAULT setlogsock); Sys::Syslog::setlogsock('unix'); use Net::CIDR::Lite; use DateTime; use MIME::Base64; use Crypt::X509; use SOAP::Lite; use Carp; use File::Basename; use lib File::Basename::dirname(__FILE__); use WardenCommon; ################################################################################ # VARIABLES ################################################################################ our $VERSION = "2.2"; our $FILENAME = File::Basename::basename(__FILE__); my $lib = File::Basename::dirname(__FILE__); my $etc = "$lib/../etc"; ################################################################################ # READING OF CONFIGURATION VARIABLES ################################################################################ # load server configuration my $conf_file = "$etc/warden-server.conf"; WardenCommon::loadConf($conf_file); ################################################################################ # DB CONNECT ################################################################################ # create database handler our $DBH = DBI->connect("DBI:mysql:database=$WardenCommon::DB_NAME;host=$WardenCommon::DB_HOST", $WardenCommon::DB_USER, $WardenCommon::DB_PASS, {RaiseError => 1, mysql_auto_reconnect => 1}) || die "Could not connect to database '$WardenCommon::DB_NAME' at '$WardenCommon::DB_HOST': $DBI::errstr"; ################################################################################ # FUNCTIONS ################################################################################ #------------------------------------------------------------------------------- # sendMsg - wrapper for more complex WardenCommon::sendMsg function #------------------------------------------------------------------------------- sub sendMsg { my $severity = shift; my $syslog_msg = shift; my $soap_msg = shift; # send message via syslog WardenCommon::sendMsg($WardenCommon::SYSLOG, $WardenCommon::SYSLOG_VERBOSE, $WardenCommon::SYSLOG_FACILITY, $severity, $syslog_msg, $soap_msg, $FILENAME); } #------------------------------------------------------------------------------- # getAltNames - parse Alternate names from SSL certifiate #------------------------------------------------------------------------------- sub getAltNames { my @an_array; my $cn = $ENV{'SSL_CLIENT_S_DN_CN'}; push(@an_array, $DBH->quote($cn)); my @a = split("\n", $ENV{'SSL_CLIENT_CERT'}); pop @a; shift @a; my $der = decode_base64(join("", @a)); my $decoded= Crypt::X509->new(cert => $der); # obtain Subject Alternative Names from SSL certificate (if any exist) if (defined $decoded->SubjectAltName) { foreach my $tmp (@{$decoded->SubjectAltName}) { if($tmp =~ s/dNSName=//){ push(@an_array, $DBH->quote($tmp)); } } } my $alt_names = join(',', @an_array); return $alt_names; } #------------------------------------------------------------------------------- # authorizeClient - authorize client by CN,AN and source IP range #------------------------------------------------------------------------------- sub authorizeClient { my ($alt_names, $ip, $service_type, $client_type, $function_name) = @_; my ($sth, $rc); # check if client is valid and obtain client_id, ip_net_client and receive_own_events if($function_name eq 'saveNewEvent') { $sth = $DBH->prepare("SELECT client_id, ip_net_client, receive_own_events FROM clients WHERE hostname IN ($alt_names) AND service = ? AND client_type = ? AND valid = 't' ORDER BY SUBSTRING_INDEX(ip_net_client,'/', -1) DESC;"); } elsif($function_name eq 'getNewEvents') { $sth = $DBH->prepare("SELECT client_id, ip_net_client, receive_own_events FROM clients WHERE hostname IN ($alt_names) AND (type = ? OR type = '_any_') AND client_type = ? AND valid = 't' ORDER BY SUBSTRING_INDEX(ip_net_client,'/', -1) DESC;"); } elsif($function_name eq 'getClientInfo') { $sth = $DBH->prepare("SELECT client_id, ip_net_client, receive_own_events FROM clients WHERE hostname IN ($alt_names) AND valid = 't' ORDER BY SUBSTRING_INDEX(ip_net_client,'/', -1) DESC;"); } elsif($function_name eq 'getLastId') { $sth = $DBH->prepare("SELECT client_id, ip_net_client, receive_own_events FROM clients WHERE hostname IN ($alt_names) AND client_type = 'r' AND valid = 't' ORDER BY SUBSTRING_INDEX(ip_net_client,'/', -1) DESC;"); } # check if db handler is defined unless (defined $sth) { sendMsg("err", "Cannot prepare authorization statement in function 'authorizeClient': $DBH->errstr", "Internal 'prepare' server error"); } # execute query for two or none params functions if ($function_name eq 'saveNewEvent' || $function_name eq 'getNewEvents') { $rc = $sth->execute($service_type, $client_type); unless ($rc) { sendMsg("err", "Cannot execute authorization statement in function 'authorizeClient': $DBH->errstr", "Internal 'execute' server error"); } } else { $rc = $sth->execute; unless ($rc) { sendMsg("err", "Cannot execute authorization statement in function 'authorizeClient': $DBH->errstr", "Internal 'execute' server error"); } } # obtain registration information about clients my ($client_id, $ip_net_client, $receive_own, $ip_net_client_list); my $correct_ip_source = 0; my %ret; while(($client_id, $ip_net_client, $receive_own) = $sth->fetchrow()) { my $ip_net_client_list = Net::CIDR::Lite->new->add($ip_net_client); $ret{'client_id'} = $client_id; $ret{'receive_own'} = $receive_own; if ($ip_net_client_list->bin_find($ip)) { $correct_ip_source = 1; last; } } # check if client is registered if ($sth->rows == 0) { sendMsg("err", "Unauthorized access to function '$function_name' from [IP: '$ip'; CN(AN): $alt_names; Client_type: '$client_type'; Service/Type: '$service_type'] - client is not registered at Warden server '$ENV{'SERVER_NAME'}'", "Access denied - client is not registered at Warden server '$ENV{'SERVER_NAME'}'"); return undef; } # check if client has IP from registered CIDR unless ($correct_ip_source) { sendMsg ("err", "Unauthorized access to function '$function_name' from [IP: '$ip'; CN(AN): $alt_names; Client_type: '$client_type'; Service/Type: '$service_type'] - access to Warden server '$ENV{'SERVER_NAME'}' from another subnet than '$ip_net_client'", "Access denied - access to Warden server '$ENV{'SERVER_NAME'}' from unauthorized subnet '$ip_net_client'"); return undef; } return %ret; } # END of authorizeClient ################################################################################ # SOAP Functions ################################################################################ #----------------------------------------------------------------------------- # saveNewEvent - save new received event into database #----------------------------------------------------------------------------- sub saveNewEvent { my ($class, $data) = @_; my ($sth, $rc); # client network information my $cn = $ENV{'SSL_CLIENT_S_DN_CN'}; my $alt_names = getAltNames(undef); my $ip = $ENV{'REMOTE_ADDR'}; # variables defined by server my $function_name = 'saveNewEvent'; my $client_type = 's'; # incoming client MUST be sender my $valid = 't'; # registered sender has valid events my $received = DateTime->now; # time of event delivery (UTC) # parse object (event) item my $service = $data->{'SERVICE'} || ""; # tested my $detected = $data->{'DETECTED'} || ""; # tested my $type = $data->{'TYPE'} || ""; # tested my $source_type = $data->{'SOURCE_TYPE'} || ""; # tested my $source = $data->{'SOURCE'} || ""; # untested my $target_proto = $data->{'TARGET_PROTO'} || ""; # untested my $target_port = $data->{'TARGET_PORT'} || ""; # tested my $attack_scale = $data->{'ATTACK_SCALE'} || ""; # tested my $note = $data->{'NOTE'} || ""; # untested my $priority = $data->{'PRIORITY'} || ""; # tested my $timeout = $data->{'TIMEOUT'} || ""; # tested # authorize incoming client my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (%client) { # log incoming event sendMsg("debug", "Incoming event: [client_id: '$client{'client_id'}', service: '$service', detected: '$detected', type: '$type', source_type: '$source_type', source: '$source', target_proto: '$target_proto', target_port: '$target_port', attack_scale: '$attack_scale', note: '$note', priority: '$priority', timeout: '$timeout']", undef); # MySQL optimalization - replace empty string to undef $service = undef if $service eq ""; $detected = undef if $detected eq ""; $type = undef if $type eq ""; $source_type = undef if $source_type eq ""; $source = undef if $source eq ""; $target_proto = undef if $target_proto eq ""; $target_port = undef if $target_port eq ""; $attack_scale = undef if $attack_scale eq ""; $note = undef if $note eq ""; $priority = undef if $priority eq ""; $timeout = undef if $timeout eq ""; # test event item: 'detected' # http://my.safaribooksonline.com/book/programming/regular-expressions/9780596802837/4dot-validation-and-formatting/id2983571 if ($detected !~ /^((?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?/) { sendMsg("err", "Unknown item 'detected' from client '$client{'client_id'}': '$detected'", "Unknown detected time format: '$detected'"); } # test event item: 'event_type', 'source_type' (based on VALIDATION HASH) if (%WardenCommon::VALID_STRINGS) { if (!(exists $WardenCommon::VALID_STRINGS{'type'} && grep $type eq $_, @{$WardenCommon::VALID_STRINGS{'type'}})) { sendMsg("err", "Unknown item 'event_type' from client '$client{'client_id'}': '$type'", "Unknown event type: '$type'"); } elsif (!(exists $WardenCommon::VALID_STRINGS{'source_type'} && grep $source_type eq $_, @{$WardenCommon::VALID_STRINGS{'source_type'}})) { sendMsg("err", "Unknown item 'source_type' from client '$client{'client_id'}': '$source_type'", "Unknown source type: '$source_type'"); } } # test event items: 'target_port', 'attack_scale', 'priority', 'timeout' my @change_list; if (defined $target_port && $target_port !~ /^\d+\z/) { push(@change_list, "target_port: '$target_port'"); $target_port = undef; } if (defined $attack_scale && $attack_scale !~ /^\d+\z/) { push(@change_list, "attack_scale: '$attack_scale'"); $attack_scale = undef; } if (defined $priority && $priority !~ /^\d+\z/) { push(@change_list, "priority: '$priority'"); $priority = undef; } if (defined $timeout && $timeout !~ /^\d+\z/) { push(@change_list, "timeout: '$timeout'"); $timeout = undef; } my $change_string = join(", ", @change_list); unless ($change_string eq "") { sendMsg("info", "Unknown other event items from client '$client{'client_id'}': ($change_string)", undef); } # save new event into database $sth = $DBH->prepare("INSERT INTO events VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);"); unless (defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $rc = $sth->execute(undef, $detected, $received, $type, $source_type, $source, $target_proto, $target_port, $attack_scale, $note, $priority, $timeout, $valid, $client{'client_id'}); unless ($rc) { sendMsg("err", "Cannot execute statement in function '$function_name': $DBH->errstr", "Internal 'execute' server error"); } return 1; } } # END of saveNewEvent #----------------------------------------------------------------------------- # getNewEvents - get new events from the DB greater than received ID #----------------------------------------------------------------------------- sub getNewEvents { my ($class, $data) = @_; my ($sth, $rc, @events, $event, @ids, $used_limit); # client network information my $cn = $ENV{'SSL_CLIENT_S_DN_CN'}; my $alt_names = getAltNames(undef); my $ip = $ENV{'REMOTE_ADDR'}; my $client_type = 'r'; # incoming client MUST be receiver my $function_name = 'getNewEvents'; # parse SOAP data object my $requested_type = $data->{'REQUESTED_TYPE'} || '_any_'; my $last_id = $data->{'LAST_ID'}; my $max_rcv_events_limit = $data->{'MAX_RCV_EVENTS_LIMIT'}; # client events limit # comparison of client and server limit - which can be used if (defined $max_rcv_events_limit && $max_rcv_events_limit < $WardenCommon::MAX_EVENTS_LIMIT) { $used_limit = $max_rcv_events_limit; } else { $used_limit = $WardenCommon::MAX_EVENTS_LIMIT; } # authorize incoming client my %client = authorizeClient($alt_names, $ip, $requested_type, $client_type, $function_name); if (%client) { # obtain events from database my $query = "SELECT id, hostname, service, detected, events.type, source_type, source, target_proto, target_port, attack_scale, note, priority, timeout FROM events INNER JOIN clients ON events.client_id = clients.client_id WHERE events.type != 'test' AND id > ? AND events.valid = 't'"; my @params = ($last_id); unless ($requested_type eq '_any_') { $query .= " AND events.type = ?"; push(@params, $requested_type); } unless ($client{'receive_own'} eq 't') { my ($domain) = $cn =~ /([^\.]+\.[^\.]+)$/; $query .= " AND hostname NOT LIKE ?"; push(@params, '%' . $domain); } $query .= " ORDER BY id ASC LIMIT ?;"; push(@params, $used_limit); $sth = $DBH->prepare($query); unless (defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $rc = $sth->execute(@params); unless (defined $rc) { sendMsg("err", "Cannot execute statement in function '$function_name': $DBH->errstr", "Internal 'execute' server error"); } # obtain event entries from query while (my @result = $sth->fetchrow()) { # create SOAP object $event = SOAP::Data->name(event => \SOAP::Data->value( SOAP::Data->name(ID => $result[0]), SOAP::Data->name(HOSTNAME => $result[1]), SOAP::Data->name(SERVICE => $result[2]), SOAP::Data->name(DETECTED => $result[3]), SOAP::Data->name(TYPE => $result[4]), SOAP::Data->name(SOURCE_TYPE => $result[5]), SOAP::Data->name(SOURCE => $result[6]), SOAP::Data->name(TARGET_PROTO => $result[7]), SOAP::Data->name(TARGET_PORT => $result[8]), SOAP::Data->name(ATTACK_SCALE => $result[9]), SOAP::Data->name(NOTE => $result[10]), SOAP::Data->name(PRIORITY => $result[11]), SOAP::Data->name(TIMEOUT => $result[12]), ), ); push(@events, $event); push(@ids, $result[0]); } # log sent ID of events if (scalar @events != 0) { if (scalar @ids == 1) { sendMsg("info", "Sent 1 event [#$ids[0]] of type '$requested_type' to client '$client{'client_id'}'", undef); } else { sendMsg("info", "Sent " . scalar @ids . " events [#$ids[0] - #$ids[-1]] of type '$requested_type' to client '$client{'client_id'}'", undef); } } return @events; } } # END of getNewEvents #----------------------------------------------------------------------------- # getLastId - get lastest saved event ID #----------------------------------------------------------------------------- sub getLastId { my ($class, $arg) = @_; # client network information my $cn = $ENV{'SSL_CLIENT_S_DN_CN'}; my $alt_names = getAltNames(undef); my $ip = $ENV{'REMOTE_ADDR'}; my $service = undef; my $client_type = undef; my $function_name = 'getLastId'; # authorize incoming client my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (%client) { # obtain max event ID my $sth = $DBH->prepare("SELECT max(id) FROM events;"); unless (defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } my $rc = $sth->execute; unless ($rc) { sendMsg("err", "Cannot execute statement in function '$function_name': $DBH->errstr", "Internal 'execute' server error"); } my $result = $sth->fetchrow(); return $result; } } # END of getLastID #------------------------------------------------------------------------------- # getClientInfo - get list of registered clients on Warden server # by Warden client #------------------------------------------------------------------------------- sub getClientInfo { my ($class, $data) = @_; my (@clients, $client); # client network information my $cn = $ENV{'SSL_CLIENT_S_DN_CN'}; my $alt_names = getAltNames(undef); my $ip = $ENV{'REMOTE_ADDR'}; my $service = undef; my $client_type = undef; my $function_name = 'getClientInfo'; # authorize incoming client my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (%client) { # obtain all valid clients from DB my $sth = $DBH->prepare("SELECT * FROM clients WHERE valid = 't' ORDER BY client_id ASC;"); unless (defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } my $rc = $sth->execute; unless ($rc) { sendMsg("err", "Cannot execute statement in function '$function_name': $DBH->errstr", "Internal 'execute' server error"); } # create SOAP object while ( my @result = $sth->fetchrow() ) { $client = SOAP::Data->name(client => \SOAP::Data->value( SOAP::Data->name(CLIENT_ID => $result[0]), SOAP::Data->name(HOSTNAME => $result[1]), SOAP::Data->name(REGISTERED => $result[2]), SOAP::Data->name(REQUESTOR => $result[3]), SOAP::Data->name(SERVICE => $result[4]), SOAP::Data->name(CLIENT_TYPE => $result[5]), SOAP::Data->name(TYPE => $result[6]), SOAP::Data->name(RECEIVE_OWN_EVENTS => $result[7]), SOAP::Data->name(DESCRIPTION_TAGS => $result[8]), SOAP::Data->name(IP_NET_CLIENT => $result[9]), ), ); push(@clients, $client); } # log information message my $sum = scalar @clients; sendMsg("info", "Sent information about $sum registered clients from Warden server '$ENV{'SERVER_NAME'}' to client '$client{'client_id'}'", undef); return @clients; } } # END of getClientInfo 1;