ruby-nmap 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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