ruby-masscan 0.1.1 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f03fa94f7c501646be712a24b98fe07a6dd156c054c7ae27714cac80b1fbb5e8
4
- data.tar.gz: a346c7b055da689cad8571a38383b43a5e797889d6206b85ca7d2a64e25b904f
3
+ metadata.gz: 064d7200b60b80a1ed4f0459239235d21f96587a3199833988dd5953728cd168
4
+ data.tar.gz: 619757cb909fcec538c63ef5d37d2cb1f1029509d71e957be9aa6f74726ad4ea
5
5
  SHA512:
6
- metadata.gz: ecdea1cc3b62cf1d0dbb2b7771807c05f2e6339b573f6208dcfb21e94fdaad160e51adc077e158ec6c4b4bc9426c7925dfd376d53d455922c5e318e6ee999936
7
- data.tar.gz: 88bbf9a1332946ae7688fae852a1a3565269a853eff17434dc8e0b1fd7eae2c3e14b09cc64fa3fc80a06781f3fd644b97e5f97fcc52ccd2aa71cbc2a32b15f3c
6
+ metadata.gz: 9d9fa47ab80653e00c4d3f16b02e1cbe824575d0979b43596f7a52c703b9eda0616882e8bd7effdb77a3f9bfafe40b50043ace08a06f4725b59335dab724fee2
7
+ data.tar.gz: ad559b68f9af88c2c2e1b66711a93b3bceda9962cd0b3f1f478400a0f1dd7d79a7a87eab81eef4ff9c1cca426c135732d05437affb757fdc3d1ebcfc64b681b2
@@ -9,8 +9,6 @@ jobs:
9
9
  fail-fast: false
10
10
  matrix:
11
11
  ruby:
12
- - 2.4
13
- - 2.5
14
12
  - 2.6
15
13
  - 2.7
16
14
  - 3.0
