#!/usr/bin/perl # # Warden.pm # # Copyright (C) 2011-2013 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; my $basedir = "/opt/warden-server/"; use lib $basedir . "lib"; use WardenCommon; ################################################################################ # VARIABLES ################################################################################ our $VERSION = "2.2"; my $etc = $basedir . "etc"; our $FILENAME = File::Basename::basename($0); ################################################################################ # READING OF CONFIGURATION VARIABLES ################################################################################ my $conf_file = "$etc/warden-server.conf"; WardenCommon::loadConf($conf_file); ################################################################################ # DB CONNECT ################################################################################ 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: $DBI::errstr"; ################################################################################ # FUNCTIONS ################################################################################ #------------------------------------------------------------------------------- # sendMsg - wrapper for more complex WardenCommon::sendMsg function #------------------------------------------------------------------------------- sub sendMsg { my $severity = shift; my $syslog_msg = shift; my $soap_msg = shift; 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); 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; # obtain cidr based on rigth common name and alternate names, service and client_type 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 = ? 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 = ? 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) 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' ORDER BY SUBSTRING_INDEX(ip_net_client,'/', -1) DESC;"); } # check db handler if (!defined $sth) { sendMsg("err", "Cannot prepare authorization statement in $function_name: $DBH->errstr", "Internal 'prepare' server error"); } # execute query for two or none params functions if ($function_name eq 'saveNewEvent' || $function_name eq 'getNewEvents') { $sth->execute($service_type, $client_type); } else { $sth->execute; } # obtain registration info 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 if (!$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; # 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) parameters my $service = $data->{'SERVICE'}; my $detected = $data->{'DETECTED'}; my $type = $data->{'TYPE'}; my $source_type = $data->{'SOURCE_TYPE'}; my $source = $data->{'SOURCE'}; my $target_proto = $data->{'TARGET_PROTO'}; my $target_port = $data->{'TARGET_PORT'}; my $attack_scale = $data->{'ATTACK_SCALE'}; my $note = $data->{'NOTE'}; my $priority = $data->{'PRIORITY'}; my $timeout = $data->{'TIMEOUT'}; my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (defined %client) { sendMsg("debug", "Incoming event: [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); if (%WardenCommon::VALID_STRINGS) { # check if hash is not empty - use VALIDATION HASH if (!(exists $WardenCommon::VALID_STRINGS{'type'} && grep $type eq $_, @{$WardenCommon::VALID_STRINGS{'type'}})) { sendMsg("err", "Unknown event type from [IP: '$ip'; CN(AN): $alt_names; Service: '$service'; Type: '$type']", "Unknown event type: '$type'"); } elsif (!(exists $WardenCommon::VALID_STRINGS{'source_type'} && grep $source_type eq $_, @{$WardenCommon::VALID_STRINGS{'source_type'}})) { sendMsg("err", "Unknown source type from [IP '$ip'; CN(AN): $alt_names; Service: '$service'; Source_type: '$source_type']", "Unknown source type: '$source_type'"); } } # 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 detected time format from [IP: '$ip'; CN(AN): $alt_names; Service: '$service'; Detected: '$detected']", "Unknown detected time format: '$detected'"); } 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); if ($change_string ne "") { sendMsg("info", "Unknown event items detected {originaly - $change_string} received in $received from [IP '$ip'; CN(AN): $alt_names; Service: '$service'; Type: '$type'; Detected: $detected]", undef); } $sth=$DBH->prepare("INSERT INTO events VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);"); if (!defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $sth->execute(undef, $detected, $received, $type, $source_type, $source, $target_proto, $target_port, $attack_scale, $note, $priority, $timeout, $valid, $client{'client_id'}); return 1; } } # END of saveNewEvent #----------------------------------------------------------------------------- # getNewEvents - get new events from the DB greater than received ID #----------------------------------------------------------------------------- sub getNewEvents { my ($class, $data) = @_; my ($sth, @events, $event, @ids); my ($id, $hostname, $service, $detected, $type, $source_type, $source, $target_proto, $target_port, $attack_scale, $note, $priority, $timeout, $client_id); # 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 sender 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 my $used_limit; 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; } my %client = authorizeClient($alt_names, $ip, $requested_type, $client_type, $function_name); if(defined %client) { if ($client{'receive_own'} eq 't') { if ($requested_type eq '_any_') { $sth = $DBH->prepare("SELECT * FROM events WHERE type != 'test' AND id > ? AND valid = 't' ORDER BY id ASC LIMIT ?;"); if (!defined $sth) { sendMsg("err", "Cannot prepare ROE-ANY statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $sth->execute($last_id, $used_limit); } else { $sth = $DBH->prepare("SELECT * FROM events WHERE type != 'test' AND id > ? AND type = ? AND valid = 't' ORDER BY id ASC LIMIT ?;"); if (!defined $sth) { sendMsg("err", "Cannot prepare ROE statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $sth->execute($last_id, $requested_type, $used_limit); } } else { if ($requested_type eq '_any_') { $sth = $DBH->prepare("SELECT * FROM events WHERE type != 'test' AND id > ? AND valid = 't' AND hostname NOT LIKE ? ORDER BY id ASC LIMIT ?;"); if (!defined $sth) { sendMsg("err", "Cannot prepare ANY statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } my ($domain) = $cn =~ /([^\.]+\.[^\.]+)$/; $domain = '\%' . $domain; $sth->execute($last_id, $domain, $used_limit); } else { $sth = $DBH->prepare("SELECT * FROM events WHERE type != 'test' AND id > ? AND type = ? AND valid = 't' AND hostname NOT LIKE ? ORDER BY id ASC LIMIT ?;"); if (!defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr\n", "Internal 'prepare' server error"); } my ($domain) = $cn =~ /([^\.]+\.[^\.]+)$/; $domain = '\%' . $domain; $sth->execute($last_id, $requested_type, $domain, $used_limit); } } # obtain items of events stored in events table while (my @result = $sth->fetchrow()) { $id = $result[0]; $detected = $result[1]; $type = $result[3]; $source_type = $result[4]; $source = $result[5]; $target_proto = $result[6]; $target_port = $result[7]; $attack_scale = $result[8]; $note = $result[9]; $priority = $result[10]; $timeout = $result[11]; $client_id = $result[13]; # obtain hostname and service of events based on client_id from clients table $sth = $DBH->prepare("SELECT hostname, service FROM clients WHERE client_id = ?;"); $sth->execute($client_id); ($hostname, $service) = $sth->fetchrow(); # create SOAP data object $event = SOAP::Data->name(event => \SOAP::Data->value( SOAP::Data->name(ID => $id), SOAP::Data->name(HOSTNAME => $hostname), SOAP::Data->name(SERVICE => $service), SOAP::Data->name(DETECTED => $detected), SOAP::Data->name(TYPE => $type), SOAP::Data->name(SOURCE_TYPE => $source_type), SOAP::Data->name(SOURCE => $source), SOAP::Data->name(TARGET_PROTO => $target_proto), SOAP::Data->name(TARGET_PORT => $target_port), SOAP::Data->name(ATTACK_SCALE => $attack_scale), SOAP::Data->name(NOTE => $note), SOAP::Data->name(PRIORITY => $priority), SOAP::Data->name(TIMEOUT => $timeout) )); push(@events, $event); push(@ids, $id); } # log sent ID of events if (scalar @events != 0) { if (scalar @ids == 1) { sendMsg("info", "Sent 1 event [#$ids[0]] to [IP: '$ip'; CN(AN): $alt_names; Client_limit: '$max_rcv_events_limit', Requested_type: '$requested_type']", undef); } else { sendMsg("info", "Sent " . scalar @ids . " events [#$ids[0] - #$ids[-1]] to [IP: '$ip'; CN(AN): $alt_names, Client_limit: '$max_rcv_events_limit', Requested_type: '$requested_type']", 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'; my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (defined %client) { my $sth = $DBH->prepare("SELECT max(id) FROM events;"); if (!defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $sth->execute; 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); my ($client_id, $hostname, $registered, $requestor, $service, $client_type, $type, $receive_own_events, $description_tags, $ip_net_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'; my %client = authorizeClient($alt_names, $ip, $service, $client_type, $function_name); if (defined %client) { my $sth = $DBH->prepare("SELECT * FROM clients WHERE valid = 't' ORDER BY client_id ASC;"); if (!defined $sth) { sendMsg("err", "Cannot prepare statement in function '$function_name': $DBH->errstr", "Internal 'prepare' server error"); } $sth->execute; while ( my @result = $sth->fetchrow() ) { $client_id = $result[0]; $hostname = $result[1]; $registered = $result[2]; $requestor = $result[3]; $service = $result[4]; $client_type = $result[5]; $type = $result[6]; $receive_own_events = $result[7]; $description_tags = $result[8]; $ip_net_client = $result[9]; $client = SOAP::Data->name(client => \SOAP::Data->value( SOAP::Data->name(CLIENT_ID => $client_id), SOAP::Data->name(HOSTNAME => $hostname), SOAP::Data->name(REGISTERED => $registered), SOAP::Data->name(REQUESTOR => $requestor), SOAP::Data->name(SERVICE => $service), SOAP::Data->name(CLIENT_TYPE => $client_type), SOAP::Data->name(TYPE => $type), SOAP::Data->name(RECEIVE_OWN_EVENTS => $receive_own_events), SOAP::Data->name(DESCRIPTION_TAGS => $description_tags), SOAP::Data->name(IP_NET_CLIENT => $ip_net_client), )); push(@clients, $client); } my $sum = scalar @clients; sendMsg("info", "Sent information about $sum registered clients from Warden server '$ENV{'SERVER_NAME'}'", undef); return @clients; } } # END of getClientInfo 1;