ruby-nmap 0.6.0 → 0.7.0

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +13 -0
  4. data/ChangeLog.md +24 -0
  5. data/Gemfile +12 -0
  6. data/LICENSE.txt +1 -1
  7. data/README.md +11 -9
  8. data/Rakefile +20 -24
  9. data/gemspec.yml +1 -3
  10. data/lib/nmap/cpe.rb +2 -0
  11. data/lib/nmap/cpe/cpe.rb +45 -0
  12. data/lib/nmap/cpe/url.rb +78 -0
  13. data/lib/nmap/hop.rb +20 -0
  14. data/lib/nmap/host.rb +69 -15
  15. data/lib/nmap/hostname.rb +20 -0
  16. data/lib/nmap/os.rb +2 -17
  17. data/lib/nmap/os_class.rb +65 -1
  18. data/lib/nmap/port.rb +10 -0
  19. data/lib/nmap/run_stat.rb +20 -0
  20. data/lib/nmap/scan_task.rb +4 -19
  21. data/lib/nmap/sequence.rb +6 -18
  22. data/lib/nmap/service.rb +76 -0
  23. data/lib/nmap/tcp_sequence.rb +1 -1
  24. data/lib/nmap/traceroute.rb +67 -0
  25. data/lib/nmap/uptime.rb +20 -0
  26. data/lib/nmap/version.rb +1 -1
  27. data/lib/nmap/xml.rb +134 -17
  28. data/spec/address_spec.rb +14 -0
  29. data/spec/cpe/url_spec.rb +99 -0
  30. data/spec/cpe_examples.rb +11 -0
  31. data/spec/hop_spec.rb +14 -0
  32. data/spec/host_spec.rb +138 -55
  33. data/spec/hostname_spec.rb +15 -0
  34. data/spec/ip_id_sequence_spec.rb +24 -10
  35. data/spec/os_class_spec.rb +31 -0
  36. data/spec/os_match_spec.rb +15 -0
  37. data/spec/os_spec.rb +23 -20
  38. data/spec/port_spec.rb +14 -15
  39. data/spec/run_stat_spec.rb +21 -0
  40. data/spec/scan.xml +137 -0
  41. data/spec/scan_spec.rb +28 -0
  42. data/spec/scan_task_spec.rb +35 -0
  43. data/spec/scanner_spec.rb +24 -0
  44. data/spec/scripts_examples.rb +10 -0
  45. data/spec/sequence_examples.rb +10 -0
  46. data/spec/service_spec.rb +55 -18
  47. data/spec/spec_helper.rb +30 -3
  48. data/spec/status_spec.rb +15 -0
  49. data/spec/tcp_sequence_spec.rb +32 -19
  50. data/spec/tcp_ts_sequence_spec.rb +24 -10
  51. data/spec/traceroute_spec.rb +31 -0
  52. data/spec/uptime_spec.rb +15 -0
  53. data/spec/xml_spec.rb +172 -31
  54. metadata +50 -54
  55. data/.gemtest +0 -0
  56. data/spec/helpers/nse.xml +0 -94
  57. data/spec/helpers/scan.xml +0 -241
  58. data/spec/helpers/xml.rb +0 -5
@@ -0,0 +1,20 @@
1
+ module Nmap
2
+ #
3
+ # Wraps a `uptime` XML element.
4
+ #
5
+ # @since 0.7.0
6
+ #
7
+ class Uptime < Struct.new(:seconds, :last_boot)
8
+
9
+ #
10
+ # Converts the uptime object to a String.
11
+ #
12
+ # @return [String]
13
+ # The String form of the Uptime.
14
+ #
15
+ def to_s
16
+ "uptime: #{self.seconds} (#{self.last_boot})"
17
+ end
18
+
19
+ end
20
+ end
@@ -1,4 +1,4 @@
1
1
  module Nmap
2
2
  # ruby-nmap version
3
- VERSION = '0.6.0'
3
+ VERSION = '0.7.0'
4
4
  end
@@ -2,6 +2,7 @@ require 'nmap/scanner'
2
2
  require 'nmap/scan_task'
3
3
  require 'nmap/scan'
4
4
  require 'nmap/host'
5
+ require 'nmap/run_stat'
5
6
 
6
7
  require 'nokogiri'
7
8
 
@@ -19,8 +20,8 @@ module Nmap
19
20
  #
20
21
  # Creates a new XML object.
21
22
  #
22
- # @param [String] path
23
- # The path to the Nmap XML scan file.
23
+ # @param [Nokogiri::XML::Document, IO, String] document
24
+ # The path to the Nmap XML scan file or Nokogiri::XML::Document.
24
25
  #
