ronin-nmap 0.1.0.rc1

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 (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