#=== FUNCTION ================================================================ # NAME: snmp_get # PURPOSE: fetch the Physical Address of the eth0 int # PARAMETERS: $sess, the snmp_session returned by the init. #=============================================================================== sub snmp_get($){ my ($interfaces_table, @interfaces_oids, $int_id, $error, $snmp_res); my $snmp_sess = shift; unless ($snmp_sess){ print STDERR "snmp_session object has fallen out of scope\n" if $debug; return; } my @interface_names_oid='1.3.6.1.2.1.2.2.1.2'; $interfaces_table=$snmp_sess->get_entries(-columns => \@interface_names_oid); if ($interfaces_table){ foreach my $int_oid (keys %{$interfaces_table} ){ ( $int_id ) = ( $int_oid =~ m{\.(\d+)$} ); push(@interfaces_oids, "1.3.6.1.2.1.2.2.1.6.$int_id") if $interfaces_table->{$int_oid}=~m{eth0}; } $snmp_res = $snmp_sess->get_request( -varbindlist => \@interfaces_oids ); }else{ $error=$snmp_sess->error(); } return ($snmp_res,$error); } #### #=== FUNCTION ================================================================ # NAME: snmp_get # PURPOSE: fetch the Physical Address of the eth0 int # PARAMETERS: $sess, the snmp_session returned by the init. #=============================================================================== sub snmp_get(){ my $snmp_sess = shift; unless ($snmp_sess){ print STDERR "snmp_session object has fallen out of scope\n" if $debug; return; } } $func_map = { INTERFACES => @{'1.3.6.1.2.1.2.2.1.2'}, MACS => @{\&get_ints } } get_ints(){ my @interface_names_oid=$func_map{INTERFACES}; $interfaces_table=$snmp_sess->get_entries(-columns => \@interface_names_oid); if ($interfaces_table){ foreach my $int_oid (keys %{$interfaces_table} ){ ( $int_id ) = ( $int_oid =~ m{\.(\d+)$} ); push(@interfaces_oids, "1.3.6.1.2.1.2.2.1.6.$int_id") if $interfaces_table->{$int_oid}=~m{eth0}; } } } get_macs(){ $snmp_res = $snmp_sess->get_request( -varbindlist => \@interfaces_oids ); } #### #--------------------------------------------------------------------------- # std opts #--------------------------------------------------------------------------- use strict; use warnings; #--------------------------------------------------------------------------- # Database connectivity #--------------------------------------------------------------------------- use DBI; # access AnyData|MySQL #--------------------------------------------------------------------------- # program pre-requsites. #--------------------------------------------------------------------------- use Getopt::Long; use Nmap::Parser; use File::Glob; use File::Basename; use Net::SNMP; use Net::Ping; use String::CRC32; #--------------------------------------------------------------------------- # Debugging #--------------------------------------------------------------------------- use Data::Dumper; $Data::Dumper::Terse=1; $Data::Dumper::Indent=1; #--------------------------------------------------------------------------- # Use the local home directory structure during development. #--------------------------------------------------------------------------- my $dir; BEGIN{ unless ($ENV{PWD} =~ m{/home/}){ $dir="/usr/local/lib/perl5" }else{ $dir="$ENV{HOME}/" . dirname($0) . "/../lib/perl5"; } } use lib ( $dir ); use util; #--------------------------------------------------------------------------- # some globals #--------------------------------------------------------------------------- my (%options, $dsn, $dbh, $debug, @files, $timeout); # my $localaddr = util::resolvname($ENV{HOSTNAME}); my $localport = sprintf("%5d",int(rand(65535))); # reset to a random highport my $community=$options{community} || 'public'; #=== FUNCTION ================================================================ # NAME: help # DESCRIPTION: Print help information #=============================================================================== sub help { print STDERR < } (this can be a quoted pattern match *.xml) | { --infilet | -it } (this can be a quoted pattern match *.txt) | { --db | -d } { --dbhost | -h } { --dbuser | -u } { --dbpass | -p } | { --outfile | -o } ------------------------------------------------------------------------ --infile requires an nmap XML file. --db and --outfile are mutually exclusive, the outfile is in CSV format. --db requires: --dbhost --dbpass --dbuser eof ; exit 1; } #end{help} #=== FUNCTION ================================================================ # Name: options # DESCRIPTION: process command line options #=============================================================================== sub options { GetOptions(\%options, 'infilex|ix=s', # input XML forced as an array 'infilet|it=s', # input TXT forced as an array 'db|d=s', # commit to DB 'dbhost|h=s', # host on which the database resides. 'dbuser|u=s', # username to connect to MySQL 'dbpass|p=s', # password for the above user. 'outfile|o=s', # commit to CSV 'debug|D' => \$debug, # debugging? 'help|h|?' # access the help routine ); if ( -e "$options{dbpass}" ){ $options{dbpass}=sub{ my $passtok; open(PASSFILE,"<$options{dbpass}"); while(){ next unless (/^.+$/); # skip blanks. chomp($passtok=$_); ($passtok) = ($passtok ? /(.+)\b\s*#.*user.*/ : /^(.+)$/); return $passtok if $passtok; } return($passtok); }->(); } print Dumper %options if $debug; return(%options); } #end{options} #=== FUNCTION ================================================================ # NAME: globfile # PURPOSE: parse shell globs #=============================================================================== sub globfile { my @files = ($options{infilex} ? glob($options{infilex}) : glob($options{infilet})); die "no files found in" . $options{infilex} ? $options{infilex} : $options{infilet} . "\n" unless @files; return(@files); } #=== FUNCTION ================================================================ # NAME: dbinit # PURPOSE: Configure the database handle # DESCRIPTION: Configure the CSV/MySQL database handle. #=============================================================================== sub dbinit { $DBI::dbi_debug=1 if $debug; # set the DB driver to debug if ($options{db}){ $dsn = "DBI:mysql:database=$options{db};host=$options{dbhost};port=3306"; $dbh = DBI->connect($dsn, $options{dbuser}, $options{dbpass}) or die "MySQL: $!\n"; }elsif ($options{csv}){ $dbh = DBI->connect('dbi:AnyData(RaiseError=>1):'); $dbh->func( 'device', 'CSV', $options{outfile}, { col_names => 'deviceid,fingerprint,name,description,slaid,devicetypeid,firstseen,lastseen'}, 'ad_catalog' ); } #end if-elsif return($dbh); } #end{dbinit} #=== FUNCTION ================================================================ # NAME: snmp_init # PURPOSE: Import the methods from net SNMP and retrun the session opject # to my caller # RETURNS: A communications handle. #=============================================================================== sub snmp_init($$;$$) { my ($hostname, $port ) =@_; my ($snmp_sess, $error)=Net::SNMP->session( -hostname => "$hostname", -port => "$port", -localaddr => "$localaddr", -localport => "$localport", -timeout => "3", -community => "$community" ); return($snmp_sess); } #end{snmp_init} #=== FUNCTION ================================================================ # NAME: snmp_get # PURPOSE: fetch the Physical Address of the eth0 int # PARAMETERS: $sess, the snmp_session returned by the init. #=============================================================================== sub snmp_get($){ my ($interfaces_table, @interfaces_oids, $int_id, $error, $snmp_res); my $snmp_sess = shift; unless ($snmp_sess){ print STDERR "snmp_session object has fallen out of scope\n" if $debug; return; } my @interface_names_oid='1.3.6.1.2.1.2.2.1.2'; $interfaces_table=$snmp_sess->get_entries(-columns => \@interface_names_oid); if ($interfaces_table){ foreach my $int_oid (keys %{$interfaces_table} ){ ( $int_id ) = ( $int_oid =~ m{\.(\d+)$} ); push(@interfaces_oids, "1.3.6.1.2.1.2.2.1.6.$int_id") if $interfaces_table->{$int_oid}=~m{eth0}; } $snmp_res = $snmp_sess->get_request( -varbindlist => \@interfaces_oids ); }else{ $error=$snmp_sess->error(); } return ($snmp_res,$error); } #=== FUNCTION ================================================================ # NAME: text_processor # PURPOSE: parse a plain text list of hostnames/IPs # RETURNS: nested hash LIKE NMAP::Parser #=============================================================================== sub text_processor { my $routine=(caller 0)[3]; my $file = shift; my $host={} ; my ($name, $ip, $os); # ersatz NMAP::Parser datastruct open (INFILE,"$file"); foreach (){ /(^\p{IsAlpha}+)/ ? $name=$1 : chomp($ip=$_); if ($name){ $ip=util::resolvip($name); }elsif ($ip){ $name= util::resolvip($ip) || $ip; } $host->{'addrs'}->{'ipv4'} = "$ip"; $host->{'hostnames'} = ["$name"]; # sub os_sid { # $host->{'os'}->{'osmatch_count'} = 1; # $host->{'os'}->{'osmatch_name'} = ['nix']; # } } } #=== FUNCTION ================================================================ # NAME: ins_host_data # PURPOSE: insert into the device table. #=============================================================================== sub ins_host_data { $dbh=&dbinit; &help unless $dbh; # bail if the DBH is missing. my $host = shift; my $os = $host->os_sig(); my $device_type; my $ip = sub { $host->{addrs}->{ipv4} || '' }->() ; my $descr = sub { $os->{osmatch_name}->[0] || $os->name || '' }->() ; my $sla = '2'; chomp (my $datetime = `date '+%Y-%m-%d %H:%M:%S'`); #--------------------------------------------------------------------------- # Is the host up on port 22? #--------------------------------------------------------------------------- my $p = Net::Ping->new('syn', 2, 64); #$p->port_number(getservbyname("ssh", "tcp")); my $res=$p->ping($ip, 2); # $res=$p->ack($ip); ($res)= $p->{econnrefused} ? 0 : $p->ack($ip); $p->close(); my ($snmp_sess, $error, $snmp_res, $mac); #--------------------------------------------------------------------------- # transform the IP to a name, resolve the IP if the name is not present. #--------------------------------------------------------------------------- my $name = sub { $host->{hostnames}->[0] || util::resolvip($host->{addrs}->{ipv4}) || '' }->() ; $name =~ tr{A-Z}{a-z}; $name = (split(/\./,$name))[0] unless ($name =~ /^\d+\./); #--------------------------------------------------------------------------- # wedged in the SNMP handling. This is a poor spot for it. inefficient. #--------------------------------------------------------------------------- $snmp_sess = &snmp_init($name, '161', $localaddr, $localport, '', $community); ($snmp_res, $error)= &snmp_get($snmp_sess); #--------------------------------------------------------------------------- # if there is an error, bail. Whilst adding the name of the affected # device if it isn't already in the message. #--------------------------------------------------------------------------- if ($error){ my $msg="SNMP Connect: $error"; $msg .= ($error =~ /$name/) ? "\n" : " \x27$name\x27\n"; print STDERR "$msg"; return; } ($mac) = (values(%{ $snmp_res })) ; if (!$mac){ print STDERR "ERROR: $name returned no MAC address\n"; return ; } $mac=~s/0x|..$//g; # slice off the ugly #--------------------------------------------------------------------------- # Set the numerics that RGIL is using. #--------------------------------------------------------------------------- my $devt = sub { if ($os->{osmatch_count} gt 0){ $device_type=1 if ($os->{osmatch_name}->[0] =~ /Linux/); $device_type=2 if ($os->{osmatch_name}->[0] =~ /Windows/); $device_type=3 if ($os->{osmatch_name}->[0] !~ /Linux|Windows/); }else{ $device_type=99; } return($device_type); }->(); #end-if-else,sub if ($host->{status}=~m{^up$} && $res && $snmp_res){ #--------------------------------------------------------------------------- # generate the "checksum data" #--------------------------------------------------------------------------- my ($fingerprint)=(crc32("$name $mac")) unless !$mac; # if (length($fingerprint) < 10){ return } if ($debug){ print "\n\t$fingerprint=crc32($name $mac)\n"unless !$mac; } my @db_res=$dbh->selectrow_array("select * from device where fingerprint=" . $dbh->quote($fingerprint)); if (@db_res){ print STDERR "$name and $mac already present, updating last seen!\n"; print STDERR "\nupdate device set lastseen\x27=$datetime\x27 where fingerprint=\x27$fingerprint\x27\n" if $debug; $dbh->do("update device set lastseen=" . $dbh->quote($datetime) . " where fingerprint=" . $dbh->quote($fingerprint)); return; }else{ print STDERR "\nINSERT INTO device set deviceid='', fingerprint=\x27$fingerprint\x27, name=\x27$name\x27, description=\x27$descr, slaid=\x27$sla\x27, devicetypeid=\x27$devt\x27, firstseen=\x27$datetime\x27, lastseen=\x27$datetime\x27\n" if $debug; $dbh->do("INSERT INTO device set deviceid=" . $dbh->quote("''") . ", fingerprint=" . $dbh->quote($fingerprint) . ", name=" . $dbh->quote($name) . ", description=" . $dbh->quote($descr) . ", slaid=" . $dbh->quote($sla) . ", devicetypeid=" . $dbh->quote($devt) . ", firstseen=" . $dbh->quote($datetime) . ", lastseen=" . $dbh->quote($datetime) ); print STDERR "\nINSERT INTO ip set fingerprint=$fingerprint, ip=inet_aton($ip)\n" if $debug; my $ip_fingerprint=crc32($ip); $dbh->do("INSERT INTO ip set fingerprint=" . $dbh->quote($fingerprint) . ", ip=" . "inet_aton(" . $dbh->quote($ip) . ")" ); } }else{ print STDERR "\n\tskipping $host->{addrs}->{ipv4}: not up/no OS determined.\n" } #end if-else } #end{ins_host_data} #--------------------------------------------------------------------------- # MAIN #--------------------------------------------------------------------------- options; our $quiet=1 unless $debug; # quiesce resolver utility if ( (!$options{infilex} && !$options{infilet} ) && ( !$options{outfile} || ( !$options{db} && !$options{dbhost} && !$options{dbuser} && !$options{dbpass} ) ) ){ help; } #end-if $options{csv} = 1 if $options{outfile}; my $np; @files=&globfile; foreach my $file (@files){ #--------------------------------------------------------------------------- # Here is some logic to ensure that we are processing a filename # that matches the argument supplied. This might be better placed in # globfile(), meh. #--------------------------------------------------------------------------- my ($typ, $ext)=sprintf("%s",grep(/le[tx]/,keys %options)); # $ext is. null I was just being lazy with space. $ext=($typ=~/x$/) ? 'xml' : 'txt'; # if we were passed XML we should've globbed xml ($file !~/$ext/) ? # mismatched?, then error out and exit. sub { print STDERR "ERROR:filename and option conflict- expected\n\t\x22--$typ '*.$ext'\x22\nand got\n\t$file\n"; exit 99 }->() : print "\nProcessing file $file...\n"; # otherwise, print and move on. if ($options{infilex}){ $np = new Nmap::Parser; $np->callback(\&ins_host_data); $np->parsefile($file); }elsif ($options{infilet}){ &ins_host_data(&process_text); }else{ die "Unsupported file-type\n"; } } #end-for