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.
@@ -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,3 @@
1
+ require 'masscan/parsers/binary'
2
+ require 'masscan/parsers/list'
3
+ require 'masscan/parsers/json'
@@ -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