#!/usr/bin/perl
 
use strict;
use warnings;
 
use JSON;
use Data::Dumper;
use LWP::UserAgent;
use IO::Socket::SSL;
use IO::Handle;

# This code was tested very well only with Ubuntu 16.04. And libwww has differences between 14.04 and 16.04
# sudo apt-get install -y libjson-perl libwww-perl

# Write some debug to /tmp
open my $fl, ">>", "/tmp/fastnetmon_radware_notify_script.log" or die "Could not open file for writing";

# Disable buffering
$fl->autoflush();

# This script executed from FastNetMon this way: ban 11.22.33.44
if (scalar @ARGV != 2) {
    print {$fl} "Please specify all arguments. Got only: @ARGV\n";
    die "Please specify all arguments\n";
}

my ($action, $ip_address) = @ARGV;

my $vision_ip = '192.168.10.138';
my $vision_username = 'radware';
my $vision_password = 'password321';

# Do not execute real login, just return ID
my $debug_fake_login_mode = '';

if (exists $ENV{'FAKE_LOGIN'}) {
    $debug_fake_login_mode = 1;
}

# When you set it to 1 it will pass source, destination ports and protocol to DefenseFlow
my $pass_port_and_protocol_information = '';

sub attack_report {
    # action can be ban or unban
    my ($vision_ip_param, $session_id_param, $action_param, $ip_address_param, $protected_object_name_param, $packets_per_second_param, $bytes_per_second_param, $attack_traffic_details) = @_;

    my $attack_start_path = 'mgmt/device/df/config/action/attackstart';
    my $attack_stop_path = 'mgmt/device/df/config/action/attackstop';

    my $attack_start_endpoint = "https://$vision_ip_param/$attack_start_path";
    my $attack_stop_endpoint = "https://$vision_ip_param/$attack_stop_path";

    my $ua = LWP::UserAgent->new( ssl_opts => { SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, verify_hostname => 0 } );

    my $current_endpoint = '';

    if ($action eq 'ban') {
        $current_endpoint = $attack_start_endpoint;
    } else {
        $current_endpoint = $attack_stop_endpoint;
    }
   
    my $attack_req = HTTP::Request->new(POST => $current_endpoint, HTTP::Headers->new('Cookie'=> "JSESSIONID=$session_id_param" ));
    $attack_req->header('content-type' => 'application/json');

    my $data = {};

    if ($action eq 'ban') {
        $data = {
            "attackVolume" => {
                "packetsPerSecond" => $packets_per_second_param,
                "bytesPerSecond"=> $bytes_per_second_param
            },
            "protectedObjectName" => $protected_object_name_param,
            "networksDetails"=> {
                "networks"=> [{
                    "ip"=> $ip_address_param,
                    "prefix"=> "32"
                }]
            }
        };

        if ($pass_port_and_protocol_information && $attack_traffic_details && ref $attack_traffic_details eq 'HASH') {
            if (defined($attack_traffic_details->{"protocol"}) && $attack_traffic_details->{"protocol"}) {
                # We have protocol here and it set to non empty value
                $data->{'protocol'} = $attack_traffic_details->{"protocol"};
            }

            if (defined($attack_traffic_details->{"source_port"})) {
                $data->{"sourcePort"} = $attack_traffic_details->{"source_port"};
            }

            if (defined($attack_traffic_details->{"destination_port"})) {
                $data->{"destinationPort"} = $attack_traffic_details->{"destination_port"}; 
            }

            if (defined($attack_traffic_details->{"source_hosts"})) {
                my @source_networks_section = ();

                for my $source_ip (@{ $attack_traffic_details->{"source_hosts"} }) {
                    push @source_networks_section, { "ip" => $source_ip, "prefix" => 32 };
                }

                $data->{"sourceNetworks"} = { "networks" => [ @source_networks_section ] };
            }
        }
    } else {
        $data = {
            "networksDetails" => {
                "networks" => [{
                    "ip" => $ip_address_param,
                    "prefix"=> "32"
                }]
            },
            "protectedObjectName" => $protected_object_name_param,
        };
    }

    my $json_query = encode_json($data);

    print {$fl} "Encoded query: $json_query\n";
    print {$fl} "Endpoint: $current_endpoint\n";

    $attack_req->content($json_query);

    my $attack_resp = $ua->request($attack_req);
    
    unless ($attack_resp->is_success) {
        my $error_message = "Cannot execute attack query. Error message: " . $attack_resp->message . " error code: " . $attack_resp->code;
        print {$fl} $error_message;
        die $error_message;
    }

    print {$fl} "Recevied response: " . $attack_resp->decoded_content . "\n";

    my $attack_response_decoded = eval{  decode_json($attack_resp->decoded_content); };

    if (defined($attack_response_decoded->{status}) && $attack_response_decoded->{status} eq 'ok') {
        my $error_message = "Successfully executed attack notification code\n";
        print {$fl} $error_message;
    } else {
        my $error_message = "Does not receive success from attack endpoint\n";
        print {$fl} $error_message;
        die $error_message;
    }

    return 1;
}

