Skip to content
Snippets Groups Projects
Warden.pm 19.1 KiB
Newer Older
Tomáš Plesník's avatar
Tomáš Plesník committed
# 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 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 lib File::Basename::dirname(__FILE__);


################################################################################
#				VARIABLES
################################################################################
our $VERSION = "2.2";
our $FILENAME = File::Basename::basename(__FILE__);
my $lib = File::Basename::dirname(__FILE__);

################################################################################
################################################################################
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 '$WardenCommon::DB_NAME' at '$WardenCommon::DB_HOST': $DBI::errstr";

################################################################################
#				FUNCTIONS
################################################################################

#-------------------------------------------------------------------------------
# sendMsg - wrapper for more complex WardenCommon::sendMsg function
#-------------------------------------------------------------------------------
  my $severity   = shift;
  my $syslog_msg = shift;
  my $soap_msg   = shift;
Jakub Cegan's avatar
Jakub Cegan committed

  WardenCommon::sendMsg($WardenCommon::SYSLOG, $WardenCommon::SYSLOG_VERBOSE, $WardenCommon::SYSLOG_FACILITY, $severity,
			$syslog_msg, $soap_msg, $FILENAME);
}


#-------------------------------------------------------------------------------
# getAltNames - parse Alternate names from SSL certifiate
#-------------------------------------------------------------------------------
{
  my @an_array;
  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) = @_;
  # check if client is valid and obtain client_id, ip_net_client and receive_own_events
    $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
            "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);
      sendMsg("err",
              "Cannot execute authorization statement in function 'authorizeClient': $DBH->errstr",
	      "Internal 'execute' server error");
    }
      sendMsg("err",
              "Cannot execute authorization statement in function 'authorizeClient': $DBH->errstr",
              "Internal 'execute' server error");
    }
  my ($client_id, $ip_net_client, $receive_own, $ip_net_client_list);
  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;
    if ($ip_net_client_list->bin_find($ip)) {
  # check if client is registered
  if ($sth->rows == 0) {
            "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
             "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'");
################################################################################
# 				SOAP Functions
################################################################################

#-----------------------------------------------------------------------------
# saveNewEvent - save new received event into database
#-----------------------------------------------------------------------------
sub saveNewEvent
{
  my ($class, $data) = @_;
  # 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
  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']",
    # 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'}})) {
                "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'}})) {
                "Unknown item 'source_type' from client '$client{'client_id'}': '$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'");
    if (defined $attack_scale && $attack_scale !~ /^\d+\z/) {
      push(@change_list, "attack_scale: '$attack_scale'");
    if (defined $priority && $priority !~ /^\d+\z/) {
      push(@change_list, "priority: '$priority'");
    if (defined $timeout && $timeout !~ /^\d+\z/) {
      push(@change_list, "timeout: '$timeout'");
    my $change_string = join(", ", @change_list);
              "Unknown other event items from client '$client{'client_id'}': ($change_string)",
    $sth = $DBH->prepare("INSERT INTO events VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
      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'});
      sendMsg("err",
              "Cannot execute statement in function '$function_name': $DBH->errstr",
	      "Internal 'execute' server error");
    }
} # 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);
  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

  # parse SOAP data object
Tomáš Plesník's avatar
Tomáš Plesník committed
  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;
  my %client = authorizeClient($alt_names, $ip, $requested_type, $client_type, $function_name);
  if (%client) {
    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");
    while (my @result = $sth->fetchrow()) {
      $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);
    }

    # log sent ID of events
    if (scalar @events != 0) {
      if (scalar @ids == 1) {
	        "Sent 1 event [#$ids[0]] of type '$requested_type' to client '$client{'client_id'}'",
                "Sent " . scalar @ids . " events [#$ids[0] - #$ids[-1]] of type '$requested_type' to client '$client{'client_id'}'",
    return @events;
  }
} # END of getNewEvents


#-----------------------------------------------------------------------------
# getLastId - get lastest saved event ID
#-----------------------------------------------------------------------------
sub getLastId
{
  my ($class, $arg) = @_;

  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 (%client) {
    my $sth = $DBH->prepare("SELECT max(id) FROM events;");
              "Cannot prepare statement in function '$function_name': $DBH->errstr",
              "Internal 'prepare' server error");
      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);
  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 (%client) {
    my $sth = $DBH->prepare("SELECT * FROM clients WHERE valid = 't' ORDER BY client_id ASC;");
            "Cannot prepare statement in function '$function_name': $DBH->errstr",
            "Internal 'prepare' server error");
      sendMsg("err",
              "Cannot execute statement in function '$function_name': $DBH->errstr",
              "Internal 'execute' server error");
    }
    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);
    }
    my $sum = scalar @clients;
            "Sent information about $sum registered clients from Warden server '$ENV{'SERVER_NAME'}' to client '$client{'client_id'}'",
    return @clients;
  }
} # END of getClientInfo