ruby-masscan 0.1.0
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 +7 -0
- data/.document +3 -0
- data/.editorconfig +11 -0
- data/.github/workflows/ruby.yml +29 -0
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +20 -0
- data/README.md +139 -0
- data/Rakefile +23 -0
- data/gemspec.yml +28 -0
- data/lib/masscan/banner.rb +11 -0
- data/lib/masscan/output_file.rb +100 -0
- data/lib/masscan/parsers/binary.rb +591 -0
- data/lib/masscan/parsers/json.rb +106 -0
- data/lib/masscan/parsers/list.rb +84 -0
- data/lib/masscan/parsers/plain_text.rb +151 -0
- data/lib/masscan/parsers.rb +3 -0
- data/lib/masscan/program.rb +100 -0
- data/lib/masscan/status.rb +7 -0
- data/lib/masscan/task.rb +179 -0
- data/lib/masscan/version.rb +4 -0
- data/lib/masscan.rb +2 -0
- data/ruby-masscan.gemspec +61 -0
- data/spec/fixtures/masscan.bin +0 -0
- data/spec/fixtures/masscan.json +17 -0
- data/spec/fixtures/masscan.list +10 -0
- data/spec/fixtures/masscan.ndjson +8 -0
- data/spec/fixtures/masscan.xml +17 -0
- data/spec/output_file_spec.rb +135 -0
- data/spec/parsers/binary_spec.rb +224 -0
- data/spec/parsers/json_spec.rb +157 -0
- data/spec/parsers/list_spec.rb +109 -0
- data/spec/parsers/parser_examples.rb +58 -0
- data/spec/parsers/plain_text_spec.rb +116 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/task_spec.rb +121 -0
- metadata +117 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'masscan/parsers/plain_text'
|
2
|
+
require 'masscan/status'
|
3
|
+
require 'masscan/banner'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Masscan
|
8
|
+
module Parsers
|
9
|
+
#
|
10
|
+
# Parses the `masscan -oJ` and `masscan --output-format ndjson` output
|
11
|
+
# formats.
|
12
|
+
#
|
13
|
+
# @api semipublic
|
14
|
+
#
|
15
|
+
module JSON
|
16
|
+
extend PlainText
|
17
|
+
|
18
|
+
#
|
19
|
+
# Opens a JSON file for parsing.
|
20
|
+
#
|
21
|
+
# @param [String] path
|
22
|
+
# The path to the file.
|
23
|
+
#
|
24
|
+
# @yield [file]
|
25
|
+
# If a block is given, it will be passed the opened file.
|
26
|
+
# Once the block returns, the file will be closed.
|
27
|
+
#
|
28
|
+
# @yieldparam [File] file
|
29
|
+
# The opened file.
|
30
|
+
#
|
31
|
+
# @return [File]
|
32
|
+
# If no block was given, the opened file will be returned.
|
33
|
+
#
|
34
|
+
def self.open(path,&block)
|
35
|
+
File.open(path,&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Parses the masscan JSON or ndjson data.
|
40
|
+
#
|
41
|
+
# @param [#each_line] io
|
42
|
+
# The IO object to read from.
|
43
|
+
#
|
44
|
+
# @yield [record]
|
45
|
+
# If a block is given, it will be passed each parsed record.
|
46
|
+
#
|
47
|
+
# @yieldparam [Status, Banner] record
|
48
|
+
# A parsed record, either a {Status} or a {Banner} object.
|
49
|
+
#
|
50
|
+
# @return [Enumerator]
|
51
|
+
# If no block is given, it will return an Enumerator.
|
52
|
+
#
|
53
|
+
def self.parse(io)
|
54
|
+
return enum_for(__method__,io) unless block_given?
|
55
|
+
|
56
|
+
io.each_line do |line|
|
57
|
+
line.chomp!
|
58
|
+
|
59
|
+
if line == "," || line == "[" || line == "]"
|
60
|
+
# skip
|
61
|
+
else
|
62
|
+
json = ::JSON.parse(line)
|
63
|
+
|
64
|
+
ip = parse_ip(json['ip'])
|
65
|
+
timestamp = parse_timestamp(json['timestamp'])
|
66
|
+
|
67
|
+
if (ports_json = json['ports'])
|
68
|
+
if (port_json = ports_json.first)
|
69
|
+
proto = parse_ip_protocol(port_json['proto'])
|
70
|
+
port = port_json['port']
|
71
|
+
|
72
|
+
if (service_json = port_json['service'])
|
73
|
+
service_name = parse_app_protocol(service_json['name'])
|
74
|
+
service_banner = service_json['banner']
|
75
|
+
|
76
|
+
yield Banner.new(
|
77
|
+
proto,
|
78
|
+
port,
|
79
|
+
ip,
|
80
|
+
timestamp,
|
81
|
+
service_name,
|
82
|
+
service_banner
|
83
|
+
)
|
84
|
+
else
|
85
|
+
status = parse_status(port_json['status'])
|
86
|
+
ttl = port_json['ttl']
|
87
|
+
reason = parse_reason(port_json['reason'])
|
88
|
+
|
89
|
+
yield Status.new(
|
90
|
+
status,
|
91
|
+
proto,
|
92
|
+
port,
|
93
|
+
reason,
|
94
|
+
ttl,
|
95
|
+
ip,
|
96
|
+
timestamp
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'masscan/parsers/plain_text'
|
2
|
+
require 'masscan/status'
|
3
|
+
require 'masscan/banner'
|
4
|
+
|
5
|
+
module Masscan
|
6
|
+
module Parsers
|
7
|
+
#
|
8
|
+
# Parses the `masscan -oL` output format.
|
9
|
+
#
|
10
|
+
# @api semipublic
|
11
|
+
#
|
12
|
+
module List
|
13
|
+
extend PlainText
|
14
|
+
|
15
|
+
#
|
16
|
+
# Opens a list file for parsing.
|
17
|
+
#
|
18
|
+
# @param [String] path
|
19
|
+
# The path to the file.
|
20
|
+
#
|
21
|
+
# @yield [file]
|
22
|
+
# If a block is given, it will be passed the opened file.
|
23
|
+
# Once the block returns, the file will be closed.
|
24
|
+
#
|
25
|
+
# @yieldparam [File] file
|
26
|
+
# The opened file.
|
27
|
+
#
|
28
|
+
# @return [File]
|
29
|
+
# If no block was given, the opened file will be returned.
|
30
|
+
#
|
31
|
+
def self.open(path,&block)
|
32
|
+
File.open(path,&block)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Parses the masscan simple list data.
|
37
|
+
#
|
38
|
+
# @param [#each_line] io
|
39
|
+
# The IO object to read from.
|
40
|
+
#
|
41
|
+
# @yield [record]
|
42
|
+
# If a block is given, it will be passed each parsed record.
|
43
|
+
#
|
44
|
+
# @yieldparam [Status, Banner] record
|
45
|
+
# A parsed record, either a {Status} or a {Banner} object.
|
46
|
+
#
|
47
|
+
# @return [Enumerator]
|
48
|
+
# If no block is given, it will return an Enumerator.
|
49
|
+
#
|
50
|
+
def self.parse(io)
|
51
|
+
return enum_for(__method__,io) unless block_given?
|
52
|
+
|
53
|
+
io.each_line do |line|
|
54
|
+
line.chomp!
|
55
|
+
|
56
|
+
if line.start_with?('open ') || line.start_with?('closed ')
|
57
|
+
type, ip_proto, port, ip, timestamp = line.split(' ',5)
|
58
|
+
|
59
|
+
yield Status.new(
|
60
|
+
parse_status(type),
|
61
|
+
parse_ip_protocol(ip_proto),
|
62
|
+
port.to_i,
|
63
|
+
nil,
|
64
|
+
nil,
|
65
|
+
parse_ip(ip),
|
66
|
+
parse_timestamp(timestamp)
|
67
|
+
)
|
68
|
+
elsif line.start_with?('banner ')
|
69
|
+
type, ip_proto, port, ip, timestamp, app_proto, banner = line.split(' ',7)
|
70
|
+
|
71
|
+
yield Banner.new(
|
72
|
+
parse_ip_protocol(ip_proto),
|
73
|
+
port.to_i,
|
74
|
+
parse_ip(ip),
|
75
|
+
parse_timestamp(timestamp),
|
76
|
+
parse_app_protocol(app_proto),
|
77
|
+
banner
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Masscan
|
2
|
+
module Parsers
|
3
|
+
#
|
4
|
+
# Common methods for parsing plain-text data.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
module PlainText
|
9
|
+
# Mapping of status strings to their keywords.
|
10
|
+
STATUSES = {
|
11
|
+
'open' => :open,
|
12
|
+
'closed' => :closed
|
13
|
+
}
|
14
|
+
|
15
|
+
#
|
16
|
+
# Parses a status string.
|
17
|
+
#
|
18
|
+
# @param [String] status
|
19
|
+
# The status string to parse.
|
20
|
+
#
|
21
|
+
# @return [:open, :closed, String]
|
22
|
+
# The status keyword or a String if the status wasn't in {STATUSES}.
|
23
|
+
#
|
24
|
+
def parse_status(status)
|
25
|
+
STATUSES[status] || status
|
26
|
+
end
|
27
|
+
|
28
|
+
REASONS = {
|
29
|
+
'fin' => :fin,
|
30
|
+
'syn' => :syn,
|
31
|
+
'rst' => :rst,
|
32
|
+
'psh' => :psh,
|
33
|
+
'ack' => :ack,
|
34
|
+
'urg' => :urg,
|
35
|
+
'ece' => :ece,
|
36
|
+
'cwr' => :cwr
|
37
|
+
}
|
38
|
+
|
39
|
+
#
|
40
|
+
# Parses a reason string.
|
41
|
+
#
|
42
|
+
# @param [String] reason
|
43
|
+
# The reason string to parse.
|
44
|
+
#
|
45
|
+
# @return [Array<:fin, :syn, :rst, :psh, :ack, :urg, :ece, :cwr>]
|
46
|
+
# The reason keywords or a String if the flag wasn't in {REASONS}.
|
47
|
+
#
|
48
|
+
def parse_reason(reason)
|
49
|
+
flags = reason.split('-')
|
50
|
+
flags.map! { |flag| REASONS[flag] || flag }
|
51
|
+
flags
|
52
|
+
end
|
53
|
+
|
54
|
+
# Mapping of IP protocol names to their keywords.
|
55
|
+
IP_PROTOCOLS = {
|
56
|
+
'tcp' => :tcp,
|
57
|
+
'udp' => :udp,
|
58
|
+
'icmp' => :icmp,
|
59
|
+
'sctp' => :sctp
|
60
|
+
}
|
61
|
+
|
62
|
+
#
|
63
|
+
# Parses an IP protocol name.
|
64
|
+
#
|
65
|
+
# @param [String] proto
|
66
|
+
# The IP protocol name.
|
67
|
+
#
|
68
|
+
# @return [:tcp, :udp, :icmp, :sctp, String]
|
69
|
+
# The IP protocol keyword or a String if the IP protocol wasn't in
|
70
|
+
# {IP_PROTOCOLS}.
|
71
|
+
#
|
72
|
+
def parse_ip_protocol(proto)
|
73
|
+
IP_PROTOCOLS[proto] || proto
|
74
|
+
end
|
75
|
+
|
76
|
+
# Mapping of application protocol names to their keywords.
|
77
|
+
APP_PROTOCOLS = {
|
78
|
+
"ssh1" => :ssh1,
|
79
|
+
"ssh2" => :ssh2,
|
80
|
+
"ssh" => :ssh,
|
81
|
+
"http" => :http,
|
82
|
+
"ftp" => :ftp,
|
83
|
+
"dns-ver" => :dns_ver,
|
84
|
+
"snmp" => :smtp,
|
85
|
+
"nbtstat" => :nbtstat,
|
86
|
+
"ssl" => :ssl3,
|
87
|
+
"smtp" => :smtp,
|
88
|
+
"smb" => :smb,
|
89
|
+
"pop" => :pop,
|
90
|
+
"imap" => :imap,
|
91
|
+
"X509" => :x509,
|
92
|
+
"zeroaccess" => :zeroaccess,
|
93
|
+
"title" => :html_title,
|
94
|
+
"html" => :html,
|
95
|
+
"ntp" => :ntp,
|
96
|
+
"vuln" => :vuln,
|
97
|
+
"heartbleed" => :heartbleed,
|
98
|
+
"ticketbleed" => :ticketbleed,
|
99
|
+
"vnc" => :vnc,
|
100
|
+
"safe" => :safe,
|
101
|
+
"memcached" => :memcached,
|
102
|
+
"scripting" => :scripting,
|
103
|
+
"versioning" => :versioning,
|
104
|
+
"coap" => :coap,
|
105
|
+
"telnet" => :telnet,
|
106
|
+
"rdp" => :rdp,
|
107
|
+
"http.server" => :http_server
|
108
|
+
}
|
109
|
+
|
110
|
+
#
|
111
|
+
# Parses an application protocol name.
|
112
|
+
#
|
113
|
+
# @param [String] proto
|
114
|
+
# The application protocol name.
|
115
|
+
#
|
116
|
+
# @return [Symbol, String]
|
117
|
+
# The IP protocol keyword or a String if the application protocol wasn't
|
118
|
+
# in {APP_PROTOCOLS}.
|
119
|
+
#
|
120
|
+
def parse_app_protocol(proto)
|
121
|
+
APP_PROTOCOLS[proto] || proto
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Parses a timestamp.
|
126
|
+
#
|
127
|
+
# @param [String] timestamp
|
128
|
+
# The numeric timestamp value.
|
129
|
+
#
|
130
|
+
# @return [Time]
|
131
|
+
# The parsed timestamp value.
|
132
|
+
#
|
133
|
+
def parse_timestamp(timestamp)
|
134
|
+
Time.at(timestamp.to_i)
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# Parses an IP address.
|
139
|
+
#
|
140
|
+
# @param [String] ip
|
141
|
+
# The string representation of the IP address.
|
142
|
+
#
|
143
|
+
# @return [IPAddr]
|
144
|
+
# The parsed IP address.
|
145
|
+
#
|
146
|
+
def parse_ip(ip)
|
147
|
+
IPAddr.new(ip)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'masscan/task'
|
2
|
+
|
3
|
+
require 'rprogram/program'
|
4
|
+
|
5
|
+
module Masscan
|
6
|
+
#
|
7
|
+
# Represents the `masscan` program.
|
8
|
+
#
|
9
|
+
class Program < RProgram::Program
|
10
|
+
|
11
|
+
name_program 'masscan'
|
12
|
+
|
13
|
+
#
|
14
|
+
# Finds the `masscan` program and performs a scan.
|
15
|
+
#
|
16
|
+
# @param [Hash{Symbol => Object}] options
|
17
|
+
# Additional options for masscan.
|
18
|
+
#
|
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
|
24
|
+
# used to specify options for masscan.
|
25
|
+
#
|
26
|
+
# @yieldparam [Task] task
|
27
|
+
# The masscan task object.
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
# Specifies whether the command exited normally.
|
31
|
+
#
|
32
|
+
# @example Specifying `masscan` options via a Hash.
|
33
|
+
# Masscan::Program.scan(
|
34
|
+
# ips: '192.168.1.1/24',
|
35
|
+
# ports: [22, 80, 443],
|
36
|
+
# )
|
37
|
+
#
|
38
|
+
# @example Specifying `masscan` options via a {Task} object.
|
39
|
+
# Masscan::Program.scan do |masscan|
|
40
|
+
# masscan.ips = '192.168.1.1/24'
|
41
|
+
# masscan.ports = [22, 80, 443]
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# @see #scan
|
45
|
+
#
|
46
|
+
def self.scan(options={},exec_options={},&block)
|
47
|
+
find.scan(options,exec_options,&block)
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Finds the `masscan` program and performs a scan, but runs `masscan` under
|
52
|
+
# `sudo`.
|
53
|
+
#
|
54
|
+
# @see scan
|
55
|
+
#
|
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)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|