25
26
  # @yield [xml]
26
27
  # If a block is given, it will be passed the new XML object.
@@ -28,13 +29,56 @@ module Nmap
28
29
  # @yieldparam [XML] xml
29
30
  # The newly created XML object.
30
31
  #
31
- def initialize(path)
32
- @path = File.expand_path(path)
33
- @doc = Nokogiri::XML(open(@path))
32
+ def initialize(document)
33
+ case document
34
+ when Nokogiri::XML::Document
35
+ @doc = document
36
+ when IO, StringIO
37
+ @doc = Nokogiri::XML(document)
38
+ else
39
+ @path = File.expand_path(document)
40
+ @doc = Nokogiri::XML(open(@path))
41
+ end
34
42
 
35
43
  yield self if block_given?
36
44
  end
37
45
 
46
+ #
47
+ # Creates a new XML object from XML text.
48
+ #
49
+ # @param [String] text
50
+ # XML text of the scan file
51
+ #
52
+ # @yield [xml]
53
+ # If a block is given, it will be passed the new XML object.
54
+ #
55
+ # @yieldparam [XML] xml
56
+ # The newly created XML object.
57
+ #
58
+ # @since 0.7.0
59
+ #
60
+ def self.load(text,&block)
61
+ new(Nokogiri::XML(text), &block)
62
+ end
63
+
64
+ #
65
+ # Creates a new XML object from the file.
66
+ #
67
+ # @param [String] path
68
+ # The path to the XML file.
69
+ #
70
+ # @yield [xml]
71
+ # If a block is given, it will be passed the new XML object.
72
+ #
73
+ # @yieldparam [XML] xml
74
+ # The newly created XML object.
75
+ #
76
+ # @since 0.7.0
77
+ #
78
+ def self.open(path,&block)
79
+ new(path,&block)
80
+ end
81
+
38
82
  #
39
83
  # Parses the scanner information.
40
84
  #
@@ -82,6 +126,47 @@ module Nmap
82
126
  end
83
127
  end
84
128
 
129
+ #
130
+ # Parses the essential runstats information.
131
+ #
132
+ # @yield [run_stat]
133
+ # The given block will be passed each runstat.
134
+ #
135
+ # @yieldparam [RunStat] run_stat
136
+ # A runstat.
137
+ #
138
+ # @return [Enumerator]
139
+ # If no block is given, an enumerator will be returned.
140
+ #
141
+ # @since 0.7.0
142
+ #
143
+ def each_run_stat
144
+ return enum_for(__method__) unless block_given?
145
+
146
+ @doc.xpath('/nmaprun/runstats/finished').each do |run_stat|
147
+ yield RunStat.new(
148
+ Time.at(run_stat['time'].to_i),
149
+ run_stat['elapsed'],
150
+ run_stat['summary'],
151
+ run_stat['exit']
152
+ )
153
+ end
154
+
155
+ return self
156
+ end
157
+
158
+ #
159
+ # Parses the essential runstats information.
160
+ #
161
+ # @return [Array<RunStat>]
162
+ # The runstats.
163
+ #
164
+ # @since 0.7.0
165
+ #
166
+ def run_stats
167
+ each_run_stat.to_a
168
+ end
169
+
85
170
  #
86
171
  # Parses the verbose level.
87
172
  #
@@ -105,22 +190,44 @@ module Nmap
105
190
  #
106
191
  # Parses the tasks of the scan.
107
192
  #
108
- # @return [Array<ScanTask>]
109
- # The tasks of the scan.
193
+ # @yield [task]
194
+ # The given block will be passed each scan task.
110
195
  #
111
- # @since 0.1.2
196
+ # @yieldparam [ScanTask] task
197
+ # A task from the scan.
112
198
  #
113
- def tasks
114
- @doc.xpath('/nmaprun/taskbegin').map do |task_begin|
199
+ # @return [Enumerator]
200
+ # If no block is given, an enumerator will be returned.
201
+ #
202
+ # @since 0.7.0
203
+ #
204
+ def each_task
205
+ return enum_for(__method__) unless block_given?
206
+
207
+ @doc.xpath('/nmaprun/taskbegin').each do |task_begin|
115
208
  task_end = task_begin.xpath('following-sibling::taskend').first
116
209
 
