ruby-masscan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|