ruby-masscan 0.1.0

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