117
- ScanTask.new(
210
+ yield ScanTask.new(
118
211
  task_begin['task'],
119
212
  Time.at(task_begin['time'].to_i),
120
213
  Time.at(task_end['time'].to_i),
121
214
  task_end['extrainfo']
122
215
  )
123
216
  end
217
+
218
+ return self
219
+ end
220
+
221
+ #
222
+ # Parses the tasks of the scan.
223
+ #
224
+ # @return [Array<ScanTask>]
225
+ # The tasks of the scan.
226
+ #
227
+ # @since 0.1.2
228
+ #
229
+ def tasks
230
+ each_task.to_a
124
231
  end
125
232
 
126
233
  #
@@ -136,11 +243,11 @@ module Nmap
136
243
  # The XML object. If no block was given, an enumerator object will
137
244
  # be returned.
138
245
  #
139
- def each_host(&block)
140
- return enum_for(__method__) unless block
246
+ def each_host
247
+ return enum_for(__method__) unless block_given?
141
248
 
142
249
  @doc.xpath('/nmaprun/host').each do |host|
143
- Host.new(host,&block)
250
+ yield Host.new(host)
144
251
  end
145
252
 
146
253
  return self
@@ -169,11 +276,11 @@ module Nmap
169
276
  # The XML parser. If no block was given, an enumerator object will
170
277
  # be returned.
171
278
  #
172
- def each_up_host(&block)
173
- return enum_for(__method__) unless block
279
+ def each_up_host
280
+ return enum_for(__method__) unless block_given?
174
281
 
175
282
  @doc.xpath("/nmaprun/host[status[@state='up']]").each do |host|
176
- Host.new(host,&block)
283
+ yield Host.new(host)
177
284
  end
178
285
 
179
286
  return self
@@ -208,5 +315,15 @@ module Nmap
208
315
  @path.to_s
209
316
  end
210
317
 
318
+ #
319
+ # Inspects the XML file.
320
+ #
321
+ # @return [String]
322
+ # The inspected XML file.
323
+ #
324
+ def inspect
325
+ "#<#{self.class}: #{self}>"
326
+ end
327
+
211
328
  end
212
329
  end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'nmap/address'
3
+
4
+ describe Address do
5
+ describe "#to_s" do
6
+ let(:addr) { '127.0.0.1' }
7
+
8
+ subject { described_class.new(:ipv4, addr) }
9
+
10
+ it "should return the address" do
11
+ subject.to_s.should == addr
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe CPE::URL do
4
+ describe "parse" do
5
+ context "when the URL does not start with 'cpe:'" do
6
+ it "should raise an ArgumentError" do
7
+ lambda {
8
+ described_class.parse('foo:')
9
+ }.should raise_error(ArgumentError)
10
+ end
11
+ end
12
+
13
+ context "when blank fields are omitted" do
14
+ let(:vendor) { :linux }
15
+ let(:product) { :linux_kernel }
16
+ let(:version) { '2.6.39' }
17
+
18
+ subject do
19
+ described_class.parse("cpe:/o:#{vendor}:#{product}:#{version}")
20
+ end
21
+
22
+ it "should leave them nil" do
23
+ subject.vendor.should == vendor
24
+ subject.product.should == product
25
+ subject.version.should == version
26
+
27
+ subject.update.should be_nil
28
+ subject.edition.should be_nil
29
+ subject.language.should be_nil
30
+ end
31
+ end
32
+
33
+ context "when part is /h" do
34
+ subject { described_class.parse("cpe:/h:foo:bar:baz") }
35
+
36
+ it "should parse it as :hardware" do
37
+ subject.part.should == :hardware
38
+ end
39
+ end
40
+
41
+ context "when part is /a" do
42
+ subject { described_class.parse("cpe:/a:foo:bar:baz") }
43
+
44
+ it "should parse it as :application" do
45
+ subject.part.should == :application
46
+ end
47
+ end
48
+
49
+ context "when part is /o" do
50
+ subject { described_class.parse("cpe:/o:foo:bar:baz") }
51
+
52
+ it "should parse it as :os" do
53
+ subject.part.should == :os
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "#to_s" do
59
+ let(:vendor) { :linux }
60
+ let(:product) { :linux_kernel }
61
+ let(:version) { '2.6.39' }
62
+
63
+ it "should add the scheme 'cpe:'" do
64
+ subject.to_s.should start_with('cpe:')
65
+ end
66
+
67
+ context "when fields are nil" do
68
+ subject { described_class.new(:os,vendor,product,version) }
69
+
70
+ it "should omit them" do
71
+ subject.to_s.should == "cpe:/o:#{vendor}:#{product}:#{version}"
72
+ end
73
+ end
74
+
75
+ context "when part is :hardware" do
76
+ subject { described_class.new(:hardware) }
77
+
78
+ it "should map it to /h" do
79
+ subject.to_s.should == "cpe:/h"
80
+ end
81
+ end
82
+
83
+ context "when part is :application" do
84
+ subject { described_class.new(:application) }
85
+
86
+ it "should map it to /h" do
87
+ subject.to_s.should == "cpe:/a"
88
+ end
89
+ end
90
+
91
+ context "when part is :os" do
92
+ subject { described_class.new(:os) }
93
+
94
+ it "should map it to /h" do
95
+ subject.to_s.should == "cpe:/o"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,11 @@
1
+ require 'rspec'
2
+
3
+ shared_examples_for "CPE" do
4
+ subject { super().cpe }
5
+
6
+ it { should_not be_empty }
7
+
8
+ it "should contain CPE URLs" do
9
+ subject.should all_be_kind_of(CPE::URL)
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'nmap/hop'
3
+
4
+ describe Hop do
5
+ describe "#to_s" do
6
+ subject do
7
+ described_class.new('10.0.0.1', nil, '1', '3.38')
8
+ end
9
+
10
+ it "should return the addr" do
11
+ subject.to_s.should == subject.addr
12
+ end
13
+ end
14
+ end
@@ -1,12 +1,11 @@
1
1
  require 'spec_helper'
2
+ require 'scripts_examples'
2
3
 
3
4
  require 'nmap/xml'
4
5
  require 'nmap/host'
5
6
 
6
7
  describe Host do
7
- let(:xml) { XML.new(Helpers::SCAN_FILE) }
8
-
9
- subject { xml.hosts.first }
8
+ subject { @xml.hosts.first }
10
9
 
11
10
  it "should parse the start_time" do
12
11
  subject.start_time.should > Time.at(0)
@@ -17,91 +16,175 @@ describe Host do
17
16
  subject.end_time.should > subject.start_time
18
17
  end
19
18
 
20
- it "should parse the status" do
21
- status = subject.status
22
-
23
- status.state.should == :up
24
- status.reason.should == 'arp-response'
19
+ describe "#uptime" do
20
+ subject { super().uptime }
21
+
22
+ it "should return an Uptime object" do
23
+ subject.should be_kind_of(Uptime)
24
+ end
25
+
26
+ it "should parse the seconds attribute" do
27
+ subject.seconds.should be > 0
28
+ end
29
+
30
+ it "should parse the lastboot attribute" do
31
+ subject.last_boot.should be_kind_of(Time)
32
+ end
25
33
  end
26
34
 
27
- it "should parse the addresses" do
28
- addresses = subject.addresses
29
-
30
- addresses.length.should == 2
35
+ describe "#tcp_sequence" do
36
+ subject { super().tcp_sequence }
31
37
 
32
- addresses[0].type.should == :ipv4
33
- addresses[0].addr.should == '192.168.5.1'
38
+ it { should be_kind_of(TcpSequence) }
39
+ end
40
+
41
+ describe "#ip_ip_sequence" do
42
+ subject { super().ip_id_sequence }
34
43
 
35
- addresses[1].type.should == :mac
36
- addresses[1].addr.should == '00:1D:7E:EF:2A:E5'
44
+ it { should be_kind_of(IpIdSequence) }
37
45
  end
38
46
 
39
- it "should parse the MAC address" do
40
- subject.mac.should == '00:1D:7E:EF:2A:E5'
47
+ describe "#tcp_ts_sequence" do
48
+ subject { super().tcp_ts_sequence }
49
+
50
+ it { should be_kind_of(TcpTsSequence) }
41
51
  end
42
52
 
43
- it "should parse the IPv4 address" do
44
- subject.ipv4.should == '192.168.5.1'
53
+ describe "#status" do
54
+ subject { super().status }
55
+
56
+ it "should parse the state" do
57
+ subject.state.should be_one_of(:up, :down)
58
+ end
59
+
60
+ it "should parse the reason" do
61
+ subject.reason.should be_one_of(
62
+ 'syn-ack',
63
+ 'timestamp-reply',
64
+ 'echo-reply',
65
+ 'reset'
66
+ )
67
+ end
68
+ end
69
+
70
+ describe "#addresses" do
71
+ subject { super().addresses.first }
72
+
73
+ it "should parse the type" do
74
+ subject.type.should == :ipv4
75
+ end
76
+
77
+ it "should parser the addr" do
78
+ subject.addr.should == '74.207.244.221'
79
+ end
45
80
  end
46
81
 
47
- it "should parse the IPv6 address" do
48
- pending "generate a Nmap XML scan file including IPv6 addresses"
82
+ describe "#mac" do
83
+ it "should parse the first MAC address" do
84
+ pending "need a host with address[@addrtype='mac']"
85
+ end
49
86
  end
50
87
 
51
- it "should have an IP" do
52
- subject.ip.should == '192.168.5.1'
88
+ describe "#ipv6" do
89
+ it "should parse the first IPv6 address" do
90
+ pending "need a host with address[@addrtype='ipv6']"
91
+ end
53
92
  end
54
93
 
55
- it "should have an address" do
56
- subject.address.should == '192.168.5.1'
94
+ describe "#ipv4" do
95
+ it "should parse the IPv4 address" do
96
+ subject.ipv4.should == '74.207.244.221'
97
+ end
57
98
  end
58
99
 
59
- it "should parse the hostnames" do
60
- pending "generate a Nmap XML scan file including hostnames"
100
+ describe "#ip" do
101
+ it "should have an IP" do
102
+ subject.ip.should == '74.207.244.221'
103
+ end
61
104
  end
62
105
 
63
- it "should parse the OS guessing information" do
64
- subject.os.should_not be_nil
106
+ describe "#address" do
107
+ it "should have an address" do
108
+ subject.address.should == '74.207.244.221'
109
+ end
65
110
  end
66
111
 
67
- it "should parse the ports" do
68
- ports = subject.ports
69
-
70
- ports.length.should == 3
112
+ describe "#hostnames" do
113
+ subject { super().hostnames }
114
+
115
+ it { should_not be_empty }
116
+
117
+ it "should parse the type" do
118
+ subject.all? { |hostname| hostname.type }.should be_true
119
+ end
120
+
121
+ it "should parse the name" do
122
+ subject.all? { |hostname| hostname.name }.should be_true
123
+ end
124
+
125
+ it "should include a user hostname" do
126
+ subject.any? { |hostname| hostname.type == 'user' }.should be_true
127
+ end
128
+
129
+ it "should include a PTR hostname" do
130
+ subject.any? { |hostname| hostname.type == 'PTR' }.should be_true
131
+ end
71
132
  end
72
133
 
73
- it "should list the open ports" do
74
- ports = subject.open_ports
75
-
76
- ports.length.should == 1
77
- ports.all? { |port| port.state == :open }.should == true
134
+ describe "#os" do
135
+ subject { super().os }
136
+
137
+ it { should_not be_nil }
138
+ it { should be_kind_of(OS) }
78
139
  end
79
140
 
80
- it "should list TCP ports" do
81
- ports = subject.tcp_ports
82
-
83
- ports.length.should == 3
84
- ports.all? { |port| port.protocol == :tcp }.should == true
141
+ describe "#ports" do
142
+ subject { super().ports }
143
+
144
+ it { should_not be_empty }
85
145
  end
86
146
 
87
- it "should list the UDP ports" do
88
- pending "generate a Nmap XML scan file including scanned UDP ports"
147
+ describe "#open_ports" do
148
+ subject { super().open_ports }
149
+
150
+ it { should_not be_empty }
151
+
152
+ it "should list the open ports" do
153
+ subject.all? { |port| port.state == :open }.should be_true
154
+ end
89
155
  end
90
156
 
91
- it "should convert to a String" do
92
- subject.to_s.should == '192.168.5.1'
157
+ describe "#tcp_ports" do
158
+ subject { super().tcp_ports }
159
+
160
+ it { should_not be_empty }
161
+
162
+ it "should contain TCP ports" do
163
+ subject.all? { |port| port.protocol == :tcp }.should be_true
164
+ end
93
165
  end
94
166
 
95
- context "when NSE scripts are ran" do
96
- let(:xml) { XML.new(Helpers::NSE_FILE) }
167
+ describe "#udp_ports" do
168
+ subject { super().udp_ports }
169
+
170
+ it { should_not be_empty }
97
171
 
98
- subject { xml.hosts.first }
172
+ it "should contain UDP ports" do
173
+ subject.all? { |port| port.protocol == :udp }.should be_true
174
+ end
175
+ end
99
176
 
100
- it "should list output of the scripts" do
101
- subject.scripts.should_not be_empty
177
+ it "should convert to a String" do
178
+ subject.to_s.should == '74.207.244.221'
179
+ end
102
180
 
103
- subject.scripts.keys.should_not include(nil)
104
- subject.scripts.values.should_not include(nil)
181
+ describe "#inspect" do
182
+ it "should include the address" do
183
+ subject.inspect.should include(subject.address)
105
184
  end
106
185
  end
186
+
187
+ pending "scan.xml does not currently include any hostscripts" do
188
+ include_examples "#scripts"
189
+ end
107
190
  end