nmap-parser 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/BUGS +2 -0
- data/LICENSE +20 -0
- data/README +43 -0
- data/lib/nmap/parser.rb +1602 -0
- metadata +52 -0
data/BUGS
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007, 2008 Kris Katterjohn
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
===============================================================================
|
2
|
+
== Ruby Nmap::Parser http://rubynmap.sourceforge.net ==
|
3
|
+
== Kris Katterjohn katterjohn@gmail.com ==
|
4
|
+
===============================================================================
|
5
|
+
|
6
|
+
$Id: README 70 2008-04-22 23:13:01Z kjak $
|
7
|
+
|
8
|
+
|
9
|
+
OVERVIEW
|
10
|
+
========
|
11
|
+
|
12
|
+
This library provides a Ruby interface to Nmap's scan data. It can run Nmap
|
13
|
+
and parse it's XML output directly from the scan, parse a file containing the
|
14
|
+
XML data from a separate scan, parse a String of XML data from a scan, or parse
|
15
|
+
XML data from an object via its read() method. This information is presented
|
16
|
+
in an easy-to-use and intuitive fashion for storing and manipulating.
|
17
|
+
|
18
|
+
|
19
|
+
REQUIREMENTS and RECOMMENDATIONS
|
20
|
+
================================
|
21
|
+
|
22
|
+
Things Required:
|
23
|
+
|
24
|
+
- Nmap (http://nmap.org) [Only required for parsescan()]
|
25
|
+
|
26
|
+
- REXML Ruby Library [In standard library with Ruby >= 1.8]
|
27
|
+
|
28
|
+
Things Recommended:
|
29
|
+
|
30
|
+
- Open3 Ruby Library [In standard library]
|
31
|
+
|
32
|
+
|
33
|
+
HELP
|
34
|
+
====
|
35
|
+
|
36
|
+
The website has detailed RDoc documentation available. You can create a quick
|
37
|
+
set of these docs yourself by issuing the following command from the base
|
38
|
+
directory:
|
39
|
+
|
40
|
+
$ rdoc lib
|
41
|
+
|
42
|
+
A "doc" directory will be created. Just point your browser to doc/index.html
|
43
|
+
|
data/lib/nmap/parser.rb
ADDED
@@ -0,0 +1,1602 @@
|
|
1
|
+
# Ruby library for parsing Nmap's XML output
|
2
|
+
#
|
3
|
+
# http://rubynmap.sourceforge.net
|
4
|
+
#
|
5
|
+
# Author: Kris Katterjohn <katterjohn@gmail.com>
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007, 2008 Kris Katterjohn
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
10
|
+
# of this software and associated documentation files (the "Software"), to deal
|
11
|
+
# in the Software without restriction, including without limitation the rights
|
12
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
# copies of the Software, and to permit persons to whom the Software is
|
14
|
+
# furnished to do so, subject to the following conditions:
|
15
|
+
#
|
16
|
+
# The above copyright notice and this permission notice shall be included in
|
17
|
+
# all copies or substantial portions of the Software.
|
18
|
+
#
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
25
|
+
# THE SOFTWARE.
|
26
|
+
|
27
|
+
# $Id: parser.rb 77 2008-04-24 22:19:29Z kjak $
|
28
|
+
# https://rubynmap.svn.sourceforge.net/svnroot/rubynmap
|
29
|
+
|
30
|
+
require 'rexml/document'
|
31
|
+
|
32
|
+
begin
|
33
|
+
require 'open3'
|
34
|
+
rescue LoadError
|
35
|
+
# We'll resort to IO.popen()
|
36
|
+
end
|
37
|
+
|
38
|
+
# Just holds the big Parser class.
|
39
|
+
module Nmap
|
40
|
+
end
|
41
|
+
|
42
|
+
=begin rdoc
|
43
|
+
|
44
|
+
== What Is This Library For?
|
45
|
+
|
46
|
+
This library provides a Ruby interface to Nmap's scan data. It can run Nmap
|
47
|
+
and parse it's XML output directly from the scan, parse a file containing the
|
48
|
+
XML data from a separate scan, parse a String of XML data from a scan, or parse
|
49
|
+
XML data from an object via its read() method. This information is presented
|
50
|
+
in an easy-to-use and intuitive fashion for storing and manipulating.
|
51
|
+
|
52
|
+
The Nmap Security Scanner is a great program written and maintained by
|
53
|
+
Fyodor <fyodor@insecure.org>. Its main function is port scanning, but it also
|
54
|
+
has service and operating system detection, it's own scripting engine and a
|
55
|
+
whole lot more. One of it's many available output formats is XML, which allows
|
56
|
+
machines to handle all of the information instead of us slowly sifting through
|
57
|
+
tons of output!
|
58
|
+
|
59
|
+
If you've ever used Anthony Persaud's Perl Nmap::Parser, you'll notice some
|
60
|
+
similarities and should pick up on this API quickly. Just remember, it's far
|
61
|
+
from exactly the same! There are more classes, many different methods, and
|
62
|
+
blocks are used extensively (or available for use).
|
63
|
+
|
64
|
+
== Conventions
|
65
|
+
|
66
|
+
Depending on the data type, unavailable information is presented differently:
|
67
|
+
|
68
|
+
- Arrays are empty
|
69
|
+
- Non-arrays are nil, unless it's a method that returns the size of one of
|
70
|
+
the previously mentioned empty arrays. In this case they still return the
|
71
|
+
size (which would be 0).
|
72
|
+
|
73
|
+
All information available as arrays are presented via methods. These methods
|
74
|
+
not only return the array, but they also yield each element to a block if one
|
75
|
+
is given.
|
76
|
+
|
77
|
+
== Module Hierarchy
|
78
|
+
|
79
|
+
Nmap::Parser
|
80
|
+
|
|
81
|
+
+ ::Session <- Scan session information
|
82
|
+
|
|
83
|
+
+ ::Host <- General host information
|
84
|
+
|
|
85
|
+
+ ::ExtraPorts <- Ports consolidated in an "ignored" state
|
86
|
+
|
|
87
|
+
+ ::Port <- General port information
|
88
|
+
| |
|
89
|
+
| + ::Service <- Port Service information
|
90
|
+
|
|
91
|
+
+ ::Script <- NSE Script information (both host and port)
|
92
|
+
|
|
93
|
+
+ ::Times <- Timimg information (srtt, etc)
|
94
|
+
|
|
95
|
+
+ ::Traceroute <- General Traceroute information
|
96
|
+
| |
|
97
|
+
| + ::Hop <- Individual Hop information
|
98
|
+
|
|
99
|
+
+ ::OS <- OS Detection information
|
100
|
+
|
|
101
|
+
+ ::OSClass <- OS Class information
|
102
|
+
|
|
103
|
+
+ ::OSMatch <- OS Match information
|
104
|
+
|
105
|
+
== Parsing XML Data Already Available
|
106
|
+
|
107
|
+
require 'nmap/parser'
|
108
|
+
|
109
|
+
parser = Nmap::Parser.new(xml) # String of XML data
|
110
|
+
|
111
|
+
== Reading and Parsing from a File
|
112
|
+
|
113
|
+
require 'nmap/parser'
|
114
|
+
|
115
|
+
parser = Nmap::Parser.parsefile("log.xml")
|
116
|
+
|
117
|
+
== Reading and Parsing from an Object
|
118
|
+
|
119
|
+
The parseread() function isn't limited to IO objects like $stdin is
|
120
|
+
in the example: it can read from any object that responds to a read()
|
121
|
+
method that returns a String.
|
122
|
+
|
123
|
+
require 'nmap/parser'
|
124
|
+
|
125
|
+
parser = Nmap::Parser.parseread($stdin)
|
126
|
+
|
127
|
+
== Scanning and Parsing
|
128
|
+
|
129
|
+
This is the only parser option that requires Nmap to be available
|
130
|
+
|
131
|
+
require 'nmap/parser'
|
132
|
+
|
133
|
+
parser = Nmap::Parser.parsescan("sudo nmap", "--script all 192.168.1.0/24")
|
134
|
+
|
135
|
+
== Actually Doing Something
|
136
|
+
|
137
|
+
After printing a little session information, this example will cycle
|
138
|
+
through all of the up hosts, printing service information on the
|
139
|
+
open TCP ports, along with the name and output of any scripts that
|
140
|
+
ran (on the host and individual ports).
|
141
|
+
|
142
|
+
puts "Nmap args: #{parser.session.scan_args}"
|
143
|
+
|
144
|
+
puts "Runtime: #{parser.session.scan_time} seconds"
|
145
|
+
|
146
|
+
puts
|
147
|
+
|
148
|
+
parser.hosts("up") do |host|
|
149
|
+
puts "** Host #{host.addr} is up **"
|
150
|
+
puts
|
151
|
+
|
152
|
+
host.tcp_ports("open") do |port|
|
153
|
+
srv = port.service
|
154
|
+
|
155
|
+
puts "Port ##{port.num} is open (#{port.reason})"
|
156
|
+
puts "\tService: #{srv.name}" if srv.name
|
157
|
+
puts "\tProduct: #{srv.product}" if srv.product
|
158
|
+
puts "\tVersion: #{srv.product}" if srv.version
|
159
|
+
puts
|
160
|
+
|
161
|
+
port.scripts do |script|
|
162
|
+
puts "\tScript Name: #{script.id}"
|
163
|
+
puts "\tScript Output: #{script.output}"
|
164
|
+
puts
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
if host.scripts.any?
|
169
|
+
puts "Host Scripts Run:"
|
170
|
+
puts
|
171
|
+
|
172
|
+
host.scripts do |script|
|
173
|
+
puts "\tScript Name: #{script.id}"
|
174
|
+
puts "\tScript Output: #{script.output}"
|
175
|
+
puts
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
puts
|
180
|
+
end
|
181
|
+
|
182
|
+
=end
|
183
|
+
class Nmap::Parser
|
184
|
+
# Holds the raw XML output from Nmap
|
185
|
+
attr_reader :rawxml
|
186
|
+
# ::Session object for this scan
|
187
|
+
attr_reader :session
|
188
|
+
|
189
|
+
# Read and parse XML from the +obj+. +obj+ can be any object type
|
190
|
+
# that responds to a read() method that returns a String. IO and
|
191
|
+
# File are just a couple of examples.
|
192
|
+
#
|
193
|
+
# Returns a new Nmap::Parser object, and passes it to a block if
|
194
|
+
# one is given
|
195
|
+
def self.parseread(obj) # :yields: parser
|
196
|
+
if not obj.respond_to?("read")
|
197
|
+
raise "Object must respond to read()"
|
198
|
+
end
|
199
|
+
|
200
|
+
r = obj.read
|
201
|
+
|
202
|
+
raise "read() returned #{r.class} type!" if not r.is_a?String
|
203
|
+
|
204
|
+
p = new(r)
|
205
|
+
|
206
|
+
yield p if block_given?
|
207
|
+
|
208
|
+
p
|
209
|
+
end
|
210
|
+
|
211
|
+
# Read and parse contents of the Nmap XML file +filename+
|
212
|
+
#
|
213
|
+
# Returns a new Nmap::Parser object, and passes it to a block if
|
214
|
+
# one is given
|
215
|
+
def self.parsefile(filename) # :yields: parser
|
216
|
+
begin
|
217
|
+
p = parseread(File.open(filename))
|
218
|
+
rescue
|
219
|
+
raise "Error parsing \"#{filename}\": #{$!}"
|
220
|
+
end
|
221
|
+
|
222
|
+
yield p if block_given?
|
223
|
+
|
224
|
+
p
|
225
|
+
end
|
226
|
+
|
227
|
+
# Runs "+nmap+ -d +args+ +targets+"; returns a new Nmap::Parser object,
|
228
|
+
# and passes it to a block if one is given.
|
229
|
+
#
|
230
|
+
# +nmap+ is here to allow you to do things like:
|
231
|
+
#
|
232
|
+
# parser = Nmap::Parser.parsescan("sudo ./nmap", ....)
|
233
|
+
#
|
234
|
+
# and still make it easy for me to inject the options for XML
|
235
|
+
# output and debugging.
|
236
|
+
#
|
237
|
+
# +args+ can't contain arguments like -oA, -oX, etc. as these can
|
238
|
+
# interfere with Parser's processing. If you need that other output,
|
239
|
+
# just run Nmap yourself and pass -oX output to Parser via new. Or,
|
240
|
+
# you can use rawxml to grab the whole XML (as a String) and save it
|
241
|
+
# to a different file.
|
242
|
+
#
|
243
|
+
# +targets+ is an array of targets which will be split and appended to
|
244
|
+
# the command. It's optional and only for convenience because you can
|
245
|
+
# put any targets you want scanned in +args+.
|
246
|
+
def self.parsescan(nmap, args, targets = []) # :yields: parser
|
247
|
+
if args =~ /[^-]-o|^-o/
|
248
|
+
raise "Output option (-o*) passed to parsescan()"
|
249
|
+
end
|
250
|
+
|
251
|
+
# Enable debugging, give us our XML output, pass args
|
252
|
+
command = nmap + " -d -oX - " + args + " "
|
253
|
+
|
254
|
+
command += targets.join(" ") if targets.any?
|
255
|
+
|
256
|
+
p = nil
|
257
|
+
|
258
|
+
begin
|
259
|
+
# First try popen3() if it loaded successfully..
|
260
|
+
Open3.popen3(command) do |sin, sout, serr|
|
261
|
+
p = parseread(sout)
|
262
|
+
end
|
263
|
+
rescue NameError
|
264
|
+
# ..but fall back to popen() if not
|
265
|
+
IO.popen(command) do |io|
|
266
|
+
p = parseread(io)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
yield p if block_given?
|
271
|
+
|
272
|
+
p
|
273
|
+
end
|
274
|
+
|
275
|
+
# Returns an array of Host objects, and passes them each to a block if
|
276
|
+
# one is given
|
277
|
+
#
|
278
|
+
# If an argument is given, only hosts matching +status+ are given
|
279
|
+
def hosts(status = "") # :yields: host
|
280
|
+
shosts = []
|
281
|
+
|
282
|
+
@hosts.each do |host|
|
283
|
+
if status.empty? or host.status == status
|
284
|
+
shosts << host
|
285
|
+
yield host if block_given?
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
shosts
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns a Host object for the host with the specified IP address
|
293
|
+
# or hostname +hostip+
|
294
|
+
def host(hostip)
|
295
|
+
@hosts.find do |host|
|
296
|
+
host.addr == hostip or host.hostname == hostip
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
alias get_host host
|
301
|
+
|
302
|
+
# Deletes host with the specified IP address or hostname +hostip+
|
303
|
+
#
|
304
|
+
# Note: From inside of a block given to a method like hosts() or
|
305
|
+
# get_ips(), calling this method on a host passed to the block may
|
306
|
+
# lead to adverse effects:
|
307
|
+
#
|
308
|
+
# parser.hosts { |x| puts x.addr; del_host(x) } # Don't do this!
|
309
|
+
def del_host(hostip)
|
310
|
+
@hosts.delete_if do |host|
|
311
|
+
host.addr == hostip or host.hostname == hostip
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
alias delete_host del_host
|
316
|
+
|
317
|
+
# Returns an array of IPs scanned, and passes them each to a
|
318
|
+
# block if one is given
|
319
|
+
#
|
320
|
+
# If an argument is given, only hosts matching +status+ are given
|
321
|
+
def get_ips(status = "") # :yields: host.addr
|
322
|
+
ips = []
|
323
|
+
|
324
|
+
@hosts.each do |host|
|
325
|
+
if status.empty? or host.status == status
|
326
|
+
ips << host.addr
|
327
|
+
yield host.addr if block_given?
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
ips
|
332
|
+
end
|
333
|
+
|
334
|
+
# This operator compares the rawxml members
|
335
|
+
def ==(parser)
|
336
|
+
@rawxml == parser.rawxml
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
def initialize(xml) # :yields: parser
|
342
|
+
if not xml.is_a?String
|
343
|
+
raise "Invalid input: new() should be passed a String"
|
344
|
+
end
|
345
|
+
|
346
|
+
parse(xml)
|
347
|
+
|
348
|
+
yield self if block_given?
|
349
|
+
end
|
350
|
+
|
351
|
+
def parse(xml)
|
352
|
+
@rawxml = xml
|
353
|
+
|
354
|
+
begin
|
355
|
+
root = REXML::Document.new(xml).root
|
356
|
+
rescue
|
357
|
+
raise "Error parsing XML: #{$!}"
|
358
|
+
end
|
359
|
+
|
360
|
+
raise "Error in XML data" if root.nil?
|
361
|
+
|
362
|
+
@session = Session.new(root)
|
363
|
+
|
364
|
+
@hosts = []
|
365
|
+
|
366
|
+
root.each_element('host') do |host|
|
367
|
+
@hosts << Host.new(host)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# This holds session information, such as runtime, Nmap's arguments,
|
373
|
+
# and verbosity/debugging
|
374
|
+
class Nmap::Parser::Session
|
375
|
+
# Holds the command run to initiate the scan
|
376
|
+
attr_reader :scan_args
|
377
|
+
# The Nmap version number used to scan
|
378
|
+
attr_reader :nmap_version
|
379
|
+
# XML version of Nmap's output
|
380
|
+
attr_reader :xml_version
|
381
|
+
# Starting time
|
382
|
+
attr_reader :start_str, :start_time
|
383
|
+
# Ending time
|
384
|
+
attr_reader :stop_str, :stop_time
|
385
|
+
# How long the scan took (in seconds). Same as stop_time - start_time
|
386
|
+
attr_reader :scan_time
|
387
|
+
# Amount of verbosity (-v) used while scanning
|
388
|
+
attr_reader :verbose
|
389
|
+
# Amount of debugging (-d) used while scanning
|
390
|
+
attr_reader :debug
|
391
|
+
|
392
|
+
alias verbosity verbose
|
393
|
+
alias debugging debug
|
394
|
+
|
395
|
+
# Returns the total number of services that were scanned or, if an
|
396
|
+
# argument is given, returns the number of services scanned for +type+
|
397
|
+
# (e.g. "syn")
|
398
|
+
def numservices(type = "")
|
399
|
+
total = 0
|
400
|
+
|
401
|
+
@scaninfo.each do |info|
|
402
|
+
if type.empty?
|
403
|
+
total += info.numservices
|
404
|
+
elsif info.type == type
|
405
|
+
return info.numservices
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
total
|
410
|
+
end
|
411
|
+
|
412
|
+
# Returns the protocol associated with the specified scan +type+
|
413
|
+
# (e.g. "tcp" for type "syn")
|
414
|
+
def scan_type_proto(type)
|
415
|
+
@scaninfo.each do |info|
|
416
|
+
return info.proto if info.type == type
|
417
|
+
end
|
418
|
+
|
419
|
+
nil
|
420
|
+
end
|
421
|
+
|
422
|
+
# Returns an array of all the scan types performed, and passes them
|
423
|
+
# each to a block if one if given
|
424
|
+
def scan_types() # :yields: scantype
|
425
|
+
types = []
|
426
|
+
|
427
|
+
@scaninfo.each do |info|
|
428
|
+
types << info.type
|
429
|
+
yield info.type if block_given?
|
430
|
+
end
|
431
|
+
|
432
|
+
types
|
433
|
+
end
|
434
|
+
|
435
|
+
private
|
436
|
+
|
437
|
+
def initialize(root)
|
438
|
+
parse(root)
|
439
|
+
end
|
440
|
+
|
441
|
+
def parse(root)
|
442
|
+
@scan_args = root.attributes['args']
|
443
|
+
|
444
|
+
@nmap_version = root.attributes['version']
|
445
|
+
|
446
|
+
@xml_version = root.attributes['xmloutputversion'].to_f
|
447
|
+
|
448
|
+
@start_str = root.attributes['startstr']
|
449
|
+
@start_time = root.attributes['start'].to_i
|
450
|
+
|
451
|
+
@stop_str = root.elements['runstats/finished'].attributes['timestr']
|
452
|
+
@stop_time = root.elements['runstats/finished'].attributes['time'].to_i
|
453
|
+
|
454
|
+
@scan_time = @stop_time - @start_time
|
455
|
+
|
456
|
+
@verbose = root.elements['verbose'].attributes['level'].to_i
|
457
|
+
@debug = root.elements['debugging'].attributes['level'].to_i
|
458
|
+
|
459
|
+
@scaninfo = []
|
460
|
+
|
461
|
+
root.each_element('scaninfo') do |info|
|
462
|
+
@scaninfo << ScanInfo.new(info)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
class Nmap::Parser::Session::ScanInfo # :nodoc: all
|
468
|
+
attr_reader :type, :proto, :numservices
|
469
|
+
|
470
|
+
private
|
471
|
+
|
472
|
+
def initialize(info)
|
473
|
+
parse(info)
|
474
|
+
end
|
475
|
+
|
476
|
+
def parse(info)
|
477
|
+
@type = info.attributes['type']
|
478
|
+
@proto = info.attributes['protocol']
|
479
|
+
@numservices = info.attributes['numservices'].to_i
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
# This holds all of the information about a target host.
|
484
|
+
#
|
485
|
+
# Status, IP/MAC addresses, hostnames, all that. Port information is
|
486
|
+
# available in this class; either accessed through here or directly
|
487
|
+
# from a Port object.
|
488
|
+
class Nmap::Parser::Host
|
489
|
+
# The status of the host, typically "up" or "down"
|
490
|
+
attr_reader :status
|
491
|
+
# The reason for the status
|
492
|
+
attr_reader :reason
|
493
|
+
# The IPv4 address
|
494
|
+
attr_reader :ip4_addr
|
495
|
+
# The IPv6 address
|
496
|
+
attr_reader :ip6_addr
|
497
|
+
# The MAC address
|
498
|
+
attr_reader :mac_addr
|
499
|
+
# The MAC vendor
|
500
|
+
attr_reader :mac_vendor
|
501
|
+
# ::OS object holding Operating System information
|
502
|
+
attr_reader :os
|
503
|
+
# The number of "weird responses"
|
504
|
+
attr_reader :smurf
|
505
|
+
# TCP Sequence Number information
|
506
|
+
attr_reader :tcpsequence_index, :tcpsequence_class
|
507
|
+
# TCP Sequence Number information
|
508
|
+
attr_reader :tcpsequence_values, :tcpsequence_difficulty
|
509
|
+
# IPID Sequence Number information
|
510
|
+
attr_reader :ipidsequence_class, :ipidsequence_values
|
511
|
+
# TCP Timestamp Sequence Number information
|
512
|
+
attr_reader :tcptssequence_class, :tcptssequence_values
|
513
|
+
# Uptime information
|
514
|
+
attr_reader :uptime_seconds, :uptime_lastboot
|
515
|
+
# ::Traceroute object
|
516
|
+
attr_reader :traceroute
|
517
|
+
# Network distance (not necessarily the same as from traceroute)
|
518
|
+
attr_reader :distance
|
519
|
+
# ::Times object holding timing information
|
520
|
+
attr_reader :times
|
521
|
+
|
522
|
+
alias ipv4_addr ip4_addr
|
523
|
+
alias ipv6_addr ip6_addr
|
524
|
+
|
525
|
+
# Returns the IPv4 or IPv6 address of host
|
526
|
+
def addr
|
527
|
+
@ip4_addr or @ip6_addr
|
528
|
+
end
|
529
|
+
|
530
|
+
# Returns an array containing all of the hostnames for this host,
|
531
|
+
# and passes them each to a block if one is given
|
532
|
+
def all_hostnames
|
533
|
+
if block_given?
|
534
|
+
@hostnames.each do |hostname|
|
535
|
+
yield hostname
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
@hostnames
|
540
|
+
end
|
541
|
+
|
542
|
+
alias hostnames all_hostnames
|
543
|
+
|
544
|
+
# Returns the first hostname, or nil if unavailable
|
545
|
+
def hostname
|
546
|
+
@hostnames[0]
|
547
|
+
end
|
548
|
+
|
549
|
+
# Returns an array of ExtraPorts objects, and passes them each to a
|
550
|
+
# block if one if given
|
551
|
+
def extraports
|
552
|
+
if block_given?
|
553
|
+
@extraports.each do |extraports|
|
554
|
+
yield extraports
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
@extraports
|
559
|
+
end
|
560
|
+
|
561
|
+
# Returns the Port object for the TCP port +portnum+, and passes it to
|
562
|
+
# a block if one is given
|
563
|
+
def tcp_port(portnum) # :yields: port
|
564
|
+
yield @tcpPorts[portnum.to_s] if block_given?
|
565
|
+
|
566
|
+
@tcpPorts[portnum.to_s]
|
567
|
+
end
|
568
|
+
|
569
|
+
# Returns an array of Port objects for each TCP port, and passes them
|
570
|
+
# each to a block if one is given
|
571
|
+
#
|
572
|
+
# If an argument is given, only ports matching +state+ are given.
|
573
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
574
|
+
def tcp_ports(state = "")
|
575
|
+
list = []
|
576
|
+
|
577
|
+
@tcpPorts.invert.each_key do |port|
|
578
|
+
list << port if state.empty? or port.state >= state
|
579
|
+
end
|
580
|
+
|
581
|
+
list.sort!
|
582
|
+
|
583
|
+
if block_given?
|
584
|
+
list.each do |port|
|
585
|
+
yield port
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
list
|
590
|
+
end
|
591
|
+
|
592
|
+
# Returns an array of TCP port numbers, and passes them each to a block
|
593
|
+
# if one given
|
594
|
+
#
|
595
|
+
# If an argument is given, only ports matching +state+ are given.
|
596
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
597
|
+
def tcp_port_list(state = "")
|
598
|
+
list = []
|
599
|
+
|
600
|
+
@tcpPorts.invert.each_key do |port|
|
601
|
+
list << port.num if state.empty? or port.state >= state
|
602
|
+
end
|
603
|
+
|
604
|
+
list.sort!
|
605
|
+
|
606
|
+
if block_given?
|
607
|
+
list.each do |port|
|
608
|
+
yield port
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
list
|
613
|
+
end
|
614
|
+
|
615
|
+
# Returns state reason of TCP port +portnum+
|
616
|
+
def tcp_reason(portnum)
|
617
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
618
|
+
|
619
|
+
@tcpPorts[portnum.to_s].reason
|
620
|
+
end
|
621
|
+
|
622
|
+
# Returns the Script object for the script +name+ run against the
|
623
|
+
# TCP port +portnum+
|
624
|
+
def tcp_script(portnum, name)
|
625
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
626
|
+
|
627
|
+
@tcpPorts[portnum.to_s].script(name)
|
628
|
+
end
|
629
|
+
|
630
|
+
# Returns an array of Script objects for each script run on the
|
631
|
+
# TCP port +portnum+, and passes them each to a block if given
|
632
|
+
def tcp_scripts(portnum)
|
633
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
634
|
+
|
635
|
+
if block_given?
|
636
|
+
@tcpPorts[portnum.to_s].scripts do |script|
|
637
|
+
yield script
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
@tcpPorts[portnum.to_s].scripts
|
642
|
+
end
|
643
|
+
|
644
|
+
# Returns the output of the script +name+ on the TCP port +portnum+
|
645
|
+
def tcp_script_output(portnum, name)
|
646
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
647
|
+
|
648
|
+
@tcpPorts[portnum.to_s].script_output(name)
|
649
|
+
end
|
650
|
+
|
651
|
+
# Returns a Port::Service object for TCP port +portnum+
|
652
|
+
def tcp_service(portnum)
|
653
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
654
|
+
|
655
|
+
@tcpPorts[portnum.to_s].service
|
656
|
+
end
|
657
|
+
|
658
|
+
# Returns state of TCP port +portnum+
|
659
|
+
def tcp_state(portnum)
|
660
|
+
return nil if @tcpPorts[portnum.to_s].nil?
|
661
|
+
|
662
|
+
@tcpPorts[portnum.to_s].state
|
663
|
+
end
|
664
|
+
|
665
|
+
# Returns the Port object for the UDP port +portnum+, and passes it to
|
666
|
+
# a block if one is given
|
667
|
+
def udp_port(portnum) # :yields: port
|
668
|
+
yield @udpPorts[portnum.to_s] if block_given?
|
669
|
+
|
670
|
+
@udpPorts[portnum.to_s]
|
671
|
+
end
|
672
|
+
|
673
|
+
# Returns an array of Port objects for each UDP port, and passes them
|
674
|
+
# each to a block if one is given
|
675
|
+
#
|
676
|
+
# If an argument is given, only ports matching +state+ are given.
|
677
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
678
|
+
def udp_ports(state = "")
|
679
|
+
list = []
|
680
|
+
|
681
|
+
@udpPorts.invert.each_key do |port|
|
682
|
+
list << port if state.empty? or port.state >= state
|
683
|
+
end
|
684
|
+
|
685
|
+
list.sort!
|
686
|
+
|
687
|
+
if block_given?
|
688
|
+
list.each do |port|
|
689
|
+
yield port
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
list
|
694
|
+
end
|
695
|
+
|
696
|
+
# Returns an array of UDP port numbers, and passes them each to a block
|
697
|
+
# if one is given
|
698
|
+
#
|
699
|
+
# If an argument is given, only ports matching +state+ are given.
|
700
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
701
|
+
def udp_port_list(state = "")
|
702
|
+
list = []
|
703
|
+
|
704
|
+
@udpPorts.invert.each_key do |port|
|
705
|
+
list << port.num if state.empty? or port.state >= state
|
706
|
+
end
|
707
|
+
|
708
|
+
list.sort!
|
709
|
+
|
710
|
+
if block_given?
|
711
|
+
list.each do |port|
|
712
|
+
yield port
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
list
|
717
|
+
end
|
718
|
+
|
719
|
+
# Returns state reason of UDP port +portnum+
|
720
|
+
def udp_reason(portnum)
|
721
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
722
|
+
|
723
|
+
@udpPorts[portnum.to_s].reason
|
724
|
+
end
|
725
|
+
|
726
|
+
# Returns the Script object for the script +name+ run against the
|
727
|
+
# UDP port +portnum+
|
728
|
+
def udp_script(portnum, name)
|
729
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
730
|
+
|
731
|
+
@udpPorts[portnum.to_s].script(name)
|
732
|
+
end
|
733
|
+
|
734
|
+
# Returns an array of Script objects for each script run on the
|
735
|
+
# UDP port +portnum+, and passes them each to a block if given
|
736
|
+
def udp_scripts(portnum)
|
737
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
738
|
+
|
739
|
+
if block_given?
|
740
|
+
@udpPorts[portnum.to_s].scripts do |script|
|
741
|
+
yield script
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
@udpPorts[portnum.to_s].scripts
|
746
|
+
end
|
747
|
+
|
748
|
+
# Returns the output of the script +name+ on the UDP port +portnum+
|
749
|
+
def udp_script_output(portnum, name)
|
750
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
751
|
+
|
752
|
+
@udpPorts[portnum.to_s].script_output(name)
|
753
|
+
end
|
754
|
+
|
755
|
+
# Returns a Port::Service object for UDP port +portnum+
|
756
|
+
def udp_service(portnum)
|
757
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
758
|
+
|
759
|
+
@udpPorts[portnum.to_s].service
|
760
|
+
end
|
761
|
+
|
762
|
+
# Returns state of UDP port +portnum+
|
763
|
+
def udp_state(portnum)
|
764
|
+
return nil if @udpPorts[portnum.to_s].nil?
|
765
|
+
|
766
|
+
@udpPorts[portnum.to_s].state
|
767
|
+
end
|
768
|
+
|
769
|
+
# Returns the Port object for the IP protocol +protonum+, and passes it
|
770
|
+
# to a block if one is given
|
771
|
+
def ip_proto(protonum) # :yields: proto
|
772
|
+
yield @ipProtos[protonum.to_s] if block_given?
|
773
|
+
|
774
|
+
@ipProtos[protonum.to_s]
|
775
|
+
end
|
776
|
+
|
777
|
+
# Returns an array of Port objects for each IP protocol, and passes
|
778
|
+
# them each to a block if one is given
|
779
|
+
#
|
780
|
+
# If an argument is given, only protocols matching +state+ are given.
|
781
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
782
|
+
def ip_protos(state = "")
|
783
|
+
list = []
|
784
|
+
|
785
|
+
@ipProtos.invert.each_key do |proto|
|
786
|
+
list << proto if state.empty? or proto.state >= state
|
787
|
+
end
|
788
|
+
|
789
|
+
list.sort!
|
790
|
+
|
791
|
+
if block_given?
|
792
|
+
list.each do |proto|
|
793
|
+
yield proto
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
list
|
798
|
+
end
|
799
|
+
|
800
|
+
# Returns an array of IP protocol numbers, and passes them each to a
|
801
|
+
# block if one given
|
802
|
+
#
|
803
|
+
# If an argument is given, only protocols matching +state+ are given.
|
804
|
+
# Note: "open" will also match "open|filtered", but not vice versa.
|
805
|
+
def ip_proto_list(state = "")
|
806
|
+
list = []
|
807
|
+
|
808
|
+
@ipProtos.invert.each_key do |proto|
|
809
|
+
list << proto.num if state.empty? or proto.state >= state
|
810
|
+
end
|
811
|
+
|
812
|
+
list.sort!
|
813
|
+
|
814
|
+
if block_given?
|
815
|
+
list.each do |proto|
|
816
|
+
yield proto
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
list
|
821
|
+
end
|
822
|
+
|
823
|
+
# Returns state reason of IP protocol +protonum+
|
824
|
+
def ip_reason(protonum)
|
825
|
+
return nil if @ipProtos[protonum.to_s].nil?
|
826
|
+
|
827
|
+
@ipProtos[protonum.to_s].reason
|
828
|
+
end
|
829
|
+
|
830
|
+
# Returns a Port::Service object for IP protocol +protonum+
|
831
|
+
def ip_service(protonum)
|
832
|
+
return nil if @ipProtos[protonum.to_s].nil?
|
833
|
+
|
834
|
+
@ipProtos[protonum.to_s].service
|
835
|
+
end
|
836
|
+
|
837
|
+
# Returns state of IP protocol +protonum+
|
838
|
+
def ip_state(protonum)
|
839
|
+
return nil if @ipProtos[protonum.to_s].nil?
|
840
|
+
|
841
|
+
@ipProtos[protonum.to_s].state
|
842
|
+
end
|
843
|
+
|
844
|
+
# Returns the Script object for the specified host script +name+
|
845
|
+
def script(name)
|
846
|
+
@scripts.find do |script|
|
847
|
+
script.id == name
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
# Returns an array of Script objects for each host script run, and
|
852
|
+
# passes them each to a block if given
|
853
|
+
def scripts
|
854
|
+
if block_given?
|
855
|
+
@scripts.each do |script|
|
856
|
+
yield script
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
860
|
+
@scripts
|
861
|
+
end
|
862
|
+
|
863
|
+
# Returns the output of the specified host script +name+
|
864
|
+
def script_output(name)
|
865
|
+
@scripts.each do |script|
|
866
|
+
return script.output if script.id == name
|
867
|
+
end
|
868
|
+
|
869
|
+
nil
|
870
|
+
end
|
871
|
+
|
872
|
+
private
|
873
|
+
|
874
|
+
def initialize(hostinfo)
|
875
|
+
parse(hostinfo)
|
876
|
+
end
|
877
|
+
|
878
|
+
# Convert a string like '22-25,80,110-900' into
|
879
|
+
# an array with all the uncondensed elements, e.g.
|
880
|
+
# [ '22', '23', '24', '25, '80', '110', '111' ... ]
|
881
|
+
def parsePortlist(ports)
|
882
|
+
list = []
|
883
|
+
|
884
|
+
ports.split(",").each do |set|
|
885
|
+
pa = set.split("-")
|
886
|
+
|
887
|
+
if pa.length > 1
|
888
|
+
pa[0].upto(pa[1]) do |p|
|
889
|
+
list << p
|
890
|
+
end
|
891
|
+
else
|
892
|
+
list << portc
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
list
|
897
|
+
end
|
898
|
+
|
899
|
+
def parseAddr(elem)
|
900
|
+
case elem.attributes['addrtype']
|
901
|
+
when "mac"
|
902
|
+
@mac_addr = elem.attributes['addr']
|
903
|
+
@mac_vendor = elem.attributes['vendor']
|
904
|
+
when "ipv4"
|
905
|
+
@ip4_addr = elem.attributes['addr']
|
906
|
+
when "ipv6"
|
907
|
+
@ip6_addr = elem.attributes['addr']
|
908
|
+
end
|
909
|
+
end
|
910
|
+
|
911
|
+
def parseHostnames(elem)
|
912
|
+
@hostnames = []
|
913
|
+
|
914
|
+
return nil if elem.nil?
|
915
|
+
|
916
|
+
elem.each_element('hostname') do |name|
|
917
|
+
@hostnames << name.attributes['name']
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
def parsePorts(ports)
|
922
|
+
@tcpPorts = {}
|
923
|
+
@udpPorts = {}
|
924
|
+
@ipProtos = {}
|
925
|
+
|
926
|
+
return nil if ports.nil?
|
927
|
+
|
928
|
+
ports.each_element('port') do |port|
|
929
|
+
num = port.attributes['portid']
|
930
|
+
proto = port.attributes['protocol']
|
931
|
+
|
932
|
+
if proto == "tcp"
|
933
|
+
@tcpPorts[num] = Port.new(port)
|
934
|
+
elsif proto == "udp"
|
935
|
+
@udpPorts[num] = Port.new(port)
|
936
|
+
elsif proto == "ip"
|
937
|
+
@ipProtos[num] = Port.new(port)
|
938
|
+
end
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
def parseExtraPorts(ports)
|
943
|
+
@extraports = []
|
944
|
+
|
945
|
+
return nil if ports.nil?
|
946
|
+
|
947
|
+
ports.each_element('extraports') do |e|
|
948
|
+
@extraports << ExtraPorts.new(e)
|
949
|
+
end
|
950
|
+
|
951
|
+
@extraports.sort! do |x, y|
|
952
|
+
x.count <=> y.count
|
953
|
+
end
|
954
|
+
end
|
955
|
+
|
956
|
+
def parseScripts(scriptlist)
|
957
|
+
@scripts = []
|
958
|
+
|
959
|
+
return nil if scriptlist.nil?
|
960
|
+
|
961
|
+
scriptlist.each_element('script') do |script|
|
962
|
+
@scripts << Script.new(script)
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
def tcpseq(seq)
|
967
|
+
return nil if seq.nil?
|
968
|
+
|
969
|
+
@tcpsequence_index = seq.attributes['index']
|
970
|
+
@tcpsequence_class = seq.attributes['class']
|
971
|
+
@tcpsequence_values = seq.attributes['values']
|
972
|
+
@tcpsequence_difficulty = seq.attributes['difficulty']
|
973
|
+
end
|
974
|
+
|
975
|
+
def ipidseq(seq)
|
976
|
+
return nil if seq.nil?
|
977
|
+
|
978
|
+
@ipidsequence_class = seq.attributes['class']
|
979
|
+
@ipidsequence_values = seq.attributes['values']
|
980
|
+
end
|
981
|
+
|
982
|
+
def tcptsseq(seq)
|
983
|
+
return nil if seq.nil?
|
984
|
+
|
985
|
+
@tcptssequence_class = seq.attributes['class']
|
986
|
+
@tcptssequence_values = seq.attributes['values']
|
987
|
+
end
|
988
|
+
|
989
|
+
def uptime(time)
|
990
|
+
return nil if time.nil?
|
991
|
+
|
992
|
+
@uptime_seconds = time.attributes['seconds'].to_i
|
993
|
+
@uptime_lastboot = time.attributes['lastboot']
|
994
|
+
end
|
995
|
+
|
996
|
+
def parse(host)
|
997
|
+
status = host.elements['status']
|
998
|
+
|
999
|
+
@status = status.attributes['state']
|
1000
|
+
|
1001
|
+
@reason = status.attributes['reason']
|
1002
|
+
|
1003
|
+
@os = OS.new(host.elements['os'])
|
1004
|
+
|
1005
|
+
host.each_element('address') do |elem|
|
1006
|
+
parseAddr(elem)
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
parseHostnames(host.elements['hostnames'])
|
1010
|
+
|
1011
|
+
smurf = host.elements['smurf']
|
1012
|
+
@smurf = smurf.attributes['responses'] if smurf
|
1013
|
+
|
1014
|
+
ports = host.elements['ports']
|
1015
|
+
|
1016
|
+
parsePorts(ports)
|
1017
|
+
|
1018
|
+
parseExtraPorts(ports)
|
1019
|
+
|
1020
|
+
parseScripts(host.elements['hostscript'])
|
1021
|
+
|
1022
|
+
if trace = host.elements['trace']
|
1023
|
+
@traceroute = Traceroute.new(trace)
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
tcpseq(host.elements['tcpsequence'])
|
1027
|
+
|
1028
|
+
ipidseq(host.elements['ipidsequence'])
|
1029
|
+
|
1030
|
+
tcptsseq(host.elements['tcptssequence'])
|
1031
|
+
|
1032
|
+
uptime(host.elements['uptime'])
|
1033
|
+
|
1034
|
+
distance = host.elements['distance']
|
1035
|
+
@distance = distance.attributes['value'].to_i if distance
|
1036
|
+
|
1037
|
+
@times = Times.new(host.elements['times'])
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
# This holds information on the time statistics for this host
|
1042
|
+
class Nmap::Parser::Host::Times
|
1043
|
+
# Smoothed round-trip time
|
1044
|
+
attr_reader :srtt
|
1045
|
+
# Round-trip time variance / deviation
|
1046
|
+
attr_reader :rttvar
|
1047
|
+
# How long before giving up on a probe (timeout)
|
1048
|
+
attr_reader :to
|
1049
|
+
|
1050
|
+
private
|
1051
|
+
|
1052
|
+
def initialize(times)
|
1053
|
+
parse(times)
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def parse(times)
|
1057
|
+
return nil if times.nil?
|
1058
|
+
|
1059
|
+
@srtt = times.attributes['srtt'].to_i
|
1060
|
+
@rttvar = times.attributes['rttvar'].to_i
|
1061
|
+
@to = times.attributes['to'].to_i
|
1062
|
+
end
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
# This holds the information about an NSE script run against a host or port
|
1066
|
+
class Nmap::Parser::Host::Script
|
1067
|
+
# NSE Script name
|
1068
|
+
attr_reader :id
|
1069
|
+
# NSE Script output
|
1070
|
+
attr_reader :output
|
1071
|
+
|
1072
|
+
alias name id
|
1073
|
+
|
1074
|
+
private
|
1075
|
+
|
1076
|
+
def initialize(script)
|
1077
|
+
parse(script)
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
def parse(script)
|
1081
|
+
return nil if script.nil?
|
1082
|
+
|
1083
|
+
@id = script.attributes['id']
|
1084
|
+
@output = script.attributes['output']
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
# This holds the information about an individual port or protocol
|
1089
|
+
class Nmap::Parser::Host::Port
|
1090
|
+
# Port number
|
1091
|
+
attr_reader :num
|
1092
|
+
# ::Service object for this port
|
1093
|
+
attr_reader :service
|
1094
|
+
# Port state ("open", "closed", "filtered", etc)
|
1095
|
+
attr_reader :state
|
1096
|
+
# Why the port is in the state
|
1097
|
+
attr_reader :reason
|
1098
|
+
# The host that responded, if different than the target
|
1099
|
+
attr_reader :reason_ip
|
1100
|
+
# TTL from the responding host
|
1101
|
+
attr_reader :reason_ttl
|
1102
|
+
|
1103
|
+
# Returns the Script object with the specified +name+
|
1104
|
+
def script(name)
|
1105
|
+
@scripts.find do |script|
|
1106
|
+
script.id == name
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
|
1110
|
+
# Returns an array of Script objects associated with this port, and
|
1111
|
+
# passes them each to a block if one is given
|
1112
|
+
def scripts
|
1113
|
+
if block_given?
|
1114
|
+
@scripts.each do |script|
|
1115
|
+
yield script
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
@scripts
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
# Returns the output of the script +name+
|
1123
|
+
def script_output(name)
|
1124
|
+
@scripts.each do |script|
|
1125
|
+
return script.output if script.id == name
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
nil
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# Compares port numbers
|
1132
|
+
def <=>(port)
|
1133
|
+
@num <=> port.num
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
private
|
1137
|
+
|
1138
|
+
def initialize(portinfo)
|
1139
|
+
parse(portinfo)
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
def parse(port)
|
1143
|
+
@num = port.attributes['portid'].to_i
|
1144
|
+
|
1145
|
+
state = port.elements['state']
|
1146
|
+
@state = state.attributes['state']
|
1147
|
+
@reason = state.attributes['reason']
|
1148
|
+
@reason_ttl = state.attributes['reason_ttl'].to_i
|
1149
|
+
@reason_ip = state.attributes['reason_ip']
|
1150
|
+
|
1151
|
+
@service = Service.new(port)
|
1152
|
+
|
1153
|
+
@scripts = []
|
1154
|
+
|
1155
|
+
port.each_element('script') do |script|
|
1156
|
+
@scripts << Nmap::Parser::Host::Script.new(script)
|
1157
|
+
end
|
1158
|
+
end
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
# This holds the information about "extra ports": groups of ports which have
|
1162
|
+
# the same state.
|
1163
|
+
class Nmap::Parser::Host::ExtraPorts
|
1164
|
+
# Total number of ports in this state
|
1165
|
+
attr_reader :count
|
1166
|
+
# What state the ports are in
|
1167
|
+
attr_reader :state
|
1168
|
+
|
1169
|
+
# Returns an array of arrays, each of which are in the form of:
|
1170
|
+
#
|
1171
|
+
# [ <port count>, reason ]
|
1172
|
+
#
|
1173
|
+
# for each set of reasons, and passes them each to a block if one is
|
1174
|
+
# given
|
1175
|
+
def reasons
|
1176
|
+
if block_given?
|
1177
|
+
@reasons.each do |reason|
|
1178
|
+
yield reason
|
1179
|
+
end
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
@reasons
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
private
|
1186
|
+
|
1187
|
+
def initialize(extraports)
|
1188
|
+
parse(extraports)
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
def parse(extraports)
|
1192
|
+
@count = extraports.attributes['count'].to_i
|
1193
|
+
@state = extraports.attributes['state']
|
1194
|
+
|
1195
|
+
@reasons = []
|
1196
|
+
|
1197
|
+
extraports.each_element('extrareasons') do |extra|
|
1198
|
+
ecount = extra.attributes['count'].to_i
|
1199
|
+
ereason = extra.attributes['reason']
|
1200
|
+
@reasons << [ ecount, ereason ]
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
# This holds information on a traceroute, such as the port and protocol used
|
1206
|
+
# and an array of responsive hops
|
1207
|
+
class Nmap::Parser::Host::Traceroute
|
1208
|
+
# The port used during traceroute
|
1209
|
+
attr_reader :port
|
1210
|
+
# The protocol used during traceroute
|
1211
|
+
attr_reader :proto
|
1212
|
+
|
1213
|
+
# Returns the Hop object for the given TTL
|
1214
|
+
def hop(ttl)
|
1215
|
+
@hops.find do |h|
|
1216
|
+
h.ttl == ttl.to_i
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
# Returns an array of Hop objects, which are each a responsive hop,
|
1221
|
+
# and passes them each to a block if one if given.
|
1222
|
+
def hops
|
1223
|
+
if block_given?
|
1224
|
+
@hops.each do |h|
|
1225
|
+
yield h
|
1226
|
+
end
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
@hops
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
private
|
1233
|
+
|
1234
|
+
def initialize(trace)
|
1235
|
+
parse(trace)
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
def parse(trace)
|
1239
|
+
@port = trace.attributes['port'].to_i
|
1240
|
+
@proto = trace.attributes['proto']
|
1241
|
+
|
1242
|
+
@hops = []
|
1243
|
+
|
1244
|
+
trace.each_element('hop') do |hop|
|
1245
|
+
@hops << Hop.new(hop)
|
1246
|
+
end
|
1247
|
+
end
|
1248
|
+
end
|
1249
|
+
|
1250
|
+
# This holds information on an individual traceroute hop
|
1251
|
+
class Nmap::Parser::Host::Traceroute::Hop
|
1252
|
+
# How many hops away the host is
|
1253
|
+
attr_reader :ttl
|
1254
|
+
# The round-trip time of the host
|
1255
|
+
attr_reader :rtt
|
1256
|
+
# The IP address of the host
|
1257
|
+
attr_reader :addr
|
1258
|
+
# The hostname of the host
|
1259
|
+
attr_reader :hostname
|
1260
|
+
|
1261
|
+
alias host hostname
|
1262
|
+
alias ipaddr addr
|
1263
|
+
|
1264
|
+
private
|
1265
|
+
|
1266
|
+
def initialize(hop)
|
1267
|
+
parse(hop)
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
def parse(hop)
|
1271
|
+
@ttl = hop.attributes['ttl'].to_i
|
1272
|
+
@rtt = hop.attributes['rtt'].to_f
|
1273
|
+
@addr = hop.attributes['ipaddr']
|
1274
|
+
@hostname = hop.attributes['host']
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
# This holds the service information for a port
|
1279
|
+
class Nmap::Parser::Host::Port::Service
|
1280
|
+
# The name of the service
|
1281
|
+
attr_reader :name
|
1282
|
+
# Vendor name
|
1283
|
+
attr_reader :product
|
1284
|
+
# Version number
|
1285
|
+
attr_reader :version
|
1286
|
+
# How this information was obtained, such as "table" or "probed"
|
1287
|
+
attr_reader :method
|
1288
|
+
# Service owner
|
1289
|
+
attr_reader :owner
|
1290
|
+
# Any tunnelling used, like "ssl"
|
1291
|
+
attr_reader :tunnel
|
1292
|
+
# RPC program number
|
1293
|
+
attr_reader :rpcnum
|
1294
|
+
# Range of RPC version numbers
|
1295
|
+
attr_reader :lowver, :highver
|
1296
|
+
# How confident the version detection is
|
1297
|
+
attr_reader :confidence
|
1298
|
+
# Protocol, such as "rpc"
|
1299
|
+
attr_reader :proto
|
1300
|
+
# Extra misc. information about the service
|
1301
|
+
attr_reader :extra
|
1302
|
+
# The type of device the service is running on
|
1303
|
+
attr_reader :devicetype
|
1304
|
+
# The OS the service is running on
|
1305
|
+
attr_reader :ostype
|
1306
|
+
# The service fingerprint
|
1307
|
+
attr_reader :fingerprint
|
1308
|
+
|
1309
|
+
alias extrainfo extra
|
1310
|
+
|
1311
|
+
private
|
1312
|
+
|
1313
|
+
def initialize(port)
|
1314
|
+
parse(port)
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
def parse(port)
|
1318
|
+
return nil if port.nil?
|
1319
|
+
|
1320
|
+
service = port.elements['service']
|
1321
|
+
|
1322
|
+
return nil if service.nil?
|
1323
|
+
|
1324
|
+
@name = service.attributes['name']
|
1325
|
+
@product = service.attributes['product']
|
1326
|
+
@version = service.attributes['version']
|
1327
|
+
@method = service.attributes['method']
|
1328
|
+
owner = port.elements['owner']
|
1329
|
+
@owner = owner.attributes['name'] if owner
|
1330
|
+
@tunnel = service.attributes['tunnel']
|
1331
|
+
rpcnum = service.attributes['rpcnum']
|
1332
|
+
@rpcnum = rpcnum.to_i if rpcnum
|
1333
|
+
lowver = service.attributes['lowver']
|
1334
|
+
@lowver = lowver.to_i if lowver
|
1335
|
+
highver = service.attributes['highver']
|
1336
|
+
@highver = highver.to_i if highver
|
1337
|
+
conf = service.attributes['conf']
|
1338
|
+
@confidence = conf.to_i if conf
|
1339
|
+
@proto = service.attributes['proto']
|
1340
|
+
@extra = service.attributes['extrainfo']
|
1341
|
+
@devicetype = service.attributes['devicetype']
|
1342
|
+
@ostype = service.attributes['ostype']
|
1343
|
+
@fingerprint = service.attributes['servicefp']
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
# This holds the OS information from OS Detection
|
1348
|
+
class Nmap::Parser::Host::OS
|
1349
|
+
# OS fingerprint
|
1350
|
+
attr_reader :fingerprint
|
1351
|
+
|
1352
|
+
# Returns an array of OSClass objects, and passes them each to a
|
1353
|
+
# block if one is given
|
1354
|
+
def osclasses
|
1355
|
+
if block_given?
|
1356
|
+
@osclasses.each do |osclass|
|
1357
|
+
yield osclass
|
1358
|
+
end
|
1359
|
+
end
|
1360
|
+
|
1361
|
+
@osclasses
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
# Returns an array of OSMatch objects, and passes them each to a
|
1365
|
+
# block if one is given
|
1366
|
+
def osmatches
|
1367
|
+
if block_given?
|
1368
|
+
@osmatches.each do |osmatch|
|
1369
|
+
yield osmatch
|
1370
|
+
end
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
@osmatches
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
# Returns the number of OS class records
|
1377
|
+
def class_count
|
1378
|
+
@osclasses.size
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
# Returns OS class accuracy of the first OS class record, or Nth record
|
1382
|
+
# as specified by +index+
|
1383
|
+
def class_accuracy(index = 0)
|
1384
|
+
return nil if @osclasses.empty?
|
1385
|
+
|
1386
|
+
@osclasses[index.to_i].accuracy
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
# Returns OS family information of first OS class record, or Nth record
|
1390
|
+
# as specified by +index+
|
1391
|
+
def osfamily(index = 0)
|
1392
|
+
return nil if @osclasses.empty?
|
1393
|
+
|
1394
|
+
@osclasses[index.to_i].osfamily
|
1395
|
+
end
|
1396
|
+
|
1397
|
+
# Returns OS generation information of first OS class record, or Nth
|
1398
|
+
# record as specified by +index+
|
1399
|
+
def osgen(index = 0)
|
1400
|
+
return nil if @osclasses.empty?
|
1401
|
+
|
1402
|
+
@osclasses[index.to_i].osgen
|
1403
|
+
end
|
1404
|
+
|
1405
|
+
# Returns OS type information of the first OS class record, or Nth
|
1406
|
+
# record as specified by +index+
|
1407
|
+
def ostype(index = 0)
|
1408
|
+
return nil if @osclasses.empty?
|
1409
|
+
|
1410
|
+
@osclasses[index.to_i].ostype
|
1411
|
+
end
|
1412
|
+
|
1413
|
+
# Returns OS vendor information of the first OS class record, or Nth
|
1414
|
+
# record as specified by +index+
|
1415
|
+
def osvendor(index = 0)
|
1416
|
+
return nil if @osclasses.empty?
|
1417
|
+
|
1418
|
+
@osclasses[index.to_i].osvendor
|
1419
|
+
end
|
1420
|
+
|
1421
|
+
# Returns the number of OS match records
|
1422
|
+
def name_count
|
1423
|
+
@osmatches.size
|
1424
|
+
end
|
1425
|
+
|
1426
|
+
# Returns name of first OS match record, or Nth record as specified by
|
1427
|
+
# +index+
|
1428
|
+
def name(index = 0)
|
1429
|
+
return nil if @osmatches.empty?
|
1430
|
+
|
1431
|
+
@osmatches[index.to_i].name
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
# Returns OS name accuracy of the first OS match record, or Nth record
|
1435
|
+
# as specified by +index+
|
1436
|
+
def name_accuracy(index = 0)
|
1437
|
+
return nil if @osmatches.empty?
|
1438
|
+
|
1439
|
+
@osmatches[index.to_i].accuracy
|
1440
|
+
end
|
1441
|
+
|
1442
|
+
# Returns an array of names from all OS records, and passes them each
|
1443
|
+
# to a block if one is given
|
1444
|
+
def all_names() # :yields: name
|
1445
|
+
names = []
|
1446
|
+
|
1447
|
+
@osmatches.each do |match|
|
1448
|
+
names << match.name
|
1449
|
+
yield match.name if block_given?
|
1450
|
+
end
|
1451
|
+
|
1452
|
+
names
|
1453
|
+
end
|
1454
|
+
|
1455
|
+
alias names all_names
|
1456
|
+
|
1457
|
+
# Returns the closed TCP port used for this OS Detection run
|
1458
|
+
def tcpport_closed
|
1459
|
+
return nil if @portsused.nil?
|
1460
|
+
|
1461
|
+
@portsused.each do |port|
|
1462
|
+
if port.proto == "tcp" and port.state == "closed"
|
1463
|
+
return port.num
|
1464
|
+
end
|
1465
|
+
end
|
1466
|
+
|
1467
|
+
nil
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
# Returns the open TCP port used for this OS Detection run
|
1471
|
+
def tcpport_open
|
1472
|
+
return nil if @portsused.nil?
|
1473
|
+
|
1474
|
+
@portsused.each do |port|
|
1475
|
+
if port.proto == "tcp" and port.state == "open"
|
1476
|
+
return port.num
|
1477
|
+
end
|
1478
|
+
end
|
1479
|
+
|
1480
|
+
nil
|
1481
|
+
end
|
1482
|
+
|
1483
|
+
# Returns the closed UDP port used for this OS Detection run
|
1484
|
+
def udpport_closed
|
1485
|
+
return nil if @portsused.nil?
|
1486
|
+
|
1487
|
+
@portsused.each do |port|
|
1488
|
+
if port.proto == "udp" and port.state == "closed"
|
1489
|
+
return port.num
|
1490
|
+
end
|
1491
|
+
end
|
1492
|
+
|
1493
|
+
nil
|
1494
|
+
end
|
1495
|
+
|
1496
|
+
private
|
1497
|
+
|
1498
|
+
def initialize(os)
|
1499
|
+
parse(os)
|
1500
|
+
end
|
1501
|
+
|
1502
|
+
def parse(os)
|
1503
|
+
@portsused = []
|
1504
|
+
@osclasses = []
|
1505
|
+
@osmatches = []
|
1506
|
+
|
1507
|
+
return nil if os.nil?
|
1508
|
+
|
1509
|
+
os.each_element('portused') do |port|
|
1510
|
+
@portsused << PortUsed.new(port)
|
1511
|
+
end
|
1512
|
+
|
1513
|
+
os.each_element('osclass') do |osclass|
|
1514
|
+
@osclasses << OSClass.new(osclass)
|
1515
|
+
end
|
1516
|
+
|
1517
|
+
@osclasses.sort!.reverse!
|
1518
|
+
|
1519
|
+
os.each_element('osmatch') do |match|
|
1520
|
+
@osmatches << OSMatch.new(match)
|
1521
|
+
end
|
1522
|
+
|
1523
|
+
@osmatches.sort!.reverse!
|
1524
|
+
|
1525
|
+
fp = os.elements['osfingerprint']
|
1526
|
+
@fingerprint = fp.attributes['fingerprint'] if fp
|
1527
|
+
end
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
class Nmap::Parser::Host::OS::PortUsed # :nodoc: all
|
1531
|
+
attr_reader :state, :proto, :num
|
1532
|
+
|
1533
|
+
private
|
1534
|
+
|
1535
|
+
def initialize(ports)
|
1536
|
+
parse(ports)
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
def parse(ports)
|
1540
|
+
@state = ports.attributes['state']
|
1541
|
+
@proto = ports.attributes['proto']
|
1542
|
+
@num = ports.attributes['portid'].to_i
|
1543
|
+
end
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
# Holds information for an individual OS class record
|
1547
|
+
class Nmap::Parser::Host::OS::OSClass
|
1548
|
+
# Device type, like "router" or "general purpose"
|
1549
|
+
attr_reader :ostype
|
1550
|
+
# Company that makes the OS, like "Apple" or "Microsoft"
|
1551
|
+
attr_reader :osvendor
|
1552
|
+
# Product name, like "Linux" or "Windows"
|
1553
|
+
attr_reader :osfamily
|
1554
|
+
# A more precise description, like "2.6.X" for Linux
|
1555
|
+
attr_reader :osgen
|
1556
|
+
# Accuracy of this information
|
1557
|
+
attr_reader :accuracy
|
1558
|
+
|
1559
|
+
# Compares accuracy
|
1560
|
+
def <=>(osclass)
|
1561
|
+
@accuracy <=> osclass.accuracy
|
1562
|
+
end
|
1563
|
+
|
1564
|
+
private
|
1565
|
+
|
1566
|
+
def initialize(osclass)
|
1567
|
+
parse(osclass)
|
1568
|
+
end
|
1569
|
+
|
1570
|
+
def parse(osclass)
|
1571
|
+
@ostype = osclass.attributes['type']
|
1572
|
+
@osvendor = osclass.attributes['vendor']
|
1573
|
+
@osfamily = osclass.attributes['osfamily']
|
1574
|
+
@osgen = osclass.attributes['osgen']
|
1575
|
+
@accuracy = osclass.attributes['accuracy'].to_i
|
1576
|
+
end
|
1577
|
+
end
|
1578
|
+
|
1579
|
+
# Holds information for an individual OS match record
|
1580
|
+
class Nmap::Parser::Host::OS::OSMatch
|
1581
|
+
# Operating System name
|
1582
|
+
attr_reader :name
|
1583
|
+
# Accuracy of this match
|
1584
|
+
attr_reader :accuracy
|
1585
|
+
|
1586
|
+
# Compares accuracy
|
1587
|
+
def <=>(osmatch)
|
1588
|
+
@accuracy <=> osmatch.accuracy
|
1589
|
+
end
|
1590
|
+
|
1591
|
+
private
|
1592
|
+
|
1593
|
+
def initialize(os)
|
1594
|
+
parse(os)
|
1595
|
+
end
|
1596
|
+
|
1597
|
+
def parse(os)
|
1598
|
+
@name = os.attributes['name']
|
1599
|
+
@accuracy = os.attributes['accuracy'].to_i
|
1600
|
+
end
|
1601
|
+
end
|
1602
|
+
|