ronin-masscan 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 +11 -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 +40 -0
  12. data/README.md +204 -0
  13. data/Rakefile +43 -0
  14. data/bin/ronin-masscan +34 -0
  15. data/data/completions/ronin-masscan +83 -0
  16. data/data/templates/script.rb.erb +43 -0
  17. data/gemspec.yml +42 -0
  18. data/lib/ronin/masscan/cli/command.rb +40 -0
  19. data/lib/ronin/masscan/cli/commands/completion.rb +61 -0
  20. data/lib/ronin/masscan/cli/commands/convert.rb +133 -0
  21. data/lib/ronin/masscan/cli/commands/dump.rb +194 -0
  22. data/lib/ronin/masscan/cli/commands/grep.rb +235 -0
  23. data/lib/ronin/masscan/cli/commands/import.rb +94 -0
  24. data/lib/ronin/masscan/cli/commands/new.rb +203 -0
  25. data/lib/ronin/masscan/cli/commands/print.rb +162 -0
  26. data/lib/ronin/masscan/cli/commands/scan.rb +206 -0
  27. data/lib/ronin/masscan/cli/filtering_options.rb +312 -0
  28. data/lib/ronin/masscan/cli/importable.rb +68 -0
  29. data/lib/ronin/masscan/cli/port_list.rb +102 -0
  30. data/lib/ronin/masscan/cli.rb +50 -0
  31. data/lib/ronin/masscan/converter.rb +129 -0
  32. data/lib/ronin/masscan/converters/csv.rb +108 -0
  33. data/lib/ronin/masscan/converters/json.rb +142 -0
  34. data/lib/ronin/masscan/converters.rb +54 -0
  35. data/lib/ronin/masscan/exceptions.rb +47 -0
  36. data/lib/ronin/masscan/importer.rb +214 -0
  37. data/lib/ronin/masscan/root.rb +28 -0
  38. data/lib/ronin/masscan/version.rb +26 -0
  39. data/lib/ronin/masscan.rb +114 -0
  40. data/man/ronin-masscan-completion.1 +76 -0
  41. data/man/ronin-masscan-completion.1.md +78 -0
  42. data/man/ronin-masscan-convert.1 +37 -0
  43. data/man/ronin-masscan-convert.1.md +40 -0
  44. data/man/ronin-masscan-dump.1 +116 -0
  45. data/man/ronin-masscan-dump.1.md +94 -0
  46. data/man/ronin-masscan-grep.1 +56 -0
  47. data/man/ronin-masscan-grep.1.md +59 -0
  48. data/man/ronin-masscan-import.1 +52 -0
  49. data/man/ronin-masscan-import.1.md +57 -0
  50. data/man/ronin-masscan-new.1 +78 -0
  51. data/man/ronin-masscan-new.1.md +70 -0
  52. data/man/ronin-masscan-print.1 +53 -0
  53. data/man/ronin-masscan-print.1.md +56 -0
  54. data/man/ronin-masscan-scan.1 +86 -0
  55. data/man/ronin-masscan-scan.1.md +84 -0
  56. data/man/ronin-masscan.1 +61 -0
  57. data/man/ronin-masscan.1.md +58 -0
  58. data/ronin-masscan.gemspec +62 -0
  59. data/scripts/setup +161 -0
  60. metadata +168 -0
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-masscan - A Ruby library and CLI for working with masscan.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-masscan 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-masscan 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-masscan. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/masscan/cli/command'
22
+ require 'ronin/masscan/cli/importable'
23
+ require 'ronin/masscan/converter'
24
+ require 'ronin/core/cli/logging'
25
+
26
+ require 'tempfile'
27
+ require 'set'
28
+
29
+ module Ronin
30
+ module Masscan
31
+ class CLI
32
+ module Commands
33
+ #
34
+ # Runs masscan and outputs data as JSON or CSV or imports into the
35
+ # database.
36
+ #
37
+ # ## Usage
38
+ #
39
+ # ronin-masscan scan [options] -- [masscan_options]
40
+ #
41
+ # ## Options
42
+ #
43
+ # --db NAME The database to connect to (Default: default)
44
+ # --db-uri URI The database URI to connect to
45
+ # --sudo Runs the masscan command under sudo
46
+ # -o, --output FILE The output file
47
+ # -F, --output-format json|csv The output format
48
+ # --import Imports the scan results into the database
49
+ # -h, --help Print help information
50
+ #
51
+ # ## Arguments
52
+ #
53
+ # masscan_options ... Additional arguments for masscan
54
+ #
55
+ # ## Examples
56
+ #
57
+ # ronin-masscan scan -o scan.json -- 192.168.1.1
58
+ # ronin-masscan scan --import -- 192.168.1.1
59
+ #
60
+ class Scan < Command
61
+
62
+ include Importable
63
+ include Core::CLI::Logging
64
+
65
+ usage '[options] -- [masscan_options]'
66
+
67
+ option :sudo, desc: 'Runs the masscan command under sudo'
68
+
69
+ option :output, short: '-o',
70
+ value: {
71
+ type: String,
72
+ usage: 'FILE'
73
+ },
74
+ desc: 'The output file'
75
+
76
+ option :output_format, short: '-F',
77
+ value: {
78
+ type: [:json, :csv]
79
+ },
80
+ desc: 'The output format'
81
+
82
+ option :import, desc: 'Imports the scan results into the database'
83
+
84
+ argument :masscan_args, required: true,
85
+ repeats: true,
86
+ usage: 'masscan_options',
87
+ desc: 'Additional arguments for masscan'
88
+
89
+ description 'Runs masscan and outputs data as JSON or CSV or imports into the database'
90
+
91
+ examples [
92
+ '-o scan.json -- 192.168.1.1',
93
+ '--import -- 192.168.1.1'
94
+ ]
95
+
96
+ man_page 'ronin-masscan-scan.1'
97
+
98
+ #
99
+ # Runs the `ronin-masscan scan` command.
100
+ #
101
+ # @param [Array<String>] masscan_args
102
+ def run(*masscan_args)
103
+ if (output = options[:output])
104
+ output_format = options.fetch(:output_format) do
105
+ infer_output_format(output)
106
+ end
107
+
108
+ if output_format.nil?
109
+ print_error "cannot infer the output format of the output file (#{output.inspect}), please specify --output-format"
110
+ exit(1)
111
+ end
112
+ end
113
+
114
+ tempfile = Tempfile.new(['ronin-masscan', '.bin'])
115
+
116
+ log_info "Running masscan #{masscan_args.join(' ')} ..."
117
+
118
+ unless run_masscan(*masscan_args, output: tempfile.path)
119
+ print_error "failed to run masscan"
120
+ exit(1)
121
+ end
122
+
123
+ if output
124
+ log_info "Saving #{output_format.upcase} output to #{output} ..."
125
+ save_output(tempfile.path,output, format: output_format)
126
+ end
127
+
128
+ if options[:import]
129
+ log_info "Importing masscan output ..."
130
+ import_scan(tempfile.path)
131
+ end
132
+ end
133
+
134
+ #
135
+ # Runs the `masscan` command.
136
+ #
137
+ # @param [Array<String>] masscan_args
138
+ # Additional arguments for `masscan`.
139
+ #
140
+ # @param [String] output
141
+ # The output file to save the scan data to.
142
+ #
143
+ # @return [Boolean, nil]
144
+ # Indicates whether the `masscan` command was successful.
145
+ #
146
+ def run_masscan(*masscan_args, output: )
147
+ masscan_command = ['masscan', '-v', *masscan_args, '-oB', output]
148
+ masscan_command.unshift('sudo') if options[:sudo]
149
+
150
+ return system(*masscan_command)
151
+ end
152
+
153
+ #
154
+ # Saves the masscan scan results to an output file in the given
155
+ # format.
156
+ #
157
+ # @param [String] path
158
+ # The path to the masscan output file.
159
+ #
160
+ # @param [String] output
161
+ # The path to the desired output file.
162
+ #
163
+ # @param [:json, :csv] format
164
+ # The desired output format.
165
+ #
166
+ def save_output(path,output, format: )
167
+ # the format has been explicitly specified
168
+ Converter.convert_file(path,output, format: format)
169
+ end
170
+
171
+ #
172
+ # Imports a masscan output file.
173
+ #
174
+ # @param [String] path
175
+ # The path to the output file.
176
+ #
177
+ def import_scan(path)
178
+ db_connect
179
+ import_file(path)
180
+ end
181
+
182
+ # Supported output formats.
183
+ OUTPUT_FORMATS = {
184
+ '.json' => :json,
185
+ '.csv' => :csv
186
+ }
187
+
188
+ #
189
+ # Infers the output format from the given path's file extension.
190
+ #
191
+ # @param [String] path
192
+ # The path to infer the output format from.
193
+ #
194
+ # @return [:json, :csv, nil]
195
+ # The output format or `nil` if the path's file extension is
196
+ # unknown.
197
+ #
198
+ def infer_output_format(path)
199
+ OUTPUT_FORMATS[File.extname(path)]
200
+ end
201
+
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-masscan - A Ruby library and CLI for working with masscan.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-masscan 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-masscan 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-masscan. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/masscan/cli/port_list'
22
+
23
+ module Ronin
24
+ module Masscan
25
+ class CLI
26
+ #
27
+ # Mixin which adds nmap target filtering options to commands.
28
+ #
29
+ module FilteringOptions
30
+ #
31
+ # Adds filtering options to the command class including
32
+ # {FilteringOptions}.
33
+ #
34
+ # @param [Class<Command>] command
35
+ # The command class including {FilteringOptions}.
36
+ #
37
+ def self.included(command)
38
+ command.option :protocol, short: '-P',
39
+ value: {
40
+ type: [:tcp, :udp]
41
+ },
42
+ desc: 'Filters the targets by protocol' do |proto|
43
+ @protocols << proto
44
+ end
45
+
46
+ command.option :ip, value: {
47
+ type: String,
48
+ usage: 'IP'
49
+ },
50
+ desc: 'Filters the targets by IP' do |ip|
51
+ @ips << ip
52
+ end
53
+
54
+ command.option :ip_range, value: {
55
+ type: String,
56
+ usage: 'CIDR'
57
+ },
58
+ desc: 'Filters the targets by IP range' do |ip_range|
59
+ @ip_ranges << IPAddr.new(ip_range)
60
+ end
61
+
62
+ command.option :ports, short: '-p',
63
+ value: {
64
+ type: /\A(?:\d+|\d+-\d+)(?:,(?:\d+|\d+-\d+))*\z/,
65
+ usage: '{PORT | PORT1-PORT2},...'
66
+ },
67
+ desc: 'Filter targets by port number' do |ports|
68
+ @ports << PortList.parse(ports)
69
+ end
70
+
71
+ command.option :with_app_protocol, value: {
72
+ type: /\A[a-z][a-z0-9_-]*\z/,
73
+ usage: 'APP_PROTOCOL[,...]'
74
+ },
75
+ desc: 'Filters targets with the app protocol' do |app_protocol|
76
+ @with_app_protocols << app_protocol.to_sym
77
+ end
78
+
79
+ command.option :with_payload, value: {
80
+ type: String,
81
+ usage: 'STRING'
82
+ },
83
+ desc: 'Filters targets containing the payload' do |string|
84
+ @with_payloads << string
85
+ end
86
+
87
+ command.option :with_payload_regex, value: {
88
+ type: Regexp,
89
+ usage: '/REGEX/'
90
+ },
91
+ desc: 'Filters targets with the matching payload' do |regexp|
92
+ @with_payloads << regexp
93
+ end
94
+ end
95
+
96
+ # The protocols to filter the targets by.
97
+ #
98
+ # @return [Set<:tcp, :udp>]
99
+ attr_reader :protocols
100
+
101
+ # The IPs to filter the targets by.
102
+ #
103
+ # @return [Set<String>]
104
+ attr_reader :ips
105
+
106
+ # The IP ranges to filter the targets by.
107
+ #
108
+ # @return [Set<IPAddr>]
109
+ attr_reader :ip_ranges
110
+
111
+ # The ports to filter the targets by.
112
+ #
113
+ # @return [Set<PortList>]
114
+ attr_reader :ports
115
+
116
+ # The app protocols to filter the targets by.
117
+ #
118
+ # @return [Set<String>]
119
+ attr_reader :with_app_protocols
120
+
121
+ # The payload Strings or Regexps to filter the targets by.
122
+ #
123
+ # @return [Set<String, Regexp>]
124
+ attr_reader :with_payloads
125
+
126
+ #
127
+ # Initializes the command.
128
+ #
129
+ # @param [Hash{Symbol => String}] kwargs
130
+ # Additional keywords for the command.
131
+ #
132
+ def initialize(**kwargs)
133
+ super(**kwargs)
134
+
135
+ @protocols = Set.new
136
+ @ips = Set.new
137
+ @ip_ranges = Set.new
138
+ @ports = Set.new
139
+
140
+ # Masscan::Banner filtering options
141
+ @with_app_protocols = Set.new
142
+ @with_payloads = Set.new
143
+ end
144
+
145
+ #
146
+ # Filters the masscan records.
147
+ #
148
+ # @param [::Masscan::OutputFile] output_file
149
+ # The parsed nmap xml data to filter.
150
+ #
151
+ # @return [Enumerator::Lazy]
152
+ # A lazy enumerator of the filtered records.
153
+ #
154
+ def filter_records(output_file)
155
+ records = output_file.each.lazy
156
+
157
+ unless @protocols.empty?
158
+ records = filter_records_by_protocol(records)
159
+ end
160
+
161
+ unless @ips.empty?
162
+ records = filter_records_by_ip(records)
163
+ end
164
+
165
+ unless @ip_ranges.empty?
166
+ records = filter_records_by_ip_range(records)
167
+ end
168
+
169
+ unless @ports.empty?
170
+ records = filter_records_by_port(records)
171
+ end
172
+
173
+ unless @with_app_protocols.empty?
174
+ records = filter_records_by_app_protocol(records)
175
+ end
176
+
177
+ unless @with_payloads.empty?
178
+ records = filter_records_by_payload(records)
179
+ end
180
+
181
+ return records
182
+ end
183
+
184
+ #
185
+ # Filter `Masscan::Status` records.
186
+ #
187
+ # @param [Enumerator::Lazy] records
188
+ # The records to filter.
189
+ #
190
+ # @return [Enumerator::Lazy]
191
+ # The filtered records.
192
+ #
193
+ def filter_status_records(records)
194
+ records.filter do |record|
195
+ record.kind_of?(::Masscan::Status)
196
+ end
197
+ end
198
+
199
+ #
200
+ # Filter `Masscan::Banner` records.
201
+ #
202
+ # @param [Enumerator::Lazy] records
203
+ # The records to filter.
204
+ #
205
+ # @return [Enumerator::Lazy]
206
+ # The filtered records.
207
+ #
208
+ def filter_banner_records(records)
209
+ records.filter do |record|
210
+ record.kind_of?(::Masscan::Banner)
211
+ end
212
+ end
213
+
214
+ #
215
+ # Filters the records by protocol
216
+ #
217
+ # @param [Enumerator::Lazy] records
218
+ # The records to filter.
219
+ #
220
+ # @return [Enumerator::Lazy]
221
+ # A lazy enumerator of the filtered records.
222
+ #
223
+ def filter_records_by_protocol(records)
224
+ records.filter do |record|
225
+ @protocols.include?(record.protocol)
226
+ end
227
+ end
228
+
229
+ #
230
+ # Filters the records by IP address.
231
+ #
232
+ # @param [Enumerator::Lazy] records
233
+ # The records to filter.
234
+ #
235
+ # @return [Enumerator::Lazy]
236
+ # A lazy enumerator of the filtered records.
237
+ #
238
+ def filter_records_by_ip(records)
239
+ records.filter do |record|
240
+ @ips.include?(record.ip)
241
+ end
242
+ end
243
+
244
+ #
245
+ # Filters the records by an IP rangeo.
246
+ #
247
+ # @param [Enumerator::Lazy] records
248
+ # The records to filter.
249
+ #
250
+ # @return [Enumerator::Lazy]
251
+ # A lazy enumerator of the filtered records.
252
+ #
253
+ def filter_records_by_ip_range(records)
254
+ records.filter do |record|
255
+ @ip_ranges.any? do |ip_range|
256
+ ip_range.include?(record.ip)
257
+ end
258
+ end
259
+ end
260
+
261
+ #
262
+ # Filters the records by port number.
263
+ #
264
+ # @param [Enumerator::Lazy] records
265
+ # The records to filter.
266
+ #
267
+ # @return [Enumerator::Lazy]
268
+ # A lazy enumerator of the filtered records.
269
+ #
270
+ def filter_records_by_port(records)
271
+ records.filter do |record|
272
+ @ports.include?(record.port)
273
+ end
274
+ end
275
+
276
+ #
277
+ # Filters the records by app-protocol IDs.
278
+ #
279
+ # @param [Enumerator::Lazy] records
280
+ # The records to filter.
281
+ #
282
+ # @return [Enumerator::Lazy]
283
+ # A lazy enumerator of the filtered records.
284
+ #
285
+ def filter_records_by_app_protocol(records)
286
+ records.filter do |record|
287
+ record.kind_of?(::Masscan::Banner) &&
288
+ @with_app_protocols.include?(record.app_protocol)
289
+ end
290
+ end
291
+
292
+ #
293
+ # Filters the records by payload contents.
294
+ #
295
+ # @param [Enumerator::Lazy] records
296
+ # The records to filter.
297
+ #
298
+ # @return [Enumerator::Lazy]
299
+ # A lazy enumerator of the filtered records.
300
+ #
301
+ def filter_records_by_payload(records)
302
+ regexp = Regexp.union(@with_payloads.to_a)
303
+
304
+ records.filter do |record|
305
+ record.kind_of?(::Masscan::Banner) &&
306
+ record.payload =~ regexp
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ronin-masscan- A Ruby library and CLI for working with masscan.
4
+ #
5
+ # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
6
+ #
7
+ # ronin-masscan 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-masscan 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-masscan. If not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ require 'ronin/masscan/importer'
22
+ require 'ronin/db/cli/database_options'
23
+ require 'ronin/db/cli/printing'
24
+ require 'ronin/core/cli/logging'
25
+
26
+ module Ronin
27
+ module Masscan
28
+ class CLI
29
+ #
30
+ # Mixin module which adds the ability to import masscan scan files into
31
+ # the [ronin-db] database.
32
+ #
33
+ # [ronin-db]: https://github.com/ronin-rb/ronin-db#readme
34
+ #
35
+ module Importable
36
+ include DB::CLI::Printing
37
+ include Core::CLI::Logging
38
+
39
+ #
40
+ # Includes `Ronin::DB::CLI::DatabaseOptions` into the including command
41
+ # class.
42
+ #
43
+ # @param [Class<Command>] command
44
+ # The command class including {Importable}.
45
+ #
46
+ def self.included(command)
47
+ command.include DB::CLI::DatabaseOptions
48
+ end
49
+
50
+ #
51
+ # Imports an masscan scan file into the [ronin-db] database.
52
+ #
53
+ # [ronin-db]: https://github.com/ronin-rb/ronin-db#readme
54
+ #
55
+ # @param [String] masscan_file
56
+ # The path to the masscan scan file to import.
57
+ #
58
+ def import_file(masscan_file,**kwargs)
59
+ Importer.import_file(masscan_file,**kwargs) do |record|
60
+ if (type = record_type(record))
61
+ log_info "Imported #{type}: #{record}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,102 @@
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 'set'
22
+
23
+ module Ronin
24
+ module Masscan
25
+ class CLI
26
+ #
27
+ # Represents a list of port numbers and port ranges.
28
+ #
29
+ class PortList
30
+
31
+ # Port numbers.
32
+ #
33
+ # @return [Array<Integer>]
34
+ attr_reader :numbers
35
+
36
+ # Port ranges.
37
+ #
38
+ # @return [Array<Range>]
39
+ attr_reader :ranges
40
+
41
+ #
42
+ # Initialize the port list.
43
+ #
44
+ # @param [Array<Integer, Range>] ports
45
+ # The port numbers and ranges.
46
+ #
47
+ # @raise [ArgumentError]
48
+ # One of the ports was not an Integer or Range object.
49
+ #
50
+ def initialize(ports)
51
+ @numbers = Set.new
52
+ @ranges = Set.new
53
+
54
+ ports.each do |port|
55
+ case port
56
+ when Integer then @numbers << port
57
+ when Range then @ranges << port
58
+ else
59
+ raise(ArgumentError,"port must be an Integer or Range: #{port.inspect}")
60
+ end
61
+ end
62
+ end
63
+
64
+ #
65
+ # Parses the port list.
66
+ #
67
+ # @param [String] ports
68
+ # The port list to parse.
69
+ #
70
+ # @return [PortList]
71
+ # The port numbers and port ranges.
72
+ #
73
+ def self.parse(ports)
74
+ new(
75
+ ports.split(',').map do |port|
76
+ if port.include?('-')
77
+ start, stop = port.split('-',2)
78
+
79
+ Range.new(start.to_i,stop.to_i)
80
+ else
81
+ port.to_i
82
+ end
83
+ end
84
+ )
85
+ end
86
+
87
+ #
88
+ # Determines if the port is in the port list.
89
+ #
90
+ # @param [Integer] port
91
+ #
92
+ # @return [Boolean]
93
+ #
94
+ def include?(port)
95
+ @numbers.include?(port) ||
96
+ @ranges.any? { |range| range.include?(port) }
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end