ronin-nmap 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.github/workflows/ruby.yml +47 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +15 -0
  7. data/.ruby-version +1 -0
  8. data/.yardopts +1 -0
  9. data/COPYING.txt +165 -0
  10. data/ChangeLog.md +10 -0
  11. data/Gemfile +42 -0
  12. data/README.md +238 -0
  13. data/Rakefile +43 -0
  14. data/bin/ronin-nmap +32 -0
  15. data/data/completions/ronin-nmap +79 -0
  16. data/data/templates/script.rb.erb +58 -0
  17. data/gemspec.yml +42 -0
  18. data/lib/ronin/nmap/cli/command.rb +40 -0
  19. data/lib/ronin/nmap/cli/commands/completion.rb +61 -0
  20. data/lib/ronin/nmap/cli/commands/convert.rb +108 -0
  21. data/lib/ronin/nmap/cli/commands/dump.rb +293 -0
  22. data/lib/ronin/nmap/cli/commands/grep.rb +378 -0
  23. data/lib/ronin/nmap/cli/commands/import.rb +79 -0
  24. data/lib/ronin/nmap/cli/commands/new.rb +226 -0
  25. data/lib/ronin/nmap/cli/commands/print.rb +133 -0
  26. data/lib/ronin/nmap/cli/commands/scan.rb +233 -0
  27. data/lib/ronin/nmap/cli/filtering_options.rb +355 -0
  28. data/lib/ronin/nmap/cli/importable.rb +68 -0
  29. data/lib/ronin/nmap/cli/port_list.rb +102 -0
  30. data/lib/ronin/nmap/cli.rb +50 -0
  31. data/lib/ronin/nmap/converter.rb +114 -0
  32. data/lib/ronin/nmap/converters/csv.rb +162 -0
  33. data/lib/ronin/nmap/converters/json.rb +562 -0
  34. data/lib/ronin/nmap/converters.rb +54 -0
  35. data/lib/ronin/nmap/exceptions.rb +47 -0
  36. data/lib/ronin/nmap/importer.rb +369 -0
  37. data/lib/ronin/nmap/root.rb +28 -0
  38. data/lib/ronin/nmap/version.rb +26 -0
  39. data/lib/ronin/nmap.rb +223 -0
  40. data/man/ronin-nmap-completion.1 +76 -0
  41. data/man/ronin-nmap-completion.1.md +78 -0
  42. data/man/ronin-nmap-convert.1 +33 -0
  43. data/man/ronin-nmap-convert.1.md +36 -0
  44. data/man/ronin-nmap-dump.1 +141 -0
  45. data/man/ronin-nmap-dump.1.md +119 -0
  46. data/man/ronin-nmap-grep.1 +33 -0
  47. data/man/ronin-nmap-grep.1.md +36 -0
  48. data/man/ronin-nmap-import.1 +52 -0
  49. data/man/ronin-nmap-import.1.md +57 -0
  50. data/man/ronin-nmap-new.1 +81 -0
  51. data/man/ronin-nmap-new.1.md +73 -0
  52. data/man/ronin-nmap-print.1 +61 -0
  53. data/man/ronin-nmap-print.1.md +63 -0
  54. data/man/ronin-nmap-scan.1 +86 -0
  55. data/man/ronin-nmap-scan.1.md +84 -0
  56. data/man/ronin-nmap.1 +58 -0
  57. data/man/ronin-nmap.1.md +57 -0
  58. data/ronin-nmap.gemspec +62 -0
  59. data/scripts/setup +161 -0
  60. metadata +168 -0
