Merge pull request #56 from wuruilong01/master
[prads.git] / tools / prads-asset-report
blob555f79b10d427f9770ed7e99d4a72c8c23fe8a04
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------------
3 # prads-asset-report.pl
5 # Edward Fjellskaal <edwardfjellskaal@gmail.com>
6 # Kacper Wysocki <comotion@krutt.org>
8 # This script will generate a formatted report based on the data
9 # produced by Passive Real-time Asset Detection System (PRADS).
11 # Copyright (C) 2004 Matt Shelton <matt@mattshelton.com>
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
27 my $version = '0.4';
28 my $date = '2012-08-30';
30 # ---------------------------------------------------------------------
31 #use strict;
32 #use warnings;
33 print_header();
34 eval("use Getopt::Long"); die "[!] ERROR: Getopt::Long module must be installed!\n" if $@;
35 eval("use Socket"); die "[!] ERROR: Socket module must be insalled!\n" if $@;
36 use vars qw ($opt_h $opt_r $opt_w $opt_n $opt_p);
38 # Variable Declarations
39 my $report_file = "/var/log/prads-asset.log";
41 # Data Structure:
42 # %asset_storage = (
43 # <IP Address> => {
44 # ARP => [ $mac, $discovered, $vendor ],
45 # ICMP => ICMP,
46 # TCP => [ $port, $service, $app, $discovered ]
47 # },
48 # }
49 # )
50 my %asset_storage = ();
52 # Parse Command Line
53 GetOptions(
54 'r=s' => \$opt_r,
55 'w=s' => \$opt_w,
56 'n' => \$opt_n,
57 'p' => \$opt_p,
58 'i=s' => \$opt_i,
59 'h|?' => \$opt_h,
60 'help' => \$opt_h
62 usage() if $opt_h;
63 $report_file = $opt_r if ($opt_r);
65 # --------------------------------------------
66 # MAIN
67 # --------------------------------------------
69 # Open Report File
70 open (REPORT, "<$report_file") or die "[!] ERROR: Unable to open $report_file - $!\n";
72 # Read in Report File
73 while (<REPORT>) {
74 chomp;
75 next if (/^asset,vlan,port,proto/);
77 R_REPORT: {
78 # asset,vlan,port,proto,service,[service-info],distance,discovered
79 /^([\w\.:]+),([\d]{1,4}),([\d]{1,5}),([\d]{1,3}),(\S+?),\[(.*)\],([\d]{1,3}),(\d{10})/ && do {
80 ($sip, $vlan, $sport, $proto, $service, $s_info, $distance, $discovered) = ($1, $2, $3, $4, $5, $6, $7, $8);
82 if ($opt_i) {
83 next if not $opt_i eq $sip;
85 $asset=$_;
86 $os = $details = "";
88 if ( $service =~ /SYN/ ) {
89 # 65535:128:1:48:M1460,N,N,S:.:Windows:2000 SP4, XP SP1+
90 #if ($s_info =~ /.*:.*:.*:.*:.*:.*:(.*):(.*):.*:.*:.*:.*hrs/) {
91 if ($s_info =~ /:[\d]{2,4}:\d:.*:.*:.*:(\w+):(.*):link/) {
92 $os = $1;
93 $details = $2;
94 #print "$os - $details\n";
96 } elsif ( $service =~ /SERVER/ || $service =~ /CLIENT/ ) {
97 $s_info =~ s/^(\w+):(.*)$/$2/;
100 # Assign this line to the asset data structure.
101 if ($service =~ /ARP/) {
102 # ARP
103 #if ($service =~ /ARP/) {
104 # $vendor = $1;
105 #} else {
106 $vendor = "unknown";
108 push ( @{ $asset_storage{$sip}->{"ARP"} }, [ $s_info, $discovered, $vendor ]);
110 } elsif ($proto == 1) {
111 # ICMP
112 $asset_storage{$sip}->{"ICMP"} = "ICMP";
114 } elsif ($proto == 6) {
115 # TCP
116 if ( $service =~ /SERVER/ || $service =~ /CLIENT/ ) {
117 push (@{$asset_storage{$sip}{"TCP"}}, [ $sport, $service, $s_info, $discovered ]);
118 } elsif ( $service =~ /SYN/ ) {
119 push (@{$asset_storage{$sip}{"OS"}}, [ $service, $os, $details, $discovered ]);
121 } elsif ($proto == 17) {
122 # UDP
123 if ( $service =~ /SERVER/ || $service =~ /CLIENT/ ) {
124 push (@{$asset_storage{$sip}{"UDP"}}, [ $sport, $service, $s_info, $discovered ]);
127 last R_REPORT;
131 # Close Report File
132 close (REPORT);
134 # Open output file if specified on the command line.
135 if ($opt_w) {
136 open (STDOUT, ">$opt_w") or die "[!] ERROR: $!\n";
139 # Print out this record.
140 my $asset;
141 my $id = 1;
142 foreach $asset (sort (keys (%asset_storage))) {
143 my ($mac);
144 my ($icmp);
145 my (@sorted);
146 my ($i);
148 # Output Asset Header
149 print "$id ------------------------------------------------------\n";
150 print "IP: $asset\n";
152 # Output DNS Name
153 unless ($opt_n) {
154 if (not $opt_p or check_rfc1918($asset)) {
155 my $ass_ip = inet_aton($asset);
156 my ($peer_host) = gethostbyaddr($ass_ip, AF_INET());
157 if($peer_host) {
158 print "DNS: $peer_host";
159 my $fwd_ip = gethostbyname($peer_host.'.');
160 if(not $fwd_ip) {
161 print ' (dnswall: no such domain)'
162 }elsif($fwd_ip ne $ass_ip){
163 print ' ('.inet_ntoa($fwd_ip).') ';
164 my ($rev_host) = gethostbyaddr($fwd_ip, AF_INET());
165 if($rev_host ne $peer_host){
166 print "[$rev_host]";
169 print "\n";
174 my ($os,$desc,$confidence,$timestamp,$flux) = guess_asset_os($asset);
175 print "OS: $os $desc ($confidence%) $flux\n";
176 # Output OS and details
177 #$i = 0;
178 #foreach $_ ( @ { $asset_storage{$asset}->{"OS"}}) {
179 # my ($date) = from_unixtime($_->[3]);
180 # printf("OS %-1s: %-1s ", $_->[0], $_->[1]);
181 # if ($_->[2] ne "unknown") {
182 # printf("- %-18s", $_->[2]);
183 # } else {
184 # printf(" ");
186 # printf(" (%-19s)\n", $date);
187 # $i++;
190 # Output MAC Addresses
191 $i = 0;
192 foreach $_ ( @ { $asset_storage{$asset}->{"ARP"}}) {
193 if ($i == 0) {
194 my ($date) = from_unixtime($_->[1]);
195 printf("MAC(s): %-18s (%-19s)\n", $_->[0], $date);
196 printf("VENDOR: %-18s\n", $_->[2]) if ($_->[2] ne "unknown");
197 $i++;
198 } else {
199 my ($date) = from_unixtime($_->[1]);
200 printf("%-09s %-18s (%-19s)\n", "", $_->[0], $date);
201 printf("%-09s %-18s\n", $_->[2]) if ($_->[2] ne "unknown");
205 # Output ICMP Status
206 if ($asset_storage{$asset}->{"ICMP"}) {
207 print "ICMP: Enabled\n";
209 print "\n";
211 # Output TCP Status
212 if ($asset_storage{$asset}->{"TCP"}) {
213 printf("%-5s %-10s %-30s\n", "Port", "Service", "TCP-Application");
214 @sorted = sort {$$a[0] <=> $$b[0]} @{$asset_storage{$asset}->{"TCP"}};
216 foreach $_ (@sorted) {
217 next if ($_->[3] < $timestamp);
218 printf("%-5d %-10s %-30s\n", $_->[0], $_->[1], $_->[2])
220 #if ($asset_storage{$asset}->{"TCP"}) {
221 print "\n";
224 # Output UDP Status
225 if ($asset_storage{$asset}->{"UDP"}) {
226 printf("%-5s %-10s %-30s\n", "Port", "Service", "UDP-Application");
227 @sorted = sort {$$a[0] <=> $$b[0]} @{$asset_storage{$asset}->{"UDP"}};
229 foreach $_ (@sorted) {
230 next if ($_->[3] < $timestamp);
231 printf("%-5d %-10s %-30s\n", $_->[0], $_->[1], $_->[2])
233 #if ($asset_storage{$asset}->{"UDP"}) {
234 print "\n";
238 $id++;
241 # Close output file if specified on the command line.
242 if ($opt_w) {
243 close (STDOUT);
246 sub check_last_os_switch {
247 my $asset = shift;
248 my $ctimestamp = 0;
249 my $syn = 0;
251 #foreach $OS (@ {$asset_storage{$asset}->{"OS"}}) {
252 foreach $OS (sort { $a <=> $b } (@ {$asset_storage{$asset}->{"OS"}})) {
253 if ($OS->[0] =~ /^SYN$/ ) {
254 $syn += 1;
255 #print "S : $OS->[0] $OS->[1] $OS->[2] $OS->[3] $syn\n";
256 $ctimestamp = $OS->[3] if ($OS->[3] > $ctimestamp);
260 if ($syn == 0) {
261 foreach $OS (sort { $a <=> $b } (@ {$asset_storage{$asset}->{"OS"}})) {
262 if ($OS->[0] =~ /^SYNACK$/ ) {
263 $syn += 1;
264 #print "SA: $OS->[0] $OS->[1] $OS->[2] $OS->[3]\n";
265 $ctimestamp = $OS->[3] if ($OS->[3] > $ctimestamp);
269 #print "R : $ctimestamp\n";
270 push my @return, ($ctimestamp, $syn);
271 return @return;
274 sub guess_asset_os {
275 my $asset = shift;
276 my ($OS, $DETAILS, $CONFIDENCE, $TS, $FLUX) = ("unknown", "unknown", 0, 0, 0);
277 push my @prefiltered, ($OS, $DETAILS, $CONFIDENCE, $TS, $FLUX);
278 my %countos;
279 my %countdesc;
281 # look for latest os switch...
282 ($TS,$FLUX) = check_last_os_switch($asset);
283 #print "TS: $TS\n";
284 # Lets look back the last 12 hours...
285 # The OS might have sent a synack long before a syn
286 # if thats what made the timestamp. And we also might
287 # have missed some servies :)
288 $TS = $TS - 43200;
290 foreach $OS (@ {$asset_storage{$asset}->{"OS"}}) {
291 next if ($OS->[3] < $TS);
292 #print "OS: $OS->[0]\n";
293 if ($OS->[0] =~ /^SYNACK$/ ) {
294 $countos{ $OS->[1] }{"count"} += 4;
295 } elsif ($OS->[0] =~ /^SYN$/ ) {
296 $countos{$OS->[1]}{"count"} += 6;
297 } elsif ($OS->[0] =~ /^ACK$/ || $OS->[0] =~ /^FIN$/ || $OS->[0] =~ /^RST$/ ) {
298 $countos{$OS->[1]}{"count"} += 1;
302 my ($os, $os1, $os2);
303 my $int = 0;
304 for my $os (sort { $countos{$a} <=> $countos{$b} } keys %countos) {
305 next if ($os =~ /unknown/ );
306 if ($int == 0) {
307 $os1 = $os;
308 } else {
309 $os2 = $os;
310 last;
312 $int +=1;
313 #print "$countos{$os}{count}\t$os\n";
315 if (not defined $os1) {
316 if (not defined $os2) {
317 $OS = "unknown";
318 } else {
319 $OS = $os2;
321 } else {
322 $OS = $os1;
325 push my @midfiltered, ("unknown", "unknown", 0, $TS, $FLUX);
326 return @midfiltered unless $OS;
327 return @midfiltered if ($OS =~ /unknown/);
329 #if ( $countos{$os1}{count} > $countos{$os2}{count} ) {
330 # $OS = $os1;
331 #} elsif ( $countos{$os1}{count} == $countos{$os2}{count} ) {
332 # # sort on last timestamp or something...
333 # # in the future
334 # $OS = $os1;
337 foreach my $DESC (@ {$asset_storage{$asset}->{"OS"}}) {
338 next if ($DESC->[3] < $TS);
339 next if not $DESC->[1] =~ /$OS/;
340 if ($DESC->[0] =~ /^SYN$/) {
341 $DETAILS = $DESC->[2];
342 last
343 } elsif ($DESC->[0] =~ /^SYNACK$/) {
344 $DETAILS = $DESC->[2];
345 } else {
346 $DETAILS = $DESC->[2];
350 if (not defined $DETAILS) {
351 foreach my $DESC (@ {$asset_storage{$asset}->{"OS"}}) {
352 next if ($DESC->[3] < $TS);
353 next if not $DESC->[1] =~ /$OS/;
354 if ($DESC->[0] =~ /^RST$/) {
355 $DETAILS = $DESC->[2];
356 last
357 } elsif ($DESC->[0] =~ /^ACK$/) {
358 $DETAILS = $DESC->[2];
359 } else {
360 $DETAILS = $DESC->[2];
365 if ( not defined $OS ) {
366 $DETAILS = "unknown";
367 $CONFIDENCE = 0;
368 $OS = "unknown";
369 } else {
370 $CONFIDENCE = 20 + (10 * $countos{$OS}{count});
371 $CONFIDENCE = 100 if $CONFIDENCE > 100;
373 push my @postfiltered, ($OS, $DETAILS, $CONFIDENCE, $TS, $FLUX);
374 return @postfiltered;
378 # --------------------------------------------
379 # FUNCTION : from_unixtime
380 # DESCRIPTION : This function will convert
381 # : a unix timestamp into a
382 # : normal date.
383 # INPUT : 0 - UNIX timestamp
384 # RETURN : 0 - Formatted Time
385 # --------------------------------------------
386 sub from_unixtime {
387 my ($unixtime) = $_[0];
388 my ($time);
390 my ($sec, $min, $hour, $dmon, $mon, $year,
391 $wday, $yday, $isdst) = localtime($unixtime);
392 $time = sprintf("%04d/%02d/%02d %02d:%02d:%02d",
393 $year + 1900, $mon + 1, $dmon, $hour, $min, $sec);
395 return $time;
398 # --------------------------------------------
399 # FUNCTION : check_rfc1918
400 # DESCRIPTION : This function will check to
401 # : see if a address is a RFC
402 # : 1918 address.
403 # INPUT : 0 - IP Address
404 # RETURN : 1 - Yes
405 # --------------------------------------------
406 sub check_rfc1918 {
407 my ($ip) = $_[0];
409 return 1 if (check_ip($ip, "10.0.0.0/8"));
410 return 1 if (check_ip($ip, "172.16.0.0/12"));
411 return 1 if (check_ip($ip, "192.168.0.0/16"));
412 return 0;
415 # --------------------------------------------
416 # FUNCTION : check_ip
417 # DESCRIPTION : This function will check to
418 # : see if a address falls
419 # : within a network CIDR block.
420 # INPUT : 0 - IP Address
421 # : 1 - CIDR Network
422 # RETURN : 1 - Yes
423 # --------------------------------------------
424 sub check_ip {
425 my ($i) = $_[0];
426 my ($n) = $_[1];
428 return ($i eq $n) unless $n =~ /^(.*)\/(.*)$/;
429 return (((unpack('N',pack('C4',split(/\./,$i))) ^ unpack('N',pack('C4'
430 ,split(/\./,$1)))) & (0xFFFFFFFF << (32 - $2))) == 0);
432 # --------------------------------------------
433 # FUNCTION : usage
434 # --------------------------------------------
435 sub usage {
436 print <<__EOT__;
437 Usage:
438 -r <file> : PRADS Raw Report File
439 -w <file> : Output file
440 -i <IP> : Just get info for this IP
441 -n : Do not convert IP addresses to names.
442 -p : Do not convert RFC 1918 IP addresses to names.
444 __EOT__
445 exit;
448 # --------------------------------------------
449 # FUNCTION : print_header
450 # --------------------------------------------
451 sub print_header {
452 print <<__EOT__;
453 \n prads-asset-report - PRADS Text Reporting Module
454 $version - $date
455 Edward Fjellskaal <edwardfjellskaal\@gmail.com>
456 Kacper Wysocki <comotion\@krutt.org>
458 http://github.com/gamemlinux/prads/
460 __EOT__