From ff124a1358755ceddc0ae6a4187d358da0d54d06 Mon Sep 17 00:00:00 2001 From: VVelox Date: Thu, 22 Nov 2018 09:04:58 -0600 Subject: [PATCH] add portactivity SNMP extend (#159) * add portactivity SNMP extend in its initial form * update for the current json_app_get * add version to the returned JSON * add basic POD documentation --- snmp/portactivity | 352 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100755 snmp/portactivity diff --git a/snmp/portactivity b/snmp/portactivity new file mode 100755 index 0000000..430ae51 --- /dev/null +++ b/snmp/portactivity @@ -0,0 +1,352 @@ +#!/usr/bin/env perl + +#Copyright (c) 2018, Zane C. Bowers-Hadley +#All rights reserved. +# +#Redistribution and use in source and binary forms, with or without modification, +#are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +#THE POSSIBILITY OF SUCH DAMAGE. + +# FreeBSD /usr/include/netinet/tcp_fsm.h +# Linux netstat(8) +# FreeBSD --> Linux +# LISTEN --> LISTEN +# CLOSED --> CLOSED +# SYN_SENT --> SYN_SENT +# SYN_RECEIVED -->SYN_RECV +# ESTABLISHED --> ESTABLISHED +# CLOSE_WAIT --> CLOSE_WAIT +# FIN_WAIT_1 --> FIN_WAIT1 +# CLOSING --> CLOSING +# LAST_ACK --> LAST_ACK +# FIN_WAIT_2 --> FIN_WAIT2 +# TIME_WAIT --> TIME_WAIT +# ((no equivalent)) --> UNKNOWN +# +# UNKNOWN is being regarded as a valid state for all and will be used on OSes that supported it +# The names returned by default are those used by FreeBSD. + +=head1 NAME + +portactivity - Generates JSON output based on netstat data for the specificied TCP services. + +=head1 SYNOPSIS + +portactivity [B<-P>] B<-p> + +=head1 USAGE + +This is meant to be used as a SNMP extend for use with json_app_get in LibreNMS. + +Below is a example of its usage with netsnmpd and checking HTTP and SSH. + + extend portactivity /etc/snmp/portactivity -p http,ssh + +=head1 SWITCHES + +=head2 B<-P> + +Prints the JSON in easily human readable format. + +=head2 B<-p> + +This is a comma seperated list of TCP services to check. + +=head1 SERVICES + +NSS is used to resolve the TCP service protocol names. All the ones listed with -p +must be findable that way or it will error. + +If you are running something on a non-standard port and want to check for it, you either +have to use the name of the port it is on, add it to the database, or change it in the +database(if it is already there under a undesired name). + +In general the file in question on most systems is going to be '/etc/services' and you +will need to run services_mkdb(8) after updating it. But for specifics you will want to +consult services(5). + +=cut + +use strict; +use warnings; +use JSON; +use Getopt::Std; +use Parse::Netstat qw(parse_netstat); + +$Getopt::Std::STANDARD_HELP_VERSION = 1; +sub main::VERSION_MESSAGE { + print "Port Activity SNMP stats extend 0.0.0\n"; +} + +sub main::HELP_MESSAGE { + print "\n". + "-p A comma seperated list of TCP protocols to check for in netstat.\n". + "-P Print the output in a human readable manner.\n"; +} + +#returns aa new hash with all zeroed values for a new protocol +sub newProto{ + + return { + 'total_conns'=>0, + 'total_to'=>0, + 'total_from'=>0, + 'total'=>{ + 'LISTEN'=>0, + 'CLOSED'=>0, + 'SYN_SENT'=>0, + 'SYN_RECEIVED'=>0, + 'ESTABLISHED'=>0, + 'CLOSE_WAIT'=>0, + 'FIN_WAIT_1'=>0, + 'CLOSING'=>0, + 'LAST_ACK'=>0, + 'FIN_WAIT_2'=>0, + 'TIME_WAIT'=>0, + 'UNKNOWN'=>0, + 'other'=>0, + }, + 'to'=>{ + 'LISTEN'=>0, + 'CLOSED'=>0, + 'SYN_SENT'=>0, + 'SYN_RECEIVED'=>0, + 'ESTABLISHED'=>0, + 'CLOSE_WAIT'=>0, + 'FIN_WAIT_1'=>0, + 'CLOSING'=>0, + 'LAST_ACK'=>0, + 'FIN_WAIT_2'=>0, + 'TIME_WAIT'=>0, + 'UNKNOWN'=>0, + 'other'=>0, + }, + 'from'=>{ + 'LISTEN'=>0, + 'CLOSED'=>0, + 'SYN_SENT'=>0, + 'SYN_RECEIVED'=>0, + 'ESTABLISHED'=>0, + 'CLOSE_WAIT'=>0, + 'FIN_WAIT_1'=>0, + 'CLOSING'=>0, + 'LAST_ACK'=>0, + 'FIN_WAIT_2'=>0, + 'TIME_WAIT'=>0, + 'UNKNOWN'=>0, + 'other'=>0, + }, + } + ; +} + +#returns the json output +sub return_json{ + my %to_return; + if(defined($_[0])){ + %to_return= %{$_[0]}; + } + my $pretty=$_[1]; + + if (!defined( $to_return{data} ) ){ + $to_return{data}={}; + } + + my $j=JSON->new; + + if ( $pretty ){ + $j->pretty(1); + } + + print $j->encode( \%to_return ); + + if ( ! $pretty ){ + print "\n"; + } +} + +my %valid_states=( + 'LISTEN'=>1, + 'CLOSED'=>1, + 'SYN_SENT'=>1, + 'SYN_RECEIVED'=>1, + 'ESTABLISHED'=>1, + 'CLOSE_WAIT'=>1, + 'FIN_WAIT_1'=>1, + 'CLOSING'=>1, + 'LAST_ACK'=>1, + 'FIN_WAIT_2'=>1, + 'TIME_WAIT'=>1, + 'UNKNOWN'=>1, + ); + +#gets the options +my %opts=(); +getopts('p:P', \%opts); + +#what will be returned +my %to_return; +$to_return{error}='0'; +$to_return{errorString}=''; +$to_return{version}=1; + +if (! defined( $opts{p} ) ){ + $to_return{errorString}='No services specificied to check for'; + $to_return{error}=1; + return_json(\%to_return, $opts{P}); + exit 1; +} + +#the list of protocols to check for +my @protos_array=split(/\,/, $opts{p}); + +#holds the various protocol hashes +my %protos; + +#make sure each one specificied is defined and build the hash that will be returned +my $protos_array_int=0; +while ( defined( $protos_array[$protos_array_int] ) ){ + $protos{ $protos_array[$protos_array_int] }=newProto; + + #check if it exists + my $port=getservbyname( $protos_array[$protos_array_int] , 'tcp' ); + + # if it is not defined, then we error + if ( !defined( $port ) ){ + $to_return{errorString}='"'.$protos_array[$protos_array_int].'" is not a known service either add it or double check your spelling'; + $to_return{error}=4; + return_json(\%to_return, $opts{P}); + exit 4; + } + + $protos_array_int++; +} + +my $os=$^O; + +my $netstat; + +#make sure this is a supported OS +if ( $os eq 'freebsd' ){ + $netstat='netstat -S -p tcp' +}elsif( $os eq 'linux' ){ + $netstat='netstat -n' +}else{ + $to_return{errorString}=$os.' is not a supported OS as of currently'; + $to_return{error}=3; + return_json(\%to_return, $opts{P}); + exit 3; +} + +my $res = parse_netstat(output => join("", `$netstat`), flavor=>$os); + +#check to make sure that it was able to parse the output +if ( + (!defined( $res->[1] )) || + ($res->[1] ne 'OK' ) + ){ + $to_return{errorString}='Unable to parse netstat output'; + $to_return{error}=2; + return_json(\%to_return, $opts{P}); + exit 2; +} + +#chew through each connection +my $active_conns_int=0; +while ( defined( $res->[2]{'active_conns'}[$active_conns_int] ) ){ + my $conn=$res->[2]{active_conns}[$active_conns_int]; + + #we only care about TCP currently + if ( $conn->{proto} =~ /^[Tt][Cc][Pp]/ ){ + $protos_array_int=0; + my $service; + while( + ( defined( $protos_array[ $protos_array_int ] ) ) && + ( !defined( $service ) ) #stop once we find it + ){ + #check if this matches either ports + if ( + ( $protos_array[ $protos_array_int ] eq $conn->{'local_port'} ) || + ( $protos_array[ $protos_array_int ] eq $conn->{'foreign_port'} ) + ){ + $service=$protos_array[ $protos_array_int ]; + } + + $protos_array_int++; + } + + #only handle it if is a service we are watching for + if ( defined( $service ) ){ + my $processed=0; + + my $state=$conn->{'state'}; + #translate the state names + if ( $os eq 'linux' ){ + if ( $state eq 'SYN_RECV' ){ + $state='SYN_RECEIVED'; + }elsif( $state eq 'FIN_WAIT1' ){ + $state='FIN_WAIT_1'; + }elsif( $state eq 'FIN_WAIT2' ){ + $state='FIN_WAIT_2' + } + } + + #only count the state towards the total if not listening + if ( $state ne 'LISTEN' ){ + $protos{$service}{'total_conns'}++; + } + + #make sure the state is a valid one + # if it is not a valid one, set it to other, meaning something unexpected was set for the state that should not be + if ( ! defined( $valid_states{$state} ) ){ + $state='other'; + } + + #increment the total state + $protos{$service}{'total'}{$state}++; + + if ( + ( $conn->{'foreign_port'} eq $service ) && + ( $state ne 'LISTEN' ) + ){ + $protos{$service}{'total_from'}++; + $protos{$service}{'from'}{$state}++; + $processed=1; + } + + if ( + ( $conn->{'local_port'} eq $service ) && + ( $state ne 'LISTEN' ) && + ( ! $processed ) + ){ + $protos{$service}{'total_to'}++; + $protos{$service}{'to'}{$state}++; + } + + } + + } + + $active_conns_int++; +} + +#return the finished product +$to_return{data}=\%protos; +return_json(\%to_return, $opts{P}); +exit 0;