@@ -0,0 +1,378 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
4
+ #
5
+ # Copyright (c) 2023 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-nmap is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-nmap is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-nmap. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/nmap/cli/command'
22
+
23
+ require 'command_kit/colors'
24
+ require 'command_kit/printing/indent'
25
+ require 'nmap/xml'
26
+
27
+ module Ronin
28
+ module Nmap
29
+ class CLI
30
+ module Commands
31
+ #
32
+ # Parses and searches nmap XML file(s) for the pattern.
33
+ #
34
+ # ## Usage
35
+ #
36
+ # ronin-nmap grep [options] PATTERN XML_FILE [...]
37
+ #
38
+ # ## Options
39
+ #
40
+ # -h, --help Print help information
41
+ #
42
+ # ## Arguments
43
+ #
44
+ class Grep < Command
45
+
46
+ include CommandKit::Colors
47
+ include CommandKit::Printing::Indent
48
+
49
+ usage '[options] PATTERN XML_FILE [...]'
50
+
51
+ argument :pattern, required: true,
52
+ desc: 'The pattern to search for'
53
+
54
+ argument :xml_file, required: true,
55
+ repeats: true,
56
+ desc: 'The nmap XML file to search'
57
+
58
+ description 'Parses and searches nmap XML file(s) for the pattern'
59
+
60
+ man_page 'ronin-nmap-grep.1'
61
+
62
+ #
63
+ # Runs the `ronin-nmap grep` command.
64
+ #
65
+ # @param [String] pattern
66
+ # The pattern to search for.
67
+ #
68
+ # @param [Array<String>] xml_files
69
+ # The nmap `.xml` file(s) to grep.
70
+ #
71
+ def run(pattern,*xml_files)
72
+ xml_files.each do |xml_file|
73
+ unless File.file?(xml_file)
74
+ print_error "no such file or directory: #{xml_file}"
75
+ next
76
+ end
77
+
78
+ xml = ::Nmap::XML.open(xml_file)
79
+ hosts = grep_xml(xml,pattern)
80
+
81
+ highlight_hosts(hosts,pattern)
82
+ end
83
+ end
84
+
85
+ #
86
+ # Searches the parsed nmap XML for the text pattern.
87
+ #
88
+ # @param [::Nmap::XML] xml
89
+ # The parsed nmap XML object.
90
+ #
91
+ # @param [String] pattern
92
+ # The text pattern to search for.
93
+ #
94
+ # @return [Enumerator::Lazy<::Nmap::XML::Host>]
95
+ # The nmap XML host objects that contain the text pattern.
96
+ #
97
+ def grep_xml(xml,pattern)
98
+ xml.each_up_host.lazy.filter do |host|
99
+ match_host(host,pattern)
100
+ end
101
+ end
102
+
103
+ #
104
+ # Determines if the nmap XML host object contains the text pattern.
105
+ #
106
+ # @param [::Nmap::XML::Host] host
107
+ # The nmap XML host object to search.
108
+ #
109
+ # @param [String] pattern
110
+ # The text pattern to search for.
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ def match_host(host,pattern)
115
+ hostnames = host.each_hostname
116
+ open_ports = host.each_open_port
117
+ host_script = host.host_script
118
+
119
+ hostnames.any? { |hostname| match_hostname(hostname,pattern) } ||
120
+ open_ports.any? { |port| match_port(port,pattern) } ||
121
+ (host_script && match_scripts(host_script,pattern))
122
+ end
123
+
124
+ #
125
+ # Determines if the nmap XML hostname object contains the text
126
+ # pattern.
127
+ #
128
+ # @param [::Nmap::XML::Hostname] hostname
129
+ # The nmap XML hostname object to search.
130
+ #
131
+ # @param [String] pattern
132
+ # The text pattern to search for.
133
+ #
134
+ # @return [Boolean]
135
+ #
136
+ def match_hostname(hostname,pattern)
137
+ hostname.name.match(pattern)
138
+ end
139
+
140
+ #
141
+ # Determines if the nmap XML port object contains the text pattern.
142
+ #
143
+ # @param [::Nmap::XML::Port] port
144
+ # The nmap XML port object to search.
145
+ #
146
+ # @param [String] pattern
147
+ # The text pattern to search for.
148
+ #
149
+ # @return [Boolean]
150
+ #
151
+ def match_port(port,pattern)
152
+ match_scripts(port,pattern) || if (service = port.service)
153
+ match_service(service,pattern)
154
+ end
155
+ end
156
+
157
+ #
158
+ # Determines if the nmap XML service object contains the text pattern.
159
+ #
160
+ # @param [::Nmap::XML::Service] service
161
+ # The nmap XML service object to search.
162
+ #
163
+ # @param [String] pattern
164
+ # The text pattern to search for.
165
+ #
166
+ # @return [Boolean]
167
+ #
168
+ def match_service(service,pattern)
169
+ product = service.product
170
+ version = service.version
171
+ extra_info = service.extra_info
172
+
173
+ service.name.match(pattern) ||
174
+ (product && product.match(pattern)) ||
175
+ (version && version.match(pattern)) ||
176
+ (extra_info && extra_info.match(pattern))
177
+ end
178
+
179
+ #
180
+ # Determines if the nmap XML scripts object contains the text pattern.
181
+ #
182
+ # @param [::Nmap::XML::Scripts] has_scripts
183
+ # The nmap XML object that includes `Nmap::XML::Scripts`.
184
+ #
185
+ # @param [String] pattern
186
+ # The text pattern to search for.
187
+ #
188
+ # @return [Boolean]
189
+ #
190
+ def match_scripts(has_scripts,pattern)
191
+ has_scripts.scripts.any? do |id,script|
192
+ match_script(script,pattern)
193
+ end
194
+ end
195
+
196
+ #
197
+ # Determines if the nmap XML script object contains the text pattern.
198
+ #
199
+ # @param [::Nmap::XML::Script] script
200
+ # The nmap XML script object to search.
201
+ #
202
+ # @param [String] pattern
203
+ # The text pattern to search for.
204
+ #
205
+ # @return [Boolean]
206
+ #
207
+ def match_script(script,pattern)
208
+ script.id.match(pattern) || script.output.match(pattern)
209
+ end
210
+
211
+ #
212
+ # Prints the nmap hosts with the pattern highlighted in the output.
213
+ #
214
+ # @param [Enumerator::Lazy<::Nmap::XML::Host>] hosts
215
+ # The nmap hosts to print.
216
+ #
217
+ # @param [String] pattern
218
+ # The pattern to highlight in the output.
219
+ #
220
+ def highlight_hosts(hosts,pattern)
221
+ hosts.each do |host|
222
+ highlight_host(host,pattern)
223
+ puts
224
+ end
225
+ end
226
+
227
+ #
228
+ # Prints the nmap host with the pattern highlighted in the output.
229
+ #
230
+ # @param [::Nmap::XML::Host] host
231
+ # The nmap host to print.
232
+ #
233
+ # @param [String] pattern
234
+ # The text pattern to highlight in the output.
235
+ #
236
+ def highlight_host(host,pattern)
237
+ addresses = host.addresses
238
+ hostnames = host.hostnames
239
+
240
+ unless hostnames.empty?
241
+ puts "[ #{addresses.first} / #{highlight(hostnames.first,pattern)} ]"
242
+ else
243
+ puts "[ #{addresses.first} ]"
244
+ end
245
+ puts
246
+
247
+ indent do
248
+ if addresses.length > 1
249
+ puts "[ addresses ]"
250
+ puts
251
+
252
+ indent do
253
+ addresses.each do |address|
254
+ puts address
255
+ end
256
+ end
257
+ puts
258
+ end
259
+
260
+ if hostnames.length > 1
261
+ puts "[ hostnames ]"
262
+ puts
263
+
264
+ indent do
265
+ hostnames.each do |hostname|
266
+ puts highlight(hostname,pattern)
267
+ end
268
+ end
269
+ puts
270
+ end
271
+
272
+ if (host_script = host.host_script)
273
+ puts "[ host scripts ]"
274
+ puts
275
+
276
+ indent do
277
+ highlight_scripts(host_script)
278
+ end
279
+ end
280
+
281
+ puts "[ ports ]"
282
+ puts
283
+
284
+ indent do
285
+ host.each_open_port do |port|
286
+ highlight_port(port,pattern)
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ #
293
+ # Prints the nmap port with the pattern highlighted in the output.
294
+ #
295
+ # @param [::Nmap::XML::Port] port
296
+ # The nmap XML port object to print.
297
+ #
298
+ # @param [String] pattern
299
+ # The text pattern to highlight in the output.
300
+ #
301
+ def highlight_port(port,pattern)
302
+ port_line = "#{port.number}/#{port.protocol}\t#{port.state}"
303
+
304
+ if (service = port.service)
305
+ port_line << "\t#{highlight(service,pattern)}"
306
+
307
+ if (extra_info = service.extra_info)
308
+ port_line << " #{highlight(extra_info,pattern)}"
309
+ end
310
+ end
311
+
312
+ puts port_line
313
+
314
+ unless port.scripts.empty?
315
+ puts
316
+
317
+ indent do
318
+ highlight_scripts(port,pattern)
319
+ end
320
+ end
321
+ end
322
+
323
+ #
324
+ # Prints the nmap scripts with the pattern highlighted in the output.
325
+ #
326
+ # @param [::Nmap::XML::Scripts] has_scripts
327
+ # The nmap XML object that has scripts.
328
+ #
329
+ # @param [String] pattern
330
+ # The text pattern to highlight in the output.
331
+ #
332
+ def highlight_scripts(has_scripts,pattern)
333
+ has_scripts.scripts.each_value do |script|
334
+ highlight_script(script,pattern)
335
+ puts
336
+ end
337
+ end
338
+
339
+ #
340
+ # Prints the nmap script with the pattern highlighted in the output.
341
+ #
342
+ # @param [::Nmap::XML::Script] script
343
+ # The nmap XML script object to print.
344
+ #
345
+ # @param [String] pattern
346
+ # The text pattern to highlight in the output.
347
+ #
348
+ def highlight_script(script,pattern)
349
+ puts "#{highlight(script.id,pattern)}:"
350
+
351
+ indent do
352
+ script.output.strip.each_line do |line|
353
+ puts highlight(line,pattern)
354
+ end
355
+ end
356
+ end
357
+
358
+ #
359
+ # Highlights the pattern in the text.
360
+ #
361
+ # @param [String] text
362
+ # The text to modify.
363
+ #
364
+ # @param [String] pattern
365
+ # The pattern to highlight.
366
+ #
367
+ # @return [String]
368
+ # The modified text.
369
+ #
370
+ def highlight(text,pattern)
371
+ text.to_s.gsub(pattern,colors.bold(colors.red(pattern)))
372
+ end
373
+
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-nmap is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-nmap is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-nmap. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/nmap/cli/command'
22
+ require 'ronin/nmap/cli/importable'
23
+ require 'ronin/nmap/importer'
24
+
25
+ module Ronin
26
+ module Nmap
27
+ class CLI
28
+ module Commands
29
+ #
30
+ # The `ronin-nmap import` command.
31
+ #
32
+ # ## Usage
33
+ #
34
+ # ronin-nmap import [options] XML_FILE
35
+ #
36
+ # ## Options
37
+ #
38
+ # --db NAME The database to connect to (Default: default)
39
+ # --db-uri URI The database URI to connect to
40
+ # -h, --help Print help information
41
+ #
42
+ # ## Arguments
43
+ #
44
+ # XML_FILE The XML file to import
45
+ #
46
+ class Import < Command
47
+
48
+ include Importable
49
+
50
+ usage '[options] XML_FILE'
51
+
52
+ argument :xml_file, required: true,
53
+ desc: 'The XML file to import'
54
+
55
+ description 'Imports an nmap XML file into ronin-db'
56
+
57
+ man_page 'ronin-nmap-import.1'
58
+
59
+ #
60
+ # Runs the `ronin-nmap import` command.
61
+ #
62
+ # @param [String] xml_file
63
+ # The nmap XML file to import.
64
+ #
65
+ def run(xml_file)
66
+ unless File.file?(xml_file)
67
+ print_error "no such file or directory: #{xml_file}"
68
+ exit(1)
69
+ end
70
+
71
+ db_connect
72
+ import_file(xml_file)
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-nmap is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published
9
+ # by the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # ronin-nmap is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with ronin-nmap. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/nmap/cli/command'
22
+ require 'ronin/nmap/root'
23
+
24
+ require 'ronin/core/cli/generator'
25
+
26
+ module Ronin
27
+ module Nmap
28
+ class CLI
29
+ module Commands
30
+ #
31
+ # Generates a new nmap ruby script.
32
+ #
33
+ # ## Usage
34
+ #
35
+ # ronin-nmap new [options] FILE
36
+ #
37
+ # ## Options
38
+ #
39
+ # --parser Generate a nmap XML parser script
40
+ # --scanner Generate a nmap scanner script
41
+ # --printing Adds additional printing of the nmap scan data
42
+ # --import Also import the nmap XML scan data
43
+ # --xml-file XML_FILE Sets the XML file to write to or parse
44
+ # -p {PORT | [PORT1]-[PORT2]}[,...],
45
+ # --ports Sets the port range to scan
46
+ # --target TARGET Sets the targets to scan (Defaults: ARGV[0])
47
+ # -h, --help Print help information
48
+ #
49
+ # ## Arguments
50
+ #
51
+ # FILE The path to the new nmap ruby script.
52
+ #
53
+ # ## Examples
54
+ #
55
+ # ronin-nmap new scanner.rb --ports 22,80,443,8000-9000 --target example.com
56
+ # ronin-nmap new parser.rb --parser --xml-file path/to/nmap.xml --printing
57
+ #
58
+ class New < Command
59
+
60
+ include Core::CLI::Generator
61
+
62
+ template_dir File.join(ROOT,'data','templates')
63
+
64
+ usage '[options] FILE'
65
+
66
+ option :parser, desc: 'Generate a nmap XML parser script' do
67
+ @script_type = :parser
68
+ end
69
+
70
+ option :scanner, desc: 'Generate a nmap scanner script' do
71
+ @script_type = :scanner
72
+ end
73
+
74
+ option :printing, desc: 'Adds additional printing of the nmap scan data' do
75
+ @features[:printing] = true
76
+ end
77
+
78
+ option :import, desc: 'Also import the nmap XML scan data' do
79
+ @features[:import] = true
80
+ end
81
+
82
+ option :xml_file, value: {
83
+ type: String,
84
+ usage: 'XML_FILE'
85
+ },
86
+ desc: 'Sets the XML file to write to or parse' do |file|
87
+ @xml_file = file
88
+ end
89
+
90
+ option :syn_scan, desc: 'Enables SYN scanning' do
91
+ @syn_scan = true
92
+ end
93
+
94
+ option :ports, short: '-p',
95
+ value: {
96
+ type: String,
97
+ usage: '{PORT | [PORT1]-[PORT2]}[,...]'
98
+ },
99
+ desc: 'Sets the port range to scan' do |ports|
100
+ @ports = parse_port_range(ports)
101
+ rescue ArgumentError => error
102
+ raise(OptionParser::InvalidArgument,error.message)
103
+ end
104
+
105
+ option :target, value: {
106
+ type: String,
107
+ usage: 'TARGET'
108
+ },
109
+ desc: 'Sets the targets to scan (Defaults: ARGV[0])' do |target|
110
+ @targets << target
111
+ end
112
+
113
+ argument :path, desc: 'The path to the new nmap ruby script'
114
+
115
+ description 'Generates a new nmap ruby script'
116
+
117
+ man_page 'ronin-nmap-new.1'
118
+
119
+ examples [
120
+ "scanner.rb --ports 22,80,443,8000-9000 --target example.com",
121
+ "parser.rb --parser --xml-file path/to/nmap.xml --printing"
122
+ ]
123
+
124
+ # The script type.
125
+ #
126
+ # @return [:scanner, :parser]
127
+ attr_reader :script_type
128
+
129
+ # The optioanl XML file to write to or parse.
130
+ #
131
+ # @return [String, nil]
132
+ attr_reader :xml_file
133
+
134
+ # Specifies whether to enable SYN scanning.
135
+ #
136
+ # @return [Boolean]
137
+ attr_reader :syn_scan
138
+
139
+ # The optional ports to scan.
140
+ #
141
+ # @return [Array<Integer, Range(Integer,Integer)>, "-", nil]
142
+ attr_reader :ports
143
+
144
+ # The targets to scan.
145
+ #
146
+ # @return [Array<String>]
147
+ attr_reader :targets
148
+
149
+ # Additional features.
150
+ #
151
+ # @return [Hash{Symbol => Boolean}]
152
+ attr_reader :features
153
+
154
+ #
155
+ # Initializes the `ronin-nmap new` command.
156
+ #
157
+ # @param [Hash{Symbol => Object}] kwargs
158
+ # Additional keyword arguments for the command.
159
+ #
160
+ def initialize(**kwargs)
161
+ super(**kwargs)
162
+
163
+ @script_type = :scanner
164
+ @targets = []
165
+ @features = {}
166
+ end
167
+
168
+ #
169
+ # Runs the `ronin-nmap new` command.
170
+ #
171
+ # @param [String] file
172
+ # The path to the new nmap ruby script.
173
+ #
174
+ def run(file)
175
+ @directory = File.dirname(file)
176
+
177
+ mkdir @directory unless File.directory?(@directory)
178
+
179
+ erb "script.rb.erb", file
180
+ chmod '+x', file
181
+ end
182
+
183
+ #
184
+ # Parses a port range.
185
+ #
186
+ # @param [String] ports
187
+ # The port range to parse.
188
+ #
189
+ # @return [Array<Integer, Range(Integer,Integer)>, "-"]
190
+ # The parsed port range.
191
+ #
192
+ # @raise [ArgumentError]
193
+ # An invalid port range was given.
194
+ #
195
+ def parse_port_range(ports)
196
+ case ports
197
+ when '-' then '-'
198
+ else
199
+ ports.split(',').map do |port|
200
+ case port
201
+ when /\A\d+-\d+\z/
202
+ start, stop = port.split('-',2)
203
+
204
+ (start.to_i..stop.to_i)
205
+ when /\A\d+-\z/
206
+ start = port.chomp('-')
207
+
208
+ (start.to_i..)
209
+ when /\A-\d+\z/
210
+ stop = port[1..]
211
+
212
+ (..stop.to_i)
213
+ when /\A\d+\z/
214
+ port.to_i
215
+ else
216
+ raise(ArgumentError,"invalid port range: #{ports.inspect}")
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end