data/ChangeLog.md CHANGED
@@ -1,3 +1,16 @@
1
+ ### 0.2.1 / 2023-03-15
2
+
3
+ * Unescape `\\xXX` hex escaped characters in payload strings parsed from `.list`
4
+ masscan files.
5
+
6
+ ### 0.2.0 / 2021-11-30
7
+
8
+ * Replaced the `rprogram` dependency with [command_mapper].
9
+ * Fixed a typo in the mapping of the `-oJ` option flag.
10
+ * Added {Masscan::OutputFile#to_s}.
11
+
12
+ [command_mapper]: https://github.com/postmodern/command_mapper.rb#readme
13
+
1
14
  ### 0.1.1 / 2021-09-09
2
15
 
3
16
  * Added missing {Masscan::Banner#ttl}.
data/Gemfile CHANGED
@@ -2,14 +2,16 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
+ # gem 'command_mapper', '~> 0.1', github: 'postmodern/command_mapper.rb'
6
+
5
7
  group :development do
6
8
  gem 'rake'
7
9
  gem 'rubygems-tasks', '~> 0.2'
8
10
  gem 'rspec', '~> 3.0'
9
-
10
- gem 'json'
11
11
  gem 'simplecov', '~> 0.7'
12
+
12
13
  gem 'kramdown'
14
+ gem 'redcarpet', platform: :mri
13
15
  gem 'yard', '~> 0.9'
14
16
  gem 'yard-spellcheck', require: false
15
17
  end
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2021 Hal Brodigan
1
+ Copyright (c) 2021-2023 Hal Brodigan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -15,17 +15,22 @@ file formats.
15
15
 
16
16
  ## Features
17
17
 
18
- * Provides a Ruby interface for running the `masscan` command.
19
- * Supports parsing masscan Binary, List, and JSON output files.
18
+ * Provides a [Ruby interface][Masscan::Command] for running the `masscan`
19
+ utility.
20
+ * Supports [parsing][Masscan::OutputFile] masscan Binary, List, and JSON output
21
+ files.
22
+
23
+ [Masscan::Command]: https://rubydoc.info/gems/ruby-masscan/Masscan/Command
24
+ [Masscan::OutputFile]: https://rubydoc.info/gems/ruby-masscan/Masscan/OutputFile
20
25
 
21
26
  ## Examples
22
27
 
23
28
  Run `sudo masscan` from Ruby:
24
29
 
25
30
  ```ruby
26
- require 'masscan/program'
31
+ require 'masscan/command'
27
32
 
28
- Masscan::Program.sudo_scan do |masscan|
33
+ Masscan::Command.sudo do |masscan|
29
34
  masscan.output_format = :list
30
35
  masscan.output_file = 'masscan.txt'
31
36
 
@@ -108,7 +113,7 @@ end
108
113
 
109
114
  * [ruby] >= 2.0.0
110
115
  * [masscan] >= 1.0.0
111
- * [rprogram] ~> 0.3
116
+ * [command_mapper] ~> 0.1
112
117
 
113
118
  ## Install
114
119
 
@@ -119,21 +124,21 @@ $ gem install ruby-masscan
119
124
  ### gemspec
120
125
 
121
126
  ```ruby
122
- gemspec.add_dependency 'ruby-masscan', '~> 0.1'
127
+ gemspec.add_dependency 'ruby-masscan', '~> 0.2'
123
128
  ```
124
129
 
125
130
  ### Gemfile
126
131
 
127
132
  ```ruby
128
- gem 'ruby-masscan', '~> 0.1'
133
+ gem 'ruby-masscan', '~> 0.2'
129
134
  ```
130
135
 
131
136
  ## License
132
137
 
133
- Copyright (c) 2021 Hal Brodigan
138
+ Copyright (c) 2021-2023 Hal Brodigan
134
139
 
135
140
  See {file:LICENSE.txt} for license information.
136
141
 
137
142
  [masscan]: https://github.com/robertdavidgraham/masscan#readme
138
143
  [ruby]: https://www.ruby-lang.org/
139
- [rprogram]: https://github.com/postmodern/rprogram#readme
144
+ [command_mapper]: https://github.com/postmodern/command_mapper.rb#readme
data/gemspec.yml CHANGED
@@ -16,13 +16,14 @@ metadata:
16
16
  source_code_uri: https://github.com/postmodern/ruby-masscan
17
17
  bug_tracker_uri: https://github.com/postmodern/ruby-masscan/issues
18
18
  changelog_uri: https://github.com/postmodern/ruby-masscan/blob/master/ChangeLog.md
19
+ rubygems_mfa_required: 'true'
19
20
 
20
21
  required_ruby_version: ">= 2.0.0"
21
22
 
22
23
  requirements: masscan >= 1.0.0
23
24
 
24
25
  dependencies:
25
- rprogram: ~> 0.3
26
+ command_mapper: ~> 0.1
26
27
 
27
28
  development_dependencies:
28
29
  bundler: ~> 2.0
@@ -0,0 +1,240 @@
1
+ require 'command_mapper/command'
2
+
3
+ module Masscan
4
+ #
5
+ # Provides an interface for invoking the `masscan` utility.
6
+ #
7
+ # ## Example
8
+ #
9
+ # require 'masscan/command'
10
+ #
11
+ # Masscan::Command.sudo do |masscan|
12
+ # masscan.output_format = :list
13
+ # masscan.output_file = 'masscan.txt'
14
+ #
15
+ # masscan.ips = '192.168.1.1/24'
16
+ # masscan.ports = [20,21,22,23,25,80,110,443,512,522,8080,1080]
17
+ # end
18
+ #
19
+ # ## `masscan` options:
20
+ #
21
+ # * `--range` - `masscan.range`
22
+ # * `-p` - `masscan.ports`
23
+ # * `--banners` - `masscan.banners`
24
+ # * `--rate` - `masscan.rate`
25
+ # * `--conf` - `masscan.config_file`
26
+ # * `--resume` - `masscan.resume`
27
+ # * `--echo` - `masscan.echo`
28
+ # * `--adapter` - `masscan.adapter`
29
+ # * `--adapter-ip` - `masscan.adapter_ip`
30
+ # * `--adapter-port` - `masscan.adapter_port`
31
+ # * `--adapter-mac` - `masscan.adapter_mac`
32
+ # * `--adapter-vlan` - `masscan.adapter_vlan`
33
+ # * `--router-mac` - `masscan.router_mac`
34
+ # * `--ping` - `masscan.ping`
35
+ # * `--exclude` - `masscan.exclude`
36
+ # * `--excludefile` - `masscan.exclude_file`
37
+ # * `--includefile` - `masscan.include_file`
38
+ # * `--append-output` - `masscan.append_output`
39
+ # * `--iflist` - `masscan.list_interfaces`
40
+ # * `--retries` - `masscan.retries`
41
+ # * `--nmap` - `masscan.nmap_help`
42
+ # * `--pcap-payloads` - `masscan.pcap_payloads`
43
+ # * `--nmap-payloads` - `masscan.nmap_payloads`
44
+ # * `--http-method` - `masscan.http_method`
45
+ # * `--http-url` - `masscan.http_url`
46
+ # * `--http-version` - `masscan.http_version`
47
+ # * `--http-host` - `masscan.http_host`
48
+ # * `--http-user-agent` - `masscan.http_user_agent`
49
+ # * `--http-field` - `masscan.http_field`
50
+ # * `--http-field-remove` - `masscan.http_field_remove`
51
+ # * `--http-cookie` - `masscan.http_cookie`
52
+ # * `--http-payload` - `masscan.http_payload`
53
+ # * `--show` - `masscan.show`
54
+ # * `--noshow` - `masscan.hide`
55
+ # * `--pcap` - `masscan.pcap`
56
+ # * `--packet-trace` - `masscan.packet_trace`
57
+ # * `--pfring` - `masscan.pfring`
58
+ # * `--resume-index` - `masscan.resume_index`
59
+ # * `--resume-count` - `masscan.resume_count`
60
+ # * `--shards` - `masscan.shards`
61
+ # * `--rotate` - `masscan.rotate`
62
+ # * `--rotate-offset` - `masscan.rotate_offset`
63
+ # * `--rotate-size` - `masscan.rotate_size`
64
+ # * `--rotate-dir` - `masscan.rotate_dir`
65
+ # * `--seed` - `masscan.seed`
66
+ # * `--regress` - `masscan.regress`
67
+ # * `--ttl` - `masscan.ttl`
68
+ # * `--wait` - `masscan.wait`
69
+ # * `--offline` - `masscan.offline`
70
+ # * `-sL` - `masscan.print_list`
71
+ # * `--interactive` - `masscan.interactive`
72
+ # * `--output-format` - `masscan.output_format`
73
+ # * `--output-filename` - `masscan.output_file`
74
+ # * `-oB` - `masscan.output_binary`
75
+ # * `-oX` - `masscan.output_xml`
76
+ # * `-oG` - `masscan.output_grepable`
77
+ # * ` -oJ` - `masscan.output_json`
78
+ # * `-oL` - `masscan.output_list`
79
+ # * `--readscan` - `masscan.read_scan`
80
+ # * `-V` - `masscan.version`
81
+ # * `-h` - `masscan.help`
82
+ #
83
+ # @see https://github.com/robertdavidgraham/masscan/blob/master/doc/masscan.8.markdown
84
+ #
85
+ # @since 0.2.0
86
+ #
87
+ class Command < CommandMapper::Command
88
+
89
+ class PortList < CommandMapper::Types::Num
90
+
91
+ def validate(value)
92
+ case value
93
+ when Array
94
+ value.each do |element|
95
+ valid, message = validate(element)
96
+
97
+ unless valid
98
+ return [valid, message]
99
+ end
100
+ end
101
+
102
+ return true
103
+ when Range
104
+ valid, message = super(value.begin)
105
+
106
+ unless valid
107
+ return [valid, message]
108
+ end
109
+
110
+ valid, message = super(value.end)
111
+
112
+ unless valid
113
+ return [valid, message]
114
+ end
115
+
116
+ return true
117
+ else
118
+ super(value)
119
+ end
120
+ end
121
+
122
+ def format(value)
123
+ case value
124
+ when Array
125
+ value.map(&method(:format)).join(',')
126
+ when Range
127
+ "#{value.begin}-#{value.end}"
128
+ else
129
+ super(value)
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ class Shards < CommandMapper::Types::Str
136
+
137
+ def validate(value)
138
+ case value
139
+ when Array
140
+ if value.length > 2
141
+ return [false, "cannot contain more tha two elements (#{value.inspect})"]
142
+ end
143
+
144
+ return true
145
+ else
146
+ super(value)
147
+ end
148
+ end
149
+
150
+ def format(value)
151
+ case value
152
+ when Array
153
+ "#{value[0]}/#{value[1]}"
154
+ else
155
+ super(value)
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ command "masscan" do
162
+ option '--range', name: :range, value: {type: List.new}
163
+ option '-p', name: :ports, value: {type: PortList.new}
164
+ option '--banners', name: :banners
165
+ option '--rate', name: :rate, value: {type: Num.new}
166
+ option '--conf', name: :config_file, value: {type: InputFile.new}
167
+ option '--resume', name: :resume, value: {type: InputFile.new}
168
+ option '--echo', name: :echo, value: true
169
+ option '--adapter', name: :adapter, value: true
170
+ option '--adapter-ip', name: :adapter_ip, value: true
171
+ option '--adapter-port', name: :adapter_port, value: {type: Num.new}
172
+ option '--adapter-mac', name: :adapter_mac, value: true
173
+ option '--adapter-vlan', name: :adapter_vlan, value: true
174
+ option '--router-mac', name: :router_mac, value: true
175
+ option '--ping', name: :ping
176
+ option '--exclude', name: :exclude, value: {type: List.new}
177
+ option '--excludefile', name: :exclude_file, value: {type: InputFile.new}
178
+ option '--includefile', name: :include_file, value: {type: InputFile.new}
179
+ option '--append-output', name: :append_output
180
+ option '--iflist', name: :list_interfaces
181
+ option '--retries', name: :retries
182
+ option '--nmap', name: :nmap_help
183
+ option '--pcap-payloads', name: :pcap_payloads
184
+ option '--nmap-payloads', name: :nmap_payloads, value: {type: InputFile.new}
185
+
186
+ option '--http-method', name: :http_method, value: true
187
+ option '--http-url', name: :http_url, value: true
188
+ option '--http-version', name: :http_version, value: true
189
+ option '--http-host', name: :http_host, value: true
190
+ option '--http-user-agent', name: :http_user_agent, value: true
191
+
192
+ option '--http-field', value: {type: KeyValue.new}, repeats: true
193
+
194
+ option '--http-field-remove', name: :http_field_remove
195
+ option '--http-cookie', name: :http_cookie
196
+ option '--http-payload', name: :http_payload
197
+
198
+ option '--show', name: :show
199
+ option '--noshow', name: :hide
200
+ option '--pcap', name: :pcap, value: true
201
+ option '--packet-trace', name: :packet_trace
202
+ option '--pfring', name: :pfring
203
+ option '--resume-index', name: :resume_index
204
+ option '--resume-count', name: :resume_count
205
+ option '--shards', name: :shards, value: {type: Shards.new}
206
+ option '--rotate', name: :rotate, value: true
207
+ option '--rotate-offset', name: :rotate_offset, value: true
208
+ option '--rotate-size', name: :rotate_size, value: true
209
+ option '--rotate-dir', name: :rotate_dir, value: {type: InputDir.new}
210
+ option '--seed', name: :seed, value: {type: Num.new}
211
+ option '--regress', name: :regress
212
+ option '--ttl', name: :ttl, value: {type: Num.new}
213
+ option '--wait', name: :wait, value: {type: Num.new}
214
+ option '--offline', name: :offline
215
+ option '-sL', name: :print_list
216
+ option '--interactive', name: :interactive
217
+ option '--output-format', name: :output_format, value: {
218
+ type: Enum[
219
+ :xml,
220
+ :binary,
221
+ :grepable,
222
+ :list,
223
+ :JSON
224
+ ]
225
+ }
226
+ option '--output-filename', name: :output_file, value: true
227
+ option '-oB', name: :output_binary, value: true
228
+ option '-oX', name: :output_xml, value: true
229
+ option '-oG', name: :output_grepable, value: true
230
+ option '-oJ', name: :output_json, value: true
231
+ option '-oL', name: :output_list, value: true
232
+ option '--readscan', name: :read_scan, value: {type: InputFile.new}
233
+ option '-V', name: :version
234
+ option '-h', name: :help
235
+
236
+ argument :ips, repeats: true
237
+ end
238
+
239
+ end
240
+ end
@@ -6,8 +6,18 @@ module Masscan
6
6
  #
7
7
  # Represents an output file.
8
8
  #
9
+ # ## Example
10
+ #
11
+ # output_file = Masscan::OutputFile.new('masscan.json')
12
+ # output_file.each do |record|
13
+ # p record
14
+ # end
15
+ #
9
16
  class OutputFile
10
17
 
18
+ # Mapping of formats to parsers.
19
+ #
20
+ # @api semipublic
11
21
  PARSERS = {
12
22
  binary: Parsers::Binary,
13
23
  list: Parsers::List,
@@ -29,6 +39,8 @@ module Masscan
29
39
  # The parser for the output file format.
30
40
  #
31
41
  # @return [Parsers::Binary, Parsers::JSON, Parsers::List]
42
+ #
43
+ # @api private
32
44
  attr_reader :parser
33
45
 
34
46
  #
@@ -52,6 +64,22 @@ module Masscan
52
64
  end
53
65
  end
54
66
 
67
+ # Mapping of file extensions to formats
68
+ #
69
+ # @api semipublic
70
+ FILE_FORMATS = {
71
+ '.bin' => :binary,
72
+ '.dat' => :binary,
73
+
74
+ '.txt' => :list,
75
+ '.list' => :list,
76
+
77
+ '.json' => :json,
78
+ '.ndjson' => :ndjson,
79
+
80
+ '.xml' => :xml
81
+ }
82
+
55
83
  #
56
84
  # Infers the format from the output file's extension name.
57
85
  #
@@ -64,14 +92,10 @@ module Masscan
64
92
  # @raise [ArgumentError]
65
93
  # The output format could not be inferred from the file's name.
66
94
  #
95
+ # @api semipublic
96
+ #
67
97
  def self.infer_format(path)
68
- case File.extname(path)
69
- when '.bin', '.dat' then :binary
70
- when '.txt', '.list' then :list
71
- when '.json' then :json
72
- when '.ndjson' then :ndjson
73
- when '.xml' then :xml
74
- else
98
+ FILE_FORMATS.fetch(File.extname(path)) do
75
99
  raise(ArgumentError,"could not infer format of #{path}")
76
100
  end
77
101
  end
@@ -96,5 +120,17 @@ module Masscan
96
120
  end
97
121
  end
98
122
 
123
+ #
124
+ # Converts the output file to a String.
125
+ #
126
+ # @return [String]
127
+ # The path to the output file.
128
+ #
129
+ # @since 0.2.0
130
+ #
131
+ def to_s
132
+ @path
133
+ end
134
+
99
135
  end
100
136
  end
@@ -64,7 +64,7 @@ module Masscan
64
64
  timestamp: parse_timestamp(timestamp)
65
65
  )
66
66
  elsif line.start_with?('banner ')
67
- type, ip_proto, port, ip, timestamp, app_proto, banner = line.split(' ',7)
67
+ type, ip_proto, port, ip, timestamp, app_proto, payload = line.split(' ',7)
68
68
 
69
69
  yield Banner.new(
70
70
  protocol: parse_ip_protocol(ip_proto),
@@ -72,11 +72,28 @@ module Masscan
72
72
  ip: parse_ip(ip),
73
73
  timestamp: parse_timestamp(timestamp),
74
74
  app_protocol: parse_app_protocol(app_proto),
75
- payload: banner
75
+ payload: parse_payload(payload)
76
76
  )
77
77
  end
78
78
  end
79
79
  end
80
+
81
+ #
82
+ # Parses a payload string and removes any `\\xXX` hex escaped characters.
83
+ #
84
+ # @param [String] payload
85
+ # The payload string to unescape.
86
+ #
87
+ # @return [String]
88
+ # The raw payload string.
89
+ #
90
+ # @api private
91
+ #
92
+ def self.parse_payload(payload)
93
+ payload.gsub(/\\x[0-9a-f]{2}/) do |hex_escape|
94
+ hex_escape[2..].to_i(16).chr
95
+ end
96
+ end
80
97
  end
81
98
  end
82
99
  end
@@ -1,99 +1,50 @@
1
- require 'masscan/task'
2
-
3
- require 'rprogram/program'
1
+ require 'masscan/command'
4
2
 
5
3
  module Masscan
6
4
  #
7
- # Represents the `masscan` program.
5
+ # @deprecated Please use {Command} instead.
8
6
  #
9
- class Program < RProgram::Program
10
-
11
- name_program 'masscan'
7
+ class Program < Command
12
8
 
13
9
  #
14
- # Finds the `masscan` program and performs a scan.
10
+ # Runs `masscan`.
15
11
  #
16
12
  # @param [Hash{Symbol => Object}] options
17
13
  # Additional options for masscan.
18
14
  #
19
- # @param [Hash{Symbol => Object}] exec_options
20
- # Additional exec-options.
21
- #
22
- # @yield [task]
23
- # If a block is given, it will be passed a task object
15
+ # @yield [masscan]
16
+ # If a block is given, it will be passed the new masscan instance
24
17
  # used to specify options for masscan.
25
18
  #
26
- # @yieldparam [Task] task
27
- # The masscan task object.
19
+ # @yieldparam [Masscan] masscan
20
+ # The masscan instance.
28
21
  #
29
22
  # @return [Boolean]
30
23
  # Specifies whether the command exited normally.
31
24
  #
32
- # @example Specifying `masscan` options via a Hash.
33
- # Masscan::Program.scan(
25
+ # @example Specifying `masscan` options via a Hash:
26
+ # Masscan::Command.scan(
34
27
  # ips: '192.168.1.1/24',
35
28
  # ports: [22, 80, 443],
36
29
  # )
37
30
  #
38
- # @example Specifying `masscan` options via a {Task} object.
39
- # Masscan::Program.scan do |masscan|
31
+ # @example Specifying `masscan` options via a block:
32
+ # Masscan::Command.scan do |masscan|
40
33
  # masscan.ips = '192.168.1.1/24'
41
34
  # masscan.ports = [22, 80, 443]
42
35
  # end
43
36
  #
44
- # @see #scan
45
- #
46
- def self.scan(options={},exec_options={},&block)
47
- find.scan(options,exec_options,&block)
37
+ def self.scan(options={},&block)
38
+ run(options,&block)
48
39
  end
49
40
 
50
41
  #
51
- # Finds the `masscan` program and performs a scan, but runs `masscan` under
52
- # `sudo`.
42
+ # Runs `masscan` but under `sudo`.
53
43
  #
54
44
  # @see scan
55
45
  #
56
- # @since 0.8.0
57
- #
58
- def self.sudo_scan(options={},exec_options={},&block)
59
- find.sudo_scan(options,exec_options,&block)
60
- end
61
-
62
- #
63
- # Performs a scan.
64
- #
65
- # @param [Hash{Symbol => Object}] options
66
- # Additional options for masscan.
67
- #
68
- # @param [Hash{Symbol => Object}] exec_options
69
- # Additional exec-options.
70
- #
71
- # @yield [task]
72
- # If a block is given, it will be passed a task object
73
- # used to specify options for masscan.
74
- #
75
- # @yieldparam [Task] task
76
- # The masscan task object.
77
- #
78
- # @return [Boolean]
79
- # Specifies whether the command exited normally.
80
- #
81
- # @see http://rubydoc.info/gems/rprogram/0.3.0/RProgram/Program#run-instance_method
82
- # For additional exec-options.
83
- #
84
- def scan(options={},exec_options={},&block)
85
- run_task(Task.new(options,&block),exec_options)
86
- end
87
-
88
- #
89
- # Performs a scan and runs `masscan` under `sudo`.
90
- #
91
- # @see #scan
92
- #
93
- # @since 0.8.0
94
- #
95
- def sudo_scan(options={},exec_options={},&block)
96
- sudo_task(Task.new(options,&block),exec_options)
46
+ def self.sudo_scan(options={},&block)
47
+ sudo(options,&block)
97
48
  end
98
49
 
99
50
  end
@@ -1,4 +1,4 @@
1
1
  module Masscan
2
2
  # ruby-masscan version
3
- VERSION = '0.1.1'
3
+ VERSION = '0.2.1'
4
4
  end
data/lib/masscan.rb CHANGED
@@ -1,2 +1,3 @@
1
+ require 'masscan/command'
1
2
  require 'masscan/program'
2
3
  require 'masscan/output_file'
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+ require 'masscan/command'
3
+
4
+ describe Masscan::Command do
5
+ describe described_class::PortList do
6
+ describe "#validate" do
7
+ context "when given a single port number" do
8
+ let(:value) { 443 }
9
+
10
+ it "must return true" do
11
+ expect(subject.validate(value)).to be(true)
12
+ end
13
+ end
14
+
15
+ context "when given a Range of port numbers" do
16
+ let(:value) { (1..1024) }
17
+
18
+ it "must return true" do
19
+ expect(subject.validate(value)).to be(true)
20
+ end
21
+ end
22
+
23
+ context "when given an Array of port numbers" do
24
+ let(:value) { [80, 443] }
25
+
26
+ it "must return true" do
27
+ expect(subject.validate(value)).to be(true)
28
+ end
29
+
30
+ context "and the Array contains Ranges" do
31
+ let(:value) { [80, (1..42), 443] }
32
+
33
+ it "must return true" do
34
+ expect(subject.validate(value)).to be(true)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "#format" do
41
+ context "when given a single port number" do
42
+ let(:value) { 443 }
43
+
44
+ it "must return the formatted port number" do
45
+ expect(subject.format(value)).to eq(value.to_s)
46
+ end
47
+ end
48
+
49
+ context "when given a Range of port numbers" do
50
+ let(:value) { (1..1024) }
51
+
52
+ it "must return the formatted port number range (ex: 1-102)" do
53
+ expect(subject.format(value)).to eq("#{value.begin}-#{value.end}")
54
+ end
55
+ end
56
+
57
+ context "when given an Array of port numbers" do
58
+ let(:value) { [80, 443] }
59
+
60
+ it "must return the formatted list of port numbers" do
61
+ expect(subject.format(value)).to eq(value.join(','))
62
+ end
63
+
64
+ context "and the Array contains Ranges" do
65
+ let(:value) { [80, (1..42), 443] }
66
+
67
+ it "must return the formatted list of port numbers and port ranges" do
68
+ expect(subject.format(value)).to eq("#{value[0]},#{value[1].begin}-#{value[1].end},#{value[2]}")
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ describe described_class::Shards do
76
+ describe "#validate" do
77
+ context "when given a Rational value" do
78
+ let(:value) { (1/2r) }
79
+
80
+ it "must return true" do
81
+ expect(subject.validate(value)).to be(true)
82
+ end
83
+ end
84
+
85
+ context "when given an Array value" do
86
+ let(:value) { [1, 2] }
87
+
88
+ it "must return true" do
89
+ expect(subject.validate(value)).to be(true)
90
+ end
91
+
92
+ context "but the Array length is > 2" do
93
+ let(:value) { [1,2,3] }
94
+
95
+ it "must return a validation error" do
96
+ expect(subject.validate(value)).to eq(
97
+ [false, "cannot contain more tha two elements (#{value.inspect})"]
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ context "otherwise" do
104
+ let(:value) { :"1/2" }
105
+
106
+ it "must return true" do
107
+ expect(subject.validate(value)).to be(true)
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#format" do
113
+ context "when given a Rational value" do
114
+ let(:value) { (1/2r) }
115
+
116
+ it "must format it into \#{numerator}/\#{denominator}" do
117
+ expect(subject.format(value)).to eq(
118
+ "#{value.numerator}/#{value.denominator}"
119
+ )
120
+ end
121
+ end
122
+
123
+ context "when given an Array value" do
124
+ let(:value) { [1, 2] }
125
+
126
+ it "must format it into \#{array[0]}/\#{array[1]}" do
127
+ expect(subject.format(value)).to eq(
128
+ "#{value[0]}/#{value[1]}"
129
+ )
130
+ end
131
+ end
132
+
133
+ context "otherwise" do
134
+ let(:value) { :"1/2" }
135
+
136
+ it "must convert the value to a String" do
137
+ expect(subject.format(value)).to eq(value.to_s)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -68,6 +68,38 @@ describe Masscan::OutputFile do
68
68
  end
69
69
  end
70
70
 
71
+ describe "FILE_FORMATS" do
72
+ subject { described_class::FILE_FORMATS }
73
+
74
+ describe ".bin" do
75
+ it { expect(subject['.bin']).to be(:binary) }
76
+ end
77
+
78
+ describe ".dat" do
79
+ it { expect(subject['.dat']).to be(:binary) }
80
+ end
81
+
82
+ describe ".txt" do
83
+ it { expect(subject['.txt']).to be(:list) }
84
+ end
85
+
86
+ describe ".list" do
87
+ it { expect(subject['.list']).to be(:list) }
88
+ end
89
+
90
+ describe ".json" do
91
+ it { expect(subject['.json']).to be(:json) }
92
+ end
93
+
94
+ describe ".ndjson" do
95
+ it { expect(subject['.ndjson']).to be(:ndjson) }
96
+ end
97
+
98
+ describe ".xml" do
99
+ it { expect(subject['.xml']).to be(:xml) }
100
+ end
101
+ end
102
+
71
103
  describe "#initialize" do
72
104
  let(:path) { "/path/to/file.json" }
73
105
 
@@ -100,7 +132,9 @@ describe Masscan::OutputFile do
100
132
  end
101
133
  end
102
134
 
103
- subject { described_class.new(Fixtures.join('masscan.list')) }
135
+ let(:path) { Fixtures.join('masscan.list') }
136
+
137
+ subject { described_class.new(path) }
104
138
 
105
139
  describe "#each" do
106
140
  context "when given a block" do
@@ -132,4 +166,10 @@ describe Masscan::OutputFile do
132
166
  end
133
167
  end
134
168
  end
169
+
170
+ describe "#to_s" do
171
+ it "must return #path" do
172
+ expect(subject.to_s).to eq(path)
173
+ end
174
+ end
135
175
  end
@@ -104,6 +104,31 @@ describe Masscan::Parsers::List do
104
104
  expect(yielded_banner.service).to eq(service_keyword)
105
105
  expect(yielded_banner.payload).to eq(payload)
106
106
  end
107
+
108
+ context "when the payload field contains '\\xXX' hex escaped characters" do
109
+ let(:escaped_payload) do
110
+ "HTTP/1.0 404 Not Found\\x0d\\x0aContent-Type: text/html\\x0d\\x0aDate: Thu, 26 Aug 2021 06:47:52 GMT\\x0d\\x0aServer: ECS (sec/974D)\\x0d\\x0aContent-Length: 345\\x0d\\x0aConnection: close\\x0d\\x0a\\x0d"
111
+ end
112
+ let(:unescaped_payload) do
113
+ "HTTP/1.0 404 Not Found\r\nContent-Type: text/html\r\nDate: Thu, 26 Aug 2021 06:47:52 GMT\r\nServer: ECS (sec/974D)\r\nContent-Length: 345\r\nConnection: close\r\n\r"
114
+ end
115
+
116
+ let(:line) do
117
+ "banner #{protocol} #{port} #{ip} #{timestamp.to_i} #{service_name} #{escaped_payload}"
118
+ end
119
+
120
+ it "must unescape the '\\xXX' hex escaped characters" do
121
+ yielded_records = []
122
+
123
+ subject.parse(io) do |record|
124
+ yielded_records << record
125
+ end
126
+
127
+ yielded_banner = yielded_records.first
128
+
129
+ expect(yielded_banner.payload).to eq(unescaped_payload)
130
+ end
131
+ end
107
132
  end
108
133
  end
109
134
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ require 'rspec'
2
2
  require 'simplecov'
3
3
  SimpleCov.start
4
4
 
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-masscan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Postmodern
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-10 00:00:00.000000000 Z
11
+ date: 2023-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rprogram
14
+ name: command_mapper
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.3'
19
+ version: '0.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.3'
26
+ version: '0.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -62,6 +62,7 @@ files:
62
62
  - gemspec.yml
63
63
  - lib/masscan.rb
64
64
  - lib/masscan/banner.rb
65
+ - lib/masscan/command.rb
65
66
  - lib/masscan/output_file.rb
66
67
  - lib/masscan/parsers.rb
67
68
  - lib/masscan/parsers/binary.rb
@@ -70,9 +71,9 @@ files:
70
71
  - lib/masscan/parsers/plain_text.rb
71
72
  - lib/masscan/program.rb
72
73
  - lib/masscan/status.rb
73
- - lib/masscan/task.rb
74
74
  - lib/masscan/version.rb
75
75
  - ruby-masscan.gemspec
76
+ - spec/command_spec.rb
76
77
  - spec/fixtures/masscan.bin
77
78
  - spec/fixtures/masscan.json
78
79
  - spec/fixtures/masscan.list
@@ -85,7 +86,6 @@ files:
85
86
  - spec/parsers/parser_examples.rb
86
87
  - spec/parsers/plain_text_spec.rb
87
88
  - spec/spec_helper.rb
88
- - spec/task_spec.rb
89
89
  homepage: https://github.com/postmodern/ruby-masscan#readme
90
90
  licenses:
91
91
  - MIT
@@ -94,6 +94,7 @@ metadata:
94
94
  source_code_uri: https://github.com/postmodern/ruby-masscan
95
95
  bug_tracker_uri: https://github.com/postmodern/ruby-masscan/issues
96
96
  changelog_uri: https://github.com/postmodern/ruby-masscan/blob/master/ChangeLog.md
97
+ rubygems_mfa_required: 'true'
97
98
  post_install_message:
98
99
  rdoc_options: []
99
100
  require_paths:
@@ -110,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
111
  version: '0'
111
112
  requirements:
112
113
  - masscan >= 1.0.0
113
- rubygems_version: 3.2.22
114
+ rubygems_version: 3.3.26
114
115
  signing_key:
115
116
  specification_version: 4
116
117
  summary: A Ruby interface to masscan.
data/lib/masscan/task.rb DELETED
@@ -1,179 +0,0 @@
1
- require 'rprogram/task'
2
-
3
- module Masscan
4
- #
5
- # ## `masscan` options:
6
- #
7
- # * `--range` - `masscan.range`
8
- # * `-p` - `masscan.ports`
9
- # * `--banners` - `masscan.banners`
10
- # * `--rate` - `masscan.rate`
11
- # * `--conf` - `masscan.config_file`
12
- # * `--resume` - `masscan.resume`
13
- # * `--echo` - `masscan.echo`
14
- # * `--adapter` - `masscan.adapter`
15
- # * `--adapter-ip` - `masscan.adapter_ip`
16
- # * `--adapter-port` - `masscan.adapter_port`
17
- # * `--adapter-mac` - `masscan.adapter_mac`
18
- # * `--adapter-vlan` - `masscan.adapter_vlan`
19
- # * `--router-mac` - `masscan.router_mac`
20
- # * `--ping` - `masscan.ping`
21
- # * `--exclude` - `masscan.exclude`
22
- # * `--excludefile` - `masscan.exclude_file`
23
- # * `--includefile` - `masscan.include_file`
24
- # * `--append-output` - `masscan.append_output`
25
- # * `--iflist` - `masscan.list_interfaces`
26
- # * `--retries` - `masscan.retries`
27
- # * `--nmap` - `masscan.nmap_help`
28
- # * `--pcap-payloads` - `masscan.pcap_payloads`
29
- # * `--nmap-payloads` - `masscan.nmap_payloads`
30
- # * `--http-method` - `masscan.http_method`
31
- # * `--http-url` - `masscan.http_url`
32
- # * `--http-version` - `masscan.http_version`
33
- # * `--http-host` - `masscan.http_host`
34
- # * `--http-user-agent` - `masscan.http_user_agent`
35
- # * `--http-field` - `masscan.http_field`
36
- # * `--http-field-remove` - `masscan.http_field_remove`
37
- # * `--http-cookie` - `masscan.http_cookie`
38
- # * `--http-payload` - `masscan.http_payload`
39
- # * `--show` - `masscan.show`
40
- # * `--noshow` - `masscan.hide`
41
- # * `--pcap` - `masscan.pcap`
42
- # * `--packet-trace` - `masscan.packet_trace`
43
- # * `--pfring` - `masscan.pfring`
44
- # * `--resume-index` - `masscan.resume_index`
45
- # * `--resume-count` - `masscan.resume_count`
46
- # * `--shards` - `masscan.shards`
47
- # * `--rotate` - `masscan.rotate`
48
- # * `--rotate-offset` - `masscan.rotate_offset`
49
- # * `--rotate-size` - `masscan.rotate_size`
50
- # * `--rotate-dir` - `masscan.rotate_dir`
51
- # * `--seed` - `masscan.seed`
52
- # * `--regress` - `masscan.regress`
53
- # * `--ttl` - `masscan.ttl`
54
- # * `--wait` - `masscan.wait`
55
- # * `--offline` - `masscan.offline`
56
- # * `-sL` - `masscan.print_list`
57
- # * `--interactive` - `masscan.interactive`
58
- # * `--output-format` - `masscan.output_format`
59
- # * `--output-filename` - `masscan.output_file`
60
- # * `-oB` - `masscan.output_binary`
61
- # * `-oX` - `masscan.output_xml`
62
- # * `-oG` - `masscan.output_grepable`
63
- # * ` -oJ` - `masscan.output_json`
64
- # * `-oL` - `masscan.output_list`
65
- # * `--readscan` - `masscan.read_scan`
66
- # * `-V` - `masscan.version`
67
- # * `-h` - `masscan.help`
68
- #
69
- # @see https://github.com/robertdavidgraham/masscan/blob/master/doc/masscan.8.markdown
70
- #
71
- class Task < RProgram::Task
72
-
73
- long_option flag: '--range', name: :range, separator: ','
74
- short_option flag: '-p', name: :ports do |opt,value|
75
- unless value.empty?
76
- [opt.flag, format_port_list(value)]
77
- end
78
- end
79
- long_option flag: '--banners', name: :banners
80
- long_option flag: '--rate', name: :rate
81
- long_option flag: '--conf', name: :config_file
82
- long_option flag: '--resume', name: :resume
83
- long_option flag: '--echo', name: :echo
84
- long_option flag: '--adapter', name: :adapter
85
- long_option flag: '--adapter-ip', name: :adapter_ip
86
- long_option flag: '--adapter-port', name: :adapter_port
87
- long_option flag: '--adapter-mac', name: :adapter_mac
88
- long_option flag: '--adapter-vlan', name: :adapter_vlan
89
- long_option flag: '--router-mac', name: :router_mac
90
- long_option flag: '--ping', name: :ping
91
- long_option flag: '--exclude', name: :exclude, separator: ','
92
- long_option flag: '--excludefile', name: :exclude_file
93
- long_option flag: '--includefile', name: :include_file
94
- long_option flag: '--append-output', name: :append_output
95
- long_option flag: '--iflist', name: :list_interfaces
96
- long_option flag: '--retries', name: :retries
97
- long_option flag: '--nmap', name: :nmap_help
98
- long_option flag: '--pcap-payloads', name: :pcap_payloads
99
- long_option flag: '--nmap-payloads', name: :nmap_payloads
100
-
101
- long_option flag: '--http-method', name: :http_method
102
- long_option flag: '--http-url', name: :http_url
103
- long_option flag: '--http-version', name: :http_version
104
- long_option flag: '--http-host', name: :http_host
105
- long_option flag: '--http-user-agent', name: :http_user_agent
106
-
107
- long_option flag: '--http-field', multiple: true do |opt,value|
108
- name, value = value.first
109
-
110
- [opt.flag, "#{name}:#{value}"]
111
- end
112
-
113
- long_option flag: '--http-field-remove', name: :http_field_remove
114
- long_option flag: '--http-cookie', name: :http_cookie
115
- long_option flag: '--http-payload', name: :http_payload
116
-
117
- long_option flag: '--show', name: :show
118
- long_option flag: '--noshow', name: :hide
119
- long_option flag: '--pcap', name: :pcap
120
- long_option flag: '--packet-trace', name: :packet_trace
121
- long_option flag: '--pfring', name: :pfring
122
- long_option flag: '--resume-index', name: :resume_index
123
- long_option flag: '--resume-count', name: :resume_count
124
- long_option flag: '--shards', name: :shards do |opt,value|
125
- case value.length
126
- when 2 then [opt.flag, "#{value[0]}/#{value[1]}"]
127
- when 1 then [opt.flag, "#{value[0]}"]
128
- else
129
- raise(ArgumentError,"#{self}#shards= does not accept more than two values")
130
- end
131
- end
132
- long_option flag: '--rotate', name: :rotate
133
- long_option flag: '--rotate-offset', name: :rotate_offset
134
- long_option flag: '--rotate-size', name: :rotate_size
135
- long_option flag: '--rotate-dir', name: :rotate_dir
136
- long_option flag: '--seed', name: :seed
137
- long_option flag: '--regress', name: :regress
138
- long_option flag: '--ttl', name: :ttl
139
- long_option flag: '--wait', name: :wait
140
- long_option flag: '--offline', name: :offline
141
- short_option flag: '-sL', name: :print_list
142
- long_option flag: '--interactive', name: :interactive
143
- long_option flag: '--output-format', name: :output_format
144
- long_option flag: '--output-filename', name: :output_file
145
- short_option flag: '-oB', name: :output_binary
146
- short_option flag: '-oX', name: :output_xml
147
- short_option flag: '-oG', name: :output_grepable
148
- short_option flag:' -oJ', name: :output_json
149
- short_option flag: '-oL', name: :output_list
150
- long_option flag: '--readscan', name: :read_scan
151
- short_option :flag => '-V', :name => :version
152
- short_option :flag => '-h', :name => :help
153
-
154
- non_option :tailing => true, :name => :ips
155
-
156
- private
157
-
158
- #
159
- # Formats a port list.
160
- #
161
- # @param [Array<Integer,Range>] ports
162
- # The port ranges.
163
- #
164
- # @return [String]
165
- # Comma separated string.
166
- #
167
- def self.format_port_list(ports)
168
- ports.map { |port|
169
- case port
170
- when Range
171
- "#{port.first}-#{port.last}"
172
- else
173
- port.to_s
174
- end
175
- }.join(',')
176
- end
177
-
178
- end
179
- end
data/spec/task_spec.rb DELETED
@@ -1,121 +0,0 @@
1
- require 'spec_helper'
2
- require 'masscan/task'
3
-
4
- describe Masscan::Task do
5
- describe "#ports=" do
6
- context "when given an empty Array" do
7
- before { subject.ports = [] }
8
-
9
- it "should ignore empty port Arrays" do
10
- subject.ports = []
11
-
12
- expect(subject.arguments).to eq([])
13
- end
14
- end
15
-
16
- context "when given a String" do
17
- let(:ports) { '80,21,25' }
18
-
19
- before { subject.ports = ports }
20
-
21
- it "should emit the String as is" do
22
- expect(subject.arguments).to eq(['-p', ports])
23
- end
24
- end
25
-
26
- context "when given an Array of Strings" do
27
- let(:ports) { %w[80 21 25] }
28
-
29
- before { subject.ports = ports }
30
-
31
- it "should format an Array of String ports" do
32
- expect(subject.arguments).to eq(['-p', ports.join(',')])
33
- end
34
- end
35
-
36
- context "when given an Array of Integers" do
37
- let(:ports) { [80, 21, 25] }
38
-
39
- before { subject.ports = ports }
40
-
41
- it "should format an Array of Integer ports" do
42
- expect(subject.arguments).to eq(['-p', ports.join(',')])
43
- end
44
- end
45
-
46
- context "when given an Array containing a Range" do
47
- let(:ports) { [80, 21..25] }
48
-
49
- before { subject.ports = ports }
50
-
51
- it "should format the Range" do
52
- expect(subject.arguments).to eq([
53
- '-p', "#{ports[0]},#{ports[1].begin}-#{ports[1].end}"
54
- ])
55
- end
56
- end
57
- end
58
-
59
- describe "#shards=" do
60
- context "when given a Rational value" do
61
- let(:rational) { (1/2r) }
62
-
63
- before { subject.shards = rational }
64
-
65
- it "must format it into \#{numerator}/\#{denominator}" do
66
- expect(subject.arguments).to eq([
67
- "--shards", "#{rational.numerator}/#{rational.denominator}"
68
- ])
69
- end
70
- end
71
-
72
- context "when given an Array value" do
73
- let(:array) { [1, 2] }
74
-
75
- before { subject.shards = array }
76
-
77
- it "must format it into \#{array[0]}/\#{array[1]}" do
78
- expect(subject.arguments).to eq([
79
- "--shards", "#{array[0]}/#{array[1]}"
80
- ])
81
- end
82
-
83
- context "but the Array length is > 2" do
84
- let(:array) { [1,2,3] }
85
-
86
- before { subject.shards = array }
87
-
88
- it do
89
- expect {
90
- subject.arguments
91
- }.to raise_error(ArgumentError,"#{described_class}#shards= does not accept more than two values")
92
- end
93
- end
94
- end
95
-
96
- context "otherwise" do
97
- let(:object) { :"1/2" }
98
-
99
- before { subject.shards = object }
100
-
101
- it "must convert the value to a String" do
102
- expect(subject.arguments).to eq([
103
- "--shards", object.to_s
104
- ])
105
- end
106
- end
107
- end
108
-
109
- describe "#http_field=" do
110
- let(:name) { 'X-Foo' }
111
- let(:value) { 'bar' }
112
-
113
- before { subject.http_field = [ [name, value] ] }
114
-
115
- it "must join two values together with a ':'" do
116
- expect(subject.arguments).to eq([
117
- "--http-field", "#{name}:#{value}"
118
- ])
119
- end
120
- end
121
- end