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,224 @@
1
+ require 'spec_helper'
2
+ require_relative 'parser_examples'
3
+
4
+ require 'masscan/parsers/binary'
5
+ require 'stringio'
6
+
7
+ describe Masscan::Parsers::Binary do
8
+ let(:path) { Fixtures.join('masscan.bin') }
9
+
10
+ describe ".open" do
11
+ include_examples "Parser.open"
12
+
13
+ it "must open the file in binary mode" do
14
+ file = subject.open(path)
15
+
16
+ expect(file.binmode?).to be(true)
17
+ end
18
+ end
19
+
20
+ let(:io) { subject.open(path) }
21
+
22
+ describe ".parse" do
23
+ include_examples "Parser.parse"
24
+ end
25
+
26
+ describe "PSEUDO_RECORD_SIZE" do
27
+ subject { super()::PSEUDO_RECORD_SIZE }
28
+
29
+ it "must be 99 ('a'.ord + 2)" do
30
+ expect(subject).to eq('a'.ord + 2)
31
+ end
32
+ end
33
+
34
+ describe "MASSCAN_MAGIC" do
35
+ subject { super()::MASSCAN_MAGIC }
36
+
37
+ it "must be 'masscan/1.1'" do
38
+ expect(subject).to eq("masscan/1.1")
39
+ end
40
+ end
41
+
42
+ describe ".read_pseudo_record" do
43
+ let(:io) { StringIO.new(buffer) }
44
+
45
+ let(:pseudo_record_size) { subject::PSEUDO_RECORD_SIZE }
46
+
47
+ context "when the read buffer length is < PSEUDO_RECORD_SIZE" do
48
+ let(:buffer) { "\0" * (pseudo_record_size - 3) }
49
+
50
+ it do
51
+ expect {
52
+ subject.read_pseudo_record(io)
53
+ }.to raise_error(subject::CorruptedFile,"invalid masscan binary format")
54
+ end
55
+ end
56
+
57
+ context "when the read buffer length is >= PSEUDO_RECORD_SIZE" do
58
+ let(:masscan_magic) { subject::MASSCAN_MAGIC }
59
+
60
+ context "but does not start with MASSCAN_MAGIC string (masscan/1.1)" do
61
+ let(:buffer) { "\0" * pseudo_record_size }
62
+
63
+ it do
64
+ expect {
65
+ subject.read_pseudo_record(io)
66
+ }.to raise_error(subject::CorruptedFile,"unknown file format (expected #{masscan_magic})")
67
+ end
68
+ end
69
+
70
+ context "and does start with MASSCAN_MAGIC" do
71
+ let(:buffer) do
72
+ buffer = "\0" * (pseudo_record_size + 1024)
73
+ buffer[0,masscan_magic.length] = masscan_magic
74
+ buffer
75
+ end
76
+
77
+ it "must return the read buffer with length of PSEUDO_RECORD_SIZE" do
78
+ pseudo_record = subject.read_pseudo_record(io)
79
+
80
+ expect(pseudo_record.length).to eq(pseudo_record_size)
81
+ expect(pseudo_record).to eq(buffer[0,pseudo_record_size])
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ describe ".decode_timestamp" do
88
+ let(:timestamp) { 1629960470 }
89
+ let(:time) { Time.at(timestamp) }
90
+
91
+ it "must convert the UNIX timestamp into a Time object" do
92
+ expect(subject.decode_timestamp(timestamp)).to eq(time)
93
+ end
94
+ end
95
+
96
+ describe ".decode_ipv4" do
97
+ let(:ipaddr) { IPAddr.new("1.2.3.4") }
98
+ let(:ip_uint) { ipaddr.to_i }
99
+
100
+ it "must convert a IPv4 address in uint form into an IPAddr object" do
101
+ expect(subject.decode_ipv4(ip_uint)).to eq(ipaddr)
102
+ end
103
+ end
104
+
105
+ describe ".decode_ipv6" do
106
+ let(:ipaddr) { IPAddr.new("2606:2800:220:1:248:1893:25c8:1946") }
107
+ let(:ip_uint_hi) { (ipaddr.to_i & (0xffffffff_ffffffff << 64)) >> 64 }
108
+ let(:ip_uint_lo) { (ipaddr.to_i & 0xffffffff_ffffffff) }
109
+
110
+ it "must combine the hi and lo 64bit uints of an IPv6 address into an IPAddr object" do
111
+ expect(subject.decode_ipv6(ip_uint_hi,ip_uint_lo)).to eq(ipaddr)
112
+ end
113
+ end
114
+
115
+ describe ".lookup_ip_protocol" do
116
+ context "when given 1 (IPPROTO_ICMP)" do
117
+ it "must reutrn :icmp" do
118
+ expect(subject.lookup_ip_protocol(1)).to be(:icmp)
119
+ end
120
+ end
121
+
122
+ context "when given 58 (IPPROTO_ICMPV6)" do
123
+ it "must reutrn :icmp" do
124
+ expect(subject.lookup_ip_protocol(58)).to be(:icmp)
125
+ end
126
+ end
127
+
128
+ context "when given 6 (IPPROTO_TCP)" do
129
+ it "must reutrn :tcp" do
130
+ expect(subject.lookup_ip_protocol(6)).to be(:tcp)
131
+ end
132
+ end
133
+
134
+ context "when given 6 (IPPROTO_UDP)" do
135
+ it "must reutrn :udp" do
136
+ expect(subject.lookup_ip_protocol(17)).to be(:udp)
137
+ end
138
+ end
139
+
140
+ context "when given 132 (IPPROTO_SCTP)" do
141
+ it "must reutrn :udp" do
142
+ expect(subject.lookup_ip_protocol(132)).to be(:sctp)
143
+ end
144
+ end
145
+ end
146
+
147
+ describe ".decode_reason" do
148
+ context "when given 0" do
149
+ it "must return []" do
150
+ expect(subject.decode_reason(0)).to eq([])
151
+ end
152
+ end
153
+
154
+ {
155
+ fin: 0x01,
156
+ syn: 0x02,
157
+ rst: 0x04,
158
+ psh: 0x08,
159
+ ack: 0x10,
160
+ urg: 0x20,
161
+ ece: 0x40,
162
+ cwr: 0x80
163
+ }.each do |reason,bitflag|
164
+ context "when given an integer with the #{"0x%x" % bitflag} bit set" do
165
+ it "must include the #{reason.inspect} flag" do
166
+ expect(subject.decode_reason(bitflag)).to eq([reason])
167
+ end
168
+ end
169
+ end
170
+
171
+ context "when given an integer containing multiple bits set" do
172
+ it "must return the associated reason flags" do
173
+ expect(subject.decode_reason(0x02 | 0x10)).to eq([:syn, :ack])
174
+ end
175
+ end
176
+ end
177
+
178
+ describe ".lookup_app_protocol" do
179
+ context "when given 0" do
180
+ it "must return nil" do
181
+ expect(subject.lookup_app_protocol(0)).to be(nil)
182
+ end
183
+ end
184
+
185
+ {
186
+ 1 => :heur,
187
+ 2 => :ssh1,
188
+ 3 => :ssh2,
189
+ 4 => :http,
190
+ 5 => :ftp,
191
+ 6 => :dns_versionbind,
192
+ 7 => :snmp,
193
+ 8 => :nbtstat,
194
+ 9 => :ssl3,
195
+ 10 => :smb,
196
+ 11 => :smtp,
197
+ 12 => :pop3,
198
+ 13 => :imap4,
199
+ 14 => :udp_zeroaccess,
200
+ 15 => :x509_cert,
201
+ 16 => :html_title,
202
+ 17 => :html_full,
203
+ 18 => :ntp,
204
+ 19 => :vuln,
205
+ 20 => :heartbleed,
206
+ 21 => :ticketbleed,
207
+ 22 => :vnc_rfb,
208
+ 23 => :safe,
209
+ 24 => :memcached,
210
+ 25 => :scripting,
211
+ 26 => :versioning,
212
+ 27 => :coap,
213
+ 28 => :telnet,
214
+ 29 => :rdp,
215
+ 30 => :http_server
216
+ }.each do |index,keyword|
217
+ context "when given #{index}" do
218
+ it "must return #{keyword.inspect} keyword" do
219
+ expect(subject.lookup_app_protocol(index)).to be(keyword)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+ require_relative 'parser_examples'
3
+
4
+ require 'masscan/parsers/json'
5
+ require 'stringio'
6
+
7
+ describe Masscan::Parsers::JSON do
8
+ let(:path) { Fixtures.join('masscan.json') }
9
+
10
+ describe ".open" do
11
+ include_examples "Parser.open"
12
+ end
13
+
14
+ let(:io) { subject.open(path) }
15
+
16
+ describe ".parse" do
17
+ include_examples "Parser.parse"
18
+
19
+ context "when the line is a '[' character" do
20
+ let(:lines) do
21
+ [
22
+ "[",
23
+ %{{ "ip": "93.184.216.34", "timestamp": "1629960621", "ports": [ {"port": 80, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 54} ] }}
24
+ ]
25
+ end
26
+
27
+ let(:io) { StringIO.new(lines.join("\n")) }
28
+
29
+ it "must skip it" do
30
+ yielded_records = []
31
+
32
+ subject.parse(io) do |record|
33
+ yielded_records << record
34
+ end
35
+
36
+ expect(yielded_records.length).to eq(1)
37
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
38
+ end
39
+ end
40
+
41
+ context "when the line is a ',' character" do
42
+ let(:lines) do
43
+ [
44
+ ",",
45
+ %{{ "ip": "93.184.216.34", "timestamp": "1629960621", "ports": [ {"port": 80, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 54} ] }}
46
+ ]
47
+ end
48
+
49
+ let(:io) { StringIO.new(lines.join("\n")) }
50
+
51
+ it "must skip it" do
52
+ yielded_records = []
53
+
54
+ subject.parse(io) do |record|
55
+ yielded_records << record
56
+ end
57
+
58
+ expect(yielded_records.length).to eq(1)
59
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
60
+ end
61
+ end
62
+
63
+ context "when the line is a ']' character" do
64
+ let(:lines) do
65
+ [
66
+ %{{ "ip": "93.184.216.34", "timestamp": "1629960621", "ports": [ {"port": 80, "proto": "tcp", "status": "open", "reason": "syn-ack", "ttl": 54} ] }},
67
+ "]",
68
+ ]
69
+ end
70
+
71
+ let(:io) { StringIO.new(lines.join("\n")) }
72
+
73
+ it "must skip it" do
74
+ yielded_records = []
75
+
76
+ subject.parse(io) do |record|
77
+ yielded_records << record
78
+ end
79
+
80
+ expect(yielded_records.length).to eq(1)
81
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
82
+ end
83
+ end
84
+
85
+ context "when the line starts with a '{'" do
86
+ context "and contains a \"ports\": JSON Hash" do
87
+ let(:protocol) { :tcp }
88
+ let(:status) { :open }
89
+ let(:port) { 443 }
90
+ let(:ip) { IPAddr.new("93.184.216.34") }
91
+ let(:timestamp) { Time.at(1629960470) }
92
+ let(:reason) { [:syn, :ack] }
93
+ let(:ttl) { 54 }
94
+
95
+ let(:line) do
96
+ %{{ "ip": "#{ip}", "timestamp": "#{timestamp.to_i}", "ports": [ {"port": #{port}, "proto": "#{protocol}", "status": "#{status}", "reason": "#{reason.join('-')}", "ttl": #{ttl}} ] }}
97
+ end
98
+ let(:io) { StringIO.new(line) }
99
+
100
+ it "must parse the line into a Masscan::Status object" do
101
+ yielded_records = []
102
+
103
+ subject.parse(io) do |record|
104
+ yielded_records << record
105
+ end
106
+
107
+ expect(yielded_records.length).to eq(1)
108
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
109
+
110
+ yielded_status = yielded_records.first
111
+
112
+ expect(yielded_status.status).to be(status)
113
+ expect(yielded_status.protocol).to be(protocol)
114
+ expect(yielded_status.port).to be(port)
115
+ expect(yielded_status.reason).to eq(reason)
116
+ expect(yielded_status.ttl).to be(ttl)
117
+ expect(yielded_status.ip).to eq(ip)
118
+ expect(yielded_status.timestamp).to eq(timestamp)
119
+ end
120
+
121
+ context "but also contains a \"service\": JSON Hash" do
122
+ let(:service_name) { "http.server" }
123
+ let(:service_keyword) { :http_server }
124
+
125
+ let(:payload) { "ECS (sec/974D)" }
126
+
127
+ let(:line) do
128
+ %{{ "ip": "#{ip}", "timestamp": "#{timestamp.to_i}", "ports": [ {"port": #{port}, "proto": "#{protocol}", "service": {"name": "#{service_name}", "banner": "#{payload}"} } ] }}
129
+ end
130
+
131
+ let(:io) { StringIO.new(line) }
132
+
133
+ it "must parse the line into a Masscan::Banner object" do
134
+ yielded_records = []
135
+
136
+ subject.parse(io) do |record|
137
+ yielded_records << record
138
+ end
139
+
140
+ expect(yielded_records.length).to eq(1)
141
+ expect(yielded_records.first).to be_kind_of(Masscan::Banner)
142
+
143
+ yielded_banner = yielded_records.first
144
+
145
+ expect(yielded_banner.protocol).to be(protocol)
146
+ expect(yielded_banner.port).to be(port)
147
+ expect(yielded_banner.ip).to eq(ip)
148
+ expect(yielded_banner.timestamp).to eq(timestamp)
149
+
150
+ expect(yielded_banner.service).to eq(service_keyword)
151
+ expect(yielded_banner.payload).to eq(payload)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require_relative 'parser_examples'
3
+
4
+ require 'masscan/parsers/list'
5
+ require 'stringio'
6
+
7
+ describe Masscan::Parsers::List do
8
+ let(:path) { Fixtures.join('masscan.list') }
9
+
10
+ describe ".open" do
11
+ include_examples "Parser.open"
12
+ end
13
+
14
+ let(:io) { subject.open(path) }
15
+
16
+ describe ".parse" do
17
+ include_examples "Parser.parse"
18
+
19
+ context "when the line begins with a '#' character" do
20
+ let(:lines) do
21
+ [
22
+ "#masscan",
23
+ "open tcp 443 93.184.216.34 1629960470",
24
+ "#end"
25
+ ]
26
+ end
27
+
28
+ let(:io) { StringIO.new(lines.join("\n")) }
29
+
30
+ it "must skip it" do
31
+ yielded_records = []
32
+
33
+ subject.parse(io) do |record|
34
+ yielded_records << record
35
+ end
36
+
37
+ expect(yielded_records.length).to eq(1)
38
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
39
+ end
40
+ end
41
+
42
+ context "when the line begins with 'open '" do
43
+ let(:protocol) { :tcp }
44
+ let(:port) { 443 }
45
+ let(:ip) { IPAddr.new("93.184.216.34") }
46
+ let(:timestamp) { Time.at(1629960470) }
47
+ let(:line) { "open #{protocol} #{port} #{ip} #{timestamp.to_i}" }
48
+ let(:io) { StringIO.new(line) }
49
+
50
+ it "must parse the line into a Masscan::Status object" do
51
+ yielded_records = []
52
+
53
+ subject.parse(io) do |record|
54
+ yielded_records << record
55
+ end
56
+
57
+ expect(yielded_records.length).to eq(1)
58
+ expect(yielded_records.first).to be_kind_of(Masscan::Status)
59
+
60
+ yielded_status = yielded_records.first
61
+
62
+ expect(yielded_status.status).to be(:open)
63
+ expect(yielded_status.protocol).to be(protocol)
64
+ expect(yielded_status.port).to be(port)
65
+ expect(yielded_status.ip).to eq(ip)
66
+ expect(yielded_status.timestamp).to eq(timestamp)
67
+ end
68
+ end
69
+
70
+ context "when the line begins with 'banner '" do
71
+ let(:protocol) { :tcp }
72
+ let(:port) { 80 }
73
+ let(:ip) { IPAddr.new("93.184.216.34") }
74
+ let(:timestamp) { Time.at(1629960472) }
75
+
76
+ let(:service_name) { "http.server" }
77
+ let(:service_keyword) { :http_server }
78
+
79
+ let(:payload) { "ECS (sec/974D)" }
80
+
81
+ let(:line) do
82
+ "banner #{protocol} #{port} #{ip} #{timestamp.to_i} #{service_name} #{payload}"
83
+ end
84
+
85
+ let(:io) { StringIO.new(line) }
86
+
87
+ it "must parse the line into a Masscan::Banner object" do
88
+ yielded_records = []
89
+
90
+ subject.parse(io) do |record|
91
+ yielded_records << record
92
+ end
93
+
94
+ expect(yielded_records.length).to eq(1)
95
+ expect(yielded_records.first).to be_kind_of(Masscan::Banner)
96
+
97
+ yielded_banner = yielded_records.first
98
+
99
+ expect(yielded_banner.protocol).to be(protocol)
100
+ expect(yielded_banner.port).to be(port)
101
+ expect(yielded_banner.ip).to eq(ip)
102
+ expect(yielded_banner.timestamp).to eq(timestamp)
103
+
104
+ expect(yielded_banner.service).to eq(service_keyword)
105
+ expect(yielded_banner.payload).to eq(payload)
106
+ end
107
+ end
108
+ end
109
+ end