sub get_most_popular_protocol {
    my $packets = shift;

    my $hs = {};

    for my $packet (@$packets) {
        $hs->{ $packet->{'protocol'} }++;
    }

    my $total_packets = scalar @$packets;
    
    # If any protocol covers 90% of packets we will use it
    my $threshold_packets = 0.9 * $total_packets;
    
    my $most_popular_protocol = '';
   
    for my $protocol (keys %$hs) {
        print {$fl} "We have $hs->{$protocol} packets for protocol: $protocol\n";

        if ($hs->{$protocol} >= $threshold_packets) {
            $most_popular_protocol = $protocol;
        }
    }
  
    print {$fl} "Most popular protocol: $most_popular_protocol\n";

    if (!$most_popular_protocol) {
        return '';
    }
    
    if ($most_popular_protocol eq 'tcp') {
        return 'TCP';
    } elsif ($most_popular_protocol eq 'udp') {
        return 'UDP';
    } elsif ($most_popular_protocol eq 'icmp') {
        return 'ICMP';
    } else {
        # We do it for all other protocols
        return 'OTHER';
    }
}

sub get_most_popular_source_port {
    my $packets = shift;
    my $our_ip_address = shift;

    my $hs = {}; 

    # Build hash which consist number of bytes per each source host
    for my $packet (@$packets) {
        my $source_ip = $packet->{"source_ip"};

        if ($source_ip eq $our_ip_address) {
            # Skip incoming traffic
            next
        }

        if (exists($hs->{ $source_ip })) {
            $hs->{ $source_ip } += $packet->{"length"};
        } else {
            $hs->{ $source_ip } = $packet->{"length"};
        }
    }   

    print {$fl} "All source hosts\n";
    for my $source_ip (keys %$hs) {
        print {$fl} "source host: $source_ip bytes: $hs->{$source_ip}\n";
    }

    my $limit_hosts = 50;
    my $index = 0;
   
    my @top_source_hosts = ();

    print {$fl} "Ordered top $limit_hosts source hosts\n";
    for my $source_ip (reverse sort { $hs->{$a} <=> $hs->{$b} } keys %$hs) {
        # We need only first X ports
        if ($index++ > $limit_hosts) {
           last
        }

        push @top_source_hosts, $source_ip; 

        print {$fl} "source host: $source_ip bytes: $hs->{$source_ip}\n"; 
    }   

    return [ @top_source_hosts ] 
}


sub get_most_popular_port {
    my $packets = shift;
    my $field = shift;

    my $hs = {}; 

    for my $packet (@$packets) {
        $hs->{ $packet->{$field} }++;
    }   

    my $total_packets = scalar @$packets;
    
    # If any protocol covers 90% of packets we will use it
    my $threshold_packets = 0.9 * $total_packets;
    
    # We do it because 0 port is possible case
    my $most_popular_port = undef; 
   
    for my $port (keys %$hs) {
        print {$fl} "We have $hs->{$port} packets for port: $port\n";

        if ($hs->{$port} >= $threshold_packets) {
            $most_popular_port = $port;
        }
    }   
 
    if (defined($most_popular_port)) {
        print {$fl} "Most popular port for $field: $most_popular_port\n";
    } else {
        print {$fl} "We cannot detect most popular port for $field\n";
    }

    if (!$most_popular_port) {
        return undef; 
    }

    return $most_popular_port;
}

sub login {
    my ($vision_ip_param, $vision_username_param, $vision_password_param) = @_;
   
    if ($debug_fake_login_mode) {
        return 'fake_session_id';
    }

    my $login_path = 'mgmt/system/user/login';

    # Suppress https peer verification because we do not have legit certificates usually
    my $ua = LWP::UserAgent->new( ssl_opts => { SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, verify_hostname => 0,} );
    my $login_endpoint = "https://" . $vision_ip_param . "/" . $login_path;

    print {$fl} "Login endpoint: $login_endpoint\n";

    my $login_req = HTTP::Request->new(POST => $login_endpoint);
    $login_req->header('content-type' => 'application/json');

    my $login_data = '{ "username": "' . $vision_username_param . '", "password":"' . $vision_password_param . '" }';
    $login_req->content($login_data);

    my $login_resp = $ua->request($login_req);

    unless ($login_resp->is_success) {
        my $error_message = "Cannot execute login query. Error message: " . $login_resp->message . " error code: " . $login_resp->code;
        print {$fl} $error_message;
        die $error_message;
    }

    print {$fl} "Recevied response: " . $login_resp->decoded_content . "\n";

    my $login_response_decoded = eval{  decode_json($login_resp->decoded_content); };

    print {$fl} "Decoded login response to: ";
    print {$fl} Dumper($login_response_decoded);
    print {$fl} "\n";

    unless (defined($login_response_decoded->{'status'}) && $login_response_decoded->{'status'} eq 'ok') {
        my $error_message = "Did not get get successful response\n"; 
        print {$fl} $error_message;
        die $error_message;
    }

    unless (defined($login_response_decoded->{'jsessionid'}) && length($login_response_decoded->{'jsessionid'}) > 0) {
        my $error_message = "Can't find jsessionid in resoponse\n";
        print {$fl} $error_message;
        die $error_message;
    }

    my $session_id_param = $login_response_decoded->{'jsessionid'};
    print {$fl} "Received session id: $session_id_param\n";
    return $session_id_param;
}

# Read data from stdin
my $input_attack_details = join '', <STDIN>;
 
# try to decode this data to json
my $attack_details = eval{  decode_json($input_attack_details); };
 
# report error
if ($@) {
    print {$fl} "JSON decode failed: $input_attack_details\n";
    die "JSON decode failed\n";
}

my $host_group = $attack_details->{attack_details}->{'host_group'};

print {$fl} "Received notification about $ip_address with action $action\n";
print {$fl} Dumper($attack_details);
print {$fl} "\n";

print {$fl} "We will use host group name: $host_group\n";

my $session_id = login($vision_ip, $vision_username, $vision_password);

print {$fl} "Session id: $session_id\n";

if ($action eq 'ban' || $action eq 'unban') {
    # We assume that we work with incoming attacks and use these values only
    my $packets_per_second = int($attack_details->{attack_details}->{'total_incoming_pps'});
    my $bytes_per_second = int($attack_details->{attack_details}->{'total_incoming_traffic'});

    my $attack_traffic_details = '';

    if ($pass_port_and_protocol_information && $action eq 'ban') {
        my $per_field_packet_dump = $attack_details->{'packet_dump_detailed'};

        if (defined($per_field_packet_dump) && ref $per_field_packet_dump eq 'ARRAY' && scalar(@$per_field_packet_dump) > 0) {
            print {$fl} "Received packet dump " . Dumper($per_field_packet_dump) . "\n";

            my $most_popular_protocol = get_most_popular_protocol($per_field_packet_dump);

            print {$fl} "Most popular protocol in dump is: $most_popular_protocol\n";

            print {$fl} "Detect source port\n";
            my $most_popular_source_port = get_most_popular_port($per_field_packet_dump, 'source_port');

            print {$fl} "Detect destination port\n";
            my $most_popular_destination_port = get_most_popular_port($per_field_packet_dump, 'destination_port');

            print {$fl} "Detect most popular source hosts\n";
            my $most_popular_sources = get_most_popular_source_port($per_field_packet_dump, $ip_address);
        
            $attack_traffic_details = { "protocol" => $most_popular_protocol,
                "source_port" => $most_popular_source_port,
                "destination_port" => $most_popular_destination_port,
                "source_hosts" => $most_popular_sources,
            };
        } else {
            print {$fl} "Packet dump is empty, cannot use it\n";
        }
    }

    attack_report($vision_ip, $session_id, $action, $ip_address, $host_group, $packets_per_second, $bytes_per_second, $attack_traffic_details);    
} else {
    die "We support only ban and unban actions\n";
}

close $fl;
