logstash-input-box_enterprise 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/CHANGELOG.md +2 -0
- data/CONTRIBUTORS +10 -0
- data/DEVELOPER.md +2 -0
- data/Gemfile +3 -0
- data/LICENSE +11 -0
- data/README.md +86 -0
- data/lib/logstash/inputs/box_enterprise.rb +619 -0
- data/logstash-input-box_enterprise.gemspec +38 -0
- data/spec/inputs/box_enterprise_spec.rb +698 -0
- metadata +221 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'logstash-input-box_enterprise'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.licenses = ['Apache License (2.0)']
|
5
|
+
s.summary = 'This plugin fetches enterprise events from Box.com to ship to a siem'
|
6
|
+
s.description = 'For SIEMs that do not have the capability to pull the log events from Box.com, this plugin can do the push and push to the SIEM'
|
7
|
+
s.homepage = 'https://github.com/SecurityRiskAdvisors/logstash-input-box_enterprise'
|
8
|
+
s.authors = ['SRA']
|
9
|
+
s.email = 'info@securityriskadvisors.com'
|
10
|
+
s.require_paths = ['lib']
|
11
|
+
|
12
|
+
# Files
|
13
|
+
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
|
14
|
+
# Tests
|
15
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
|
+
|
17
|
+
# Special flag to let us know this is actually a logstash plugin
|
18
|
+
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
19
|
+
|
20
|
+
# Gem dependencies
|
21
|
+
#s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
|
22
|
+
# Retaining logstash 2.4 compat
|
23
|
+
s.add_runtime_dependency "logstash-core-plugin-api", "~> 1.0"
|
24
|
+
s.add_runtime_dependency 'logstash-codec-plain'
|
25
|
+
s.add_runtime_dependency 'stud', '~> 0.0.22'
|
26
|
+
# Retaining logstash 2.4 compat
|
27
|
+
s.add_runtime_dependency 'logstash-mixin-http_client', ">= 2.2.4", "< 3.0.0"
|
28
|
+
#s.add_runtime_dependency 'logstash-mixin-http_client', ">= 2.2.4", "< 7.0.0"
|
29
|
+
s.add_runtime_dependency 'manticore', ">=0.6.1"
|
30
|
+
s.add_runtime_dependency 'rufus-scheduler', "~>3.0.9"
|
31
|
+
s.add_runtime_dependency 'jwt', '~> 1.5', '>= 1.5.6'
|
32
|
+
|
33
|
+
s.add_development_dependency 'logstash-devutils'
|
34
|
+
s.add_development_dependency 'logstash-codec-json'
|
35
|
+
s.add_development_dependency 'flores'
|
36
|
+
s.add_development_dependency 'timecop'
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,698 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/devutils/rspec/spec_helper"
|
3
|
+
require "logstash/inputs/box_enterprise"
|
4
|
+
require "timecop"
|
5
|
+
require "rspec/wait"
|
6
|
+
|
7
|
+
describe LogStash::Inputs::BoxEnterprise do
|
8
|
+
|
9
|
+
private_key = %{
|
10
|
+
-----BEGIN RSA PRIVATE KEY-----
|
11
|
+
Proc-Type: 4,ENCRYPTED
|
12
|
+
DEK-Info: AES-128-CBC,5E292076AF2B7909C68D78A2271DABFA
|
13
|
+
|
14
|
+
16VO/fgqgT+xlLmso9ldpzYW850j3JDL935Gca3qow9Nt8LcmigZjYca6MZE0Rp6
|
15
|
+
LcZBKNpvOsk4722dTFKnia0MqVRI6paYU+07gl3Dh7NJ2Y3K5wsGl9Y25yQaZG1i
|
16
|
+
45dA8TOinvOrM0bxTKhmuHodh1GhkJwvLY4ya994Lf1LNfS3gmMMNbA1ehwCkq0V
|
17
|
+
0+BOc/jpR2ah8k5e/MErI5meDmWxscos7yleg8lpnrTdBGE3R97Xn+0KbW6hCeow
|
18
|
+
LaYkFoBwb4wl4nVnLjDhTBeaiCYk8NsLqaake775LW00GNF/Y+xSXTcrk/Fm/x5q
|
19
|
+
KgSxaMXZ076QgrR3qlShmzTFeoI3hYI3OU3MOykB8aiiaKG8RT7g7vvZMd8pZvtu
|
20
|
+
dXgS6t77VFAaB4VuQsqfpgZQRFa8RMP4TX9Eom/OC3s=
|
21
|
+
-----END RSA PRIVATE KEY-----
|
22
|
+
}.gsub(/^ +/m,'') # I want to have the formatting correct but also the leading spaces need to be stripped
|
23
|
+
|
24
|
+
let(:queue) { Queue.new }
|
25
|
+
let(:default_schedule) {
|
26
|
+
{ "every" => "30s" }
|
27
|
+
}
|
28
|
+
let(:default_chunk_size) { 100 }
|
29
|
+
let(:metadata_target) { "_http_poller_metadata" }
|
30
|
+
let(:default_client_secret_env) { "Z36nr1HVNaessT0R6SFOlk2sQFwk6" }
|
31
|
+
let(:default_client_id) { "hwwqLg38lAsBdFgXVgNUxVl4lVovYsOd" }
|
32
|
+
let(:default_enterprise_id) { "284423" }
|
33
|
+
let(:default_kid) { "aiggs32v" }
|
34
|
+
let(:default_private_key_file) { "/dev/null" }
|
35
|
+
let(:default_private_key_pass_env) { "strong_password" }
|
36
|
+
|
37
|
+
|
38
|
+
let(:default_opts) {
|
39
|
+
{
|
40
|
+
"schedule" => default_schedule,
|
41
|
+
"chunk_size" => default_chunk_size,
|
42
|
+
"metadata_target" => metadata_target,
|
43
|
+
"client_secret_env" => default_client_secret_env,
|
44
|
+
"client_id" => default_client_id,
|
45
|
+
"enterprise_id" => default_enterprise_id,
|
46
|
+
"kid" => default_kid,
|
47
|
+
"private_key_file" => default_private_key_file,
|
48
|
+
"private_key_pass_env" => default_private_key_pass_env,
|
49
|
+
"codec" => "json"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
let(:klass) { LogStash::Inputs::BoxEnterprise }
|
53
|
+
|
54
|
+
|
55
|
+
describe "config" do
|
56
|
+
shared_examples "configuration errors" do
|
57
|
+
it "raises an exception" do
|
58
|
+
expect {subject.register}.to raise_exception(LogStash::ConfigurationError)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
subject { klass.new(opts) }
|
63
|
+
|
64
|
+
before(:each) do
|
65
|
+
subject
|
66
|
+
end
|
67
|
+
|
68
|
+
context "created_after is not in the correct format" do
|
69
|
+
let(:opts) { default_opts.merge({"created_after" => "1234567890"}) }
|
70
|
+
include_examples("configuration errors")
|
71
|
+
end
|
72
|
+
|
73
|
+
context "created_before is not in the correct format" do
|
74
|
+
let(:opts) { default_opts.merge({"created_before" => "1234567890"}) }
|
75
|
+
include_examples("configuration errors")
|
76
|
+
end
|
77
|
+
|
78
|
+
context "incorrect algorithim is provided" do
|
79
|
+
let(:opts) { default_opts.merge({"algo" => "ABC123"}) }
|
80
|
+
include_examples("configuration errors")
|
81
|
+
end
|
82
|
+
|
83
|
+
context "chunk_size is too small" do
|
84
|
+
let(:opts) { default_opts.merge({"chunk_size" => "-1"}) }
|
85
|
+
include_examples("configuration errors")
|
86
|
+
end
|
87
|
+
|
88
|
+
context "chunk_size is too big" do
|
89
|
+
let(:opts) { default_opts.merge({"chunk_size" => "501"}) }
|
90
|
+
include_examples("configuration errors")
|
91
|
+
end
|
92
|
+
|
93
|
+
context "env and file management" do
|
94
|
+
|
95
|
+
shared_examples "env and file examples" do
|
96
|
+
context "neither env and file are provided" do
|
97
|
+
let(:key_env) { "#{key_id}_env" }
|
98
|
+
let(:opts) {
|
99
|
+
opts = default_opts.clone
|
100
|
+
opts.delete(key_env)
|
101
|
+
opts
|
102
|
+
}
|
103
|
+
include_examples("configuration errors")
|
104
|
+
end
|
105
|
+
context "both env or file are provided" do
|
106
|
+
let(:key_file) { "#{key_id}_file" }
|
107
|
+
let (:opts) { default_opts.merge({key_file => "/dev/null"}) }
|
108
|
+
include_examples("configuration errors")
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
end
|
113
|
+
context "private_key_pass" do
|
114
|
+
let(:key_id) { "private_key_pass" }
|
115
|
+
include_examples("env and file examples")
|
116
|
+
end
|
117
|
+
|
118
|
+
context "client_secret" do
|
119
|
+
let(:key_id) { "client_secret" }
|
120
|
+
include_examples("env and file examples")
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "secret file management" do
|
126
|
+
shared_examples "secret file import tests" do
|
127
|
+
context "file is too large" do
|
128
|
+
before { allow(File).to receive(:size).with(opts[file_test_key]) { 10 * 2**21 } }
|
129
|
+
include_examples("configuration errors")
|
130
|
+
end
|
131
|
+
|
132
|
+
context "file cannot be read" do
|
133
|
+
before { allow(File).to receive(:read).with(opts[file_test_key]) { raise IOError } }
|
134
|
+
include_examples("configuration errors")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "private_key_pass_file" do
|
139
|
+
let(:file_test_key) { "private_key_pass_file" }
|
140
|
+
let(:opts) {
|
141
|
+
opts = default_opts.merge({"private_key_pass_file" => "/dev/null"} ).clone
|
142
|
+
opts.delete("private_key_pass_env")
|
143
|
+
opts
|
144
|
+
}
|
145
|
+
include_examples("secret file import tests")
|
146
|
+
end
|
147
|
+
context "client_secret_file" do
|
148
|
+
let(:file_test_key) {"client_secret_file"}
|
149
|
+
let(:opts) { default_opts.merge({"client_secret_file" => "/dev/null"} ) }
|
150
|
+
include_examples("secret file import tests")
|
151
|
+
end
|
152
|
+
|
153
|
+
context "private_key_file" do
|
154
|
+
let(:file_test_key) { "private_key_file" }
|
155
|
+
let(:opts) { default_opts }
|
156
|
+
include_examples("secret file import tests")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "Error loading private key" do
|
161
|
+
let(:opts) { default_opts }
|
162
|
+
before { allow(OpenSSL::PKey::RSA).to receive(:new) { raise OpenSSL::PKey::RSAError } }
|
163
|
+
include_examples("configuration errors")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "instances" do
|
168
|
+
|
169
|
+
subject { klass.new(default_opts) }
|
170
|
+
before do
|
171
|
+
# Load a custom private key for testing purposes
|
172
|
+
allow(File).to receive(:read).with(default_private_key_file) { private_key }
|
173
|
+
subject.register
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#run" do
|
177
|
+
it "should setup a scheduler" do
|
178
|
+
|
179
|
+
runner = Thread.new do
|
180
|
+
subject.run(double("queue"))
|
181
|
+
expect(subject.instance_variable_get("@scheduler")).to be_a_kind_of(Rufus::Scheduler)
|
182
|
+
end
|
183
|
+
runner.kill
|
184
|
+
runner.join
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#run_once" do
|
189
|
+
let(:auth_token) { "" }
|
190
|
+
let(:params_event) { {} }
|
191
|
+
it "should issue an async request for each url" do
|
192
|
+
expect(subject).to receive(:run_fetcher).with(queue,auth_token,params_event).once
|
193
|
+
|
194
|
+
subject.send(:run_once, queue,auth_token,params_event) # :run_once is a private method
|
195
|
+
end
|
196
|
+
end
|
197
|
+
describe "#handle_success" do
|
198
|
+
|
199
|
+
let(:entry) { {"foo" => "bar"} }
|
200
|
+
let(:payload) { {"entries" => [entry] } }
|
201
|
+
let(:response_body) { LogStash::Json.dump(payload) }
|
202
|
+
let(:response) { Manticore::StubbedResponse.stub(body: response_body, code: 200).call }
|
203
|
+
let(:auth_token) { "asdf" }
|
204
|
+
let(:requested_url) { subject.instance_variable_get(:@event_url) }
|
205
|
+
let(:exec_time) { 1 }
|
206
|
+
|
207
|
+
it "should generate an error, fixes bug" do
|
208
|
+
|
209
|
+
allow(subject).to receive(:decorate)
|
210
|
+
expect(subject.instance_variable_get(:@logger)).to receive(:error)
|
211
|
+
subject.send(:handle_success, queue, response, auth_token, requested_url, exec_time)
|
212
|
+
expect(subject.instance_variable_get(:@continue)).to be(false)
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|
216
|
+
describe "#handle_auth" do
|
217
|
+
|
218
|
+
let(:public_key) { OpenSSL::PKey::RSA.new(private_key, default_opts['private_key_pass_env']).public_key }
|
219
|
+
let(:header_position) { 1 }
|
220
|
+
let(:payload_position) { 0 }
|
221
|
+
let(:auth_token) { "" }
|
222
|
+
|
223
|
+
context "with a valid response for an auth token" do
|
224
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
225
|
+
let(:response_payload) { {:access_token => "#{token_payload}"} }
|
226
|
+
let(:response_body) { LogStash::Json.dump(response_payload) }
|
227
|
+
it "should setup the claim and modify auth_token" do
|
228
|
+
|
229
|
+
response = Manticore::StubbedResponse.stub(body: response_body, code: 200).call
|
230
|
+
allow(subject.client).to receive(:post) do |url, options={}|
|
231
|
+
expect(options[:params][:client_secret]).to eq(default_opts['client_secret_env'])
|
232
|
+
expect(options[:params][:client_id]).to eq(default_opts['client_id'])
|
233
|
+
jwt = nil
|
234
|
+
expect { jwt = JWT.decode(options[:params][:assertion], public_key, true ) }.not_to raise_error
|
235
|
+
expect(jwt[payload_position]['iss']).to eq(default_opts['client_id'])
|
236
|
+
expect(jwt[payload_position]['sub']).to eq(default_opts['enterprise_id'])
|
237
|
+
expect(jwt[payload_position]['exp']).to be_within(30).of(Time.now.to_i)
|
238
|
+
expect(jwt[header_position]['kid']).to eq(default_opts['kid'])
|
239
|
+
response
|
240
|
+
end
|
241
|
+
|
242
|
+
subject.send(:handle_auth, queue, auth_token)
|
243
|
+
expect(auth_token).to eq(token_payload)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
context "with an empty response from the server" do
|
247
|
+
|
248
|
+
let(:response_body) { "" }
|
249
|
+
it "should generate an error" do
|
250
|
+
response = Manticore::StubbedResponse.stub(body: response_body, code: 200).call
|
251
|
+
allow(subject.client).to receive(:post) do |url, options={}|
|
252
|
+
expect(options[:params][:client_secret]).to eq(default_opts['client_secret_env'])
|
253
|
+
expect(options[:params][:client_id]).to eq(default_opts['client_id'])
|
254
|
+
jwt = nil
|
255
|
+
expect { jwt = JWT.decode(options[:params][:assertion], public_key, true ) }.not_to raise_error
|
256
|
+
expect(jwt[payload_position]['iss']).to eq(default_opts['client_id'])
|
257
|
+
expect(jwt[payload_position]['sub']).to eq(default_opts['enterprise_id'])
|
258
|
+
expect(jwt[payload_position]['exp']).to be_within(30).of(Time.now.to_i)
|
259
|
+
expect(jwt[header_position]['kid']).to eq(default_opts['kid'])
|
260
|
+
response
|
261
|
+
end
|
262
|
+
expect(subject).to receive(:handle_unknown_error)
|
263
|
+
subject.send(:handle_auth, queue, auth_token)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
context "with a non-success code from the server" do
|
267
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
268
|
+
let(:response_payload) { {:access_token => "#{token_payload}"} }
|
269
|
+
let(:response_body) { LogStash::Json.dump(response_payload) }
|
270
|
+
it "should generate an error" do
|
271
|
+
response = Manticore::StubbedResponse.stub(body: response_body, code: 403).call
|
272
|
+
allow(subject.client).to receive(:post) do |url, options={}|
|
273
|
+
expect(options[:params][:client_secret]).to eq(default_opts['client_secret_env'])
|
274
|
+
expect(options[:params][:client_id]).to eq(default_opts['client_id'])
|
275
|
+
jwt = nil
|
276
|
+
expect { jwt = JWT.decode(options[:params][:assertion], public_key, true ) }.not_to raise_error
|
277
|
+
expect(jwt[payload_position]['iss']).to eq(default_opts['client_id'])
|
278
|
+
expect(jwt[payload_position]['sub']).to eq(default_opts['enterprise_id'])
|
279
|
+
expect(jwt[payload_position]['exp']).to be_within(30).of(Time.now.to_i)
|
280
|
+
expect(jwt[header_position]['kid']).to eq(default_opts['kid'])
|
281
|
+
response
|
282
|
+
end
|
283
|
+
expect(subject).to receive(:handle_unknown_error)
|
284
|
+
subject.send(:handle_auth, queue, auth_token)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
context "with a timeout connecting to the server" do
|
288
|
+
it "should generate an error" do
|
289
|
+
allow_any_instance_of(Manticore::Client).to receive_message_chain("client.execute") { raise Manticore::SocketException }
|
290
|
+
|
291
|
+
expect(subject).to receive(:handle_failure)
|
292
|
+
expect(subject).not_to receive(:handle_unknown_error)
|
293
|
+
subject.send(:handle_auth, queue, auth_token)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
describe "scheduler configuration" do
|
299
|
+
|
300
|
+
let(:auth_token) { "" }
|
301
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
302
|
+
before do
|
303
|
+
# disable network connections
|
304
|
+
allow_any_instance_of(Manticore::Client).to receive_message_chain("client.execute") { raise Manticore::SocketException }
|
305
|
+
# Disable private key read
|
306
|
+
allow_any_instance_of(OpenSSL::PKey::RSA).to receive(:initialize) { true }
|
307
|
+
|
308
|
+
allow(subject).to receive(:handle_auth) do |queue, auth_token|
|
309
|
+
auth_token.clear
|
310
|
+
auth_token << token_payload
|
311
|
+
auth_token
|
312
|
+
end
|
313
|
+
subject.register
|
314
|
+
end
|
315
|
+
|
316
|
+
context "given 'cron' expression" do
|
317
|
+
let(:opts) { default_opts.merge({"schedule" => {"cron" => "* * * * * UTC"}}) }
|
318
|
+
subject { klass.new(opts) }
|
319
|
+
it "should run at the schedule" do
|
320
|
+
Timecop.travel(Time.new(2000,1,1,0,0,0,'+00:00'))
|
321
|
+
Timecop.scale(60)
|
322
|
+
queue = Queue.new
|
323
|
+
runner = Thread.new do
|
324
|
+
subject.run(queue)
|
325
|
+
end
|
326
|
+
sleep 3
|
327
|
+
subject.stop
|
328
|
+
runner.kill
|
329
|
+
runner.join
|
330
|
+
expect(queue.size).to eq(2)
|
331
|
+
Timecop.return
|
332
|
+
end
|
333
|
+
end
|
334
|
+
context "given 'at' expression" do
|
335
|
+
let(:opts) { default_opts.merge("schedule" => {"at" => "2000-01-01 00:05:00 +0000"}) }
|
336
|
+
subject { klass.new(opts) }
|
337
|
+
it "should run at the schedule" do
|
338
|
+
Timecop.travel(Time.new(2000,1,1,0,0,0,'+00:00'))
|
339
|
+
Timecop.scale(60 * 5)
|
340
|
+
queue = Queue.new
|
341
|
+
runner = Thread.new do
|
342
|
+
subject.run(queue)
|
343
|
+
end
|
344
|
+
sleep 2
|
345
|
+
subject.stop
|
346
|
+
runner.kill
|
347
|
+
runner.join
|
348
|
+
expect(queue.size).to eq(1)
|
349
|
+
Timecop.return
|
350
|
+
end
|
351
|
+
end
|
352
|
+
context "given 'every' expression" do
|
353
|
+
let(:opts) { default_opts.merge("schedule" => {"every" => "2s"}) }
|
354
|
+
subject { klass.new(opts) }
|
355
|
+
it "should run at the schedule" do
|
356
|
+
queue = Queue.new
|
357
|
+
runner = Thread.new do
|
358
|
+
subject.run(queue)
|
359
|
+
end
|
360
|
+
#T 0123456
|
361
|
+
#events x x x x
|
362
|
+
#expects 3 events at T=5
|
363
|
+
sleep 5
|
364
|
+
subject.stop
|
365
|
+
runner.kill
|
366
|
+
runner.join
|
367
|
+
expect(queue.size).to eq(3)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
context "given 'in' expression" do
|
371
|
+
let(:opts) { default_opts.merge("schedule" => {"in" => "2s"}) }
|
372
|
+
subject { klass.new(opts) }
|
373
|
+
it "should run at the schedule" do
|
374
|
+
queue = Queue.new
|
375
|
+
runner = Thread.new do
|
376
|
+
subject.run(queue)
|
377
|
+
end
|
378
|
+
sleep 3
|
379
|
+
subject.stop
|
380
|
+
runner.kill
|
381
|
+
runner.join
|
382
|
+
expect(queue.size).to eq(1)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
describe "events" do
|
387
|
+
|
388
|
+
shared_examples("matching metadata") {
|
389
|
+
let(:metadata) { event.get(metadata_target) }
|
390
|
+
let(:options) { defined?(settings) ? settings : opts }
|
391
|
+
let(:metadata_url) { poller.instance_variable_get("@event_url") }
|
392
|
+
it "should have the correct request url" do
|
393
|
+
expect(metadata["url"].to_s).to eql(metadata_url)
|
394
|
+
end
|
395
|
+
|
396
|
+
it "should have the correct code" do
|
397
|
+
expect(metadata["code"]).to eql(code)
|
398
|
+
end
|
399
|
+
|
400
|
+
}
|
401
|
+
|
402
|
+
shared_examples "unprocessable_requests" do
|
403
|
+
let(:poller) { klass.new(settings) }
|
404
|
+
let(:auth_token) { "" }
|
405
|
+
let(:params_event) { {} }
|
406
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
407
|
+
subject(:event) {
|
408
|
+
poller.send(:run_once, queue, auth_token, params_event)
|
409
|
+
queue.pop(true)
|
410
|
+
}
|
411
|
+
|
412
|
+
before do
|
413
|
+
# Disable private key read
|
414
|
+
allow_any_instance_of(OpenSSL::PKey::RSA).to receive(:initialize) { true }
|
415
|
+
|
416
|
+
allow(poller).to receive(:handle_auth) do |queue, auth_token|
|
417
|
+
auth_token.clear
|
418
|
+
auth_token << token_payload
|
419
|
+
auth_token
|
420
|
+
end
|
421
|
+
|
422
|
+
poller.register
|
423
|
+
allow(poller).to receive(:handle_failure).and_call_original
|
424
|
+
allow(poller).to receive(:handle_success)
|
425
|
+
event
|
426
|
+
end
|
427
|
+
|
428
|
+
it "should enqueue a message" do
|
429
|
+
expect(event).to be_a(LogStash::Event)
|
430
|
+
end
|
431
|
+
|
432
|
+
it "should enqueue a message with 'http_request_failure' set" do
|
433
|
+
expect(event.get("http_request_failure")).to be_a(Hash)
|
434
|
+
end
|
435
|
+
|
436
|
+
it "should tag the event with '_http_request_failure'" do
|
437
|
+
expect(event.get("tags")).to include('_http_request_failure')
|
438
|
+
end
|
439
|
+
|
440
|
+
it "should invoke handle failure exactly once" do
|
441
|
+
expect(poller).to have_received(:handle_failure)
|
442
|
+
end
|
443
|
+
|
444
|
+
it "should not invoke handle success at all" do
|
445
|
+
expect(poller).not_to have_received(:handle_success)
|
446
|
+
end
|
447
|
+
|
448
|
+
include_examples("matching metadata")
|
449
|
+
|
450
|
+
end
|
451
|
+
|
452
|
+
context "with a non responsive server" do
|
453
|
+
context "due to a non-existent host" do # Fail with handlers
|
454
|
+
let(:code) { nil } # no response expected
|
455
|
+
let(:settings) { default_opts }
|
456
|
+
before {allow_any_instance_of(Manticore::Client).to receive_message_chain("client.execute") { raise Manticore::ResolutionFailure } }
|
457
|
+
|
458
|
+
include_examples("unprocessable_requests")
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
describe "a valid request and decoded response" do
|
464
|
+
let(:entry) { {"foo" => "bar"} }
|
465
|
+
let(:payload) { {"next_stream_position" => 0, "entries" => [entry] } }
|
466
|
+
let(:response_body) { LogStash::Json.dump(payload) }
|
467
|
+
|
468
|
+
let(:code) { 200 }
|
469
|
+
let(:opts) { default_opts }
|
470
|
+
|
471
|
+
let(:auth_token) { "" }
|
472
|
+
let(:params_event) { {} }
|
473
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
474
|
+
|
475
|
+
let(:instance) { klass.new(opts) }
|
476
|
+
let(:poller) { instance }
|
477
|
+
|
478
|
+
subject(:event) { queue.pop(true) }
|
479
|
+
|
480
|
+
before do
|
481
|
+
allow_any_instance_of(OpenSSL::PKey::RSA).to receive(:initialize) { true }
|
482
|
+
allow(instance).to receive(:handle_auth) do |queue, auth_token|
|
483
|
+
# setup for 401 test case
|
484
|
+
unless (auth_token.empty?)
|
485
|
+
instance.instance_variable_set("@continue", false)
|
486
|
+
end
|
487
|
+
auth_token.clear
|
488
|
+
auth_token << token_payload
|
489
|
+
auth_token
|
490
|
+
end
|
491
|
+
instance.register
|
492
|
+
allow(instance).to receive(:decorate)
|
493
|
+
instance.client.stub(%r{#{instance.instance_variable_get("@event_url")}.*},
|
494
|
+
:body => response_body,
|
495
|
+
:code => code
|
496
|
+
)
|
497
|
+
instance.send(:run_once, queue, auth_token, params_event)
|
498
|
+
|
499
|
+
end
|
500
|
+
|
501
|
+
it "should have a matching message" do
|
502
|
+
expect(event.to_hash).to include(entry)
|
503
|
+
end
|
504
|
+
|
505
|
+
it "should decorate the event" do
|
506
|
+
expect(instance).to have_received(:decorate).once
|
507
|
+
end
|
508
|
+
|
509
|
+
include_examples("matching metadata")
|
510
|
+
|
511
|
+
context "with an empty body" do
|
512
|
+
let(:response_body) { "" }
|
513
|
+
it "should return an empty event" do
|
514
|
+
expect(event.get("[_http_poller_metadata][response_headers][content-length]")).to eql("0")
|
515
|
+
end
|
516
|
+
end
|
517
|
+
context "with metadata omitted" do
|
518
|
+
let(:opts) {
|
519
|
+
opts = default_opts.clone
|
520
|
+
opts.delete("metadata_target")
|
521
|
+
opts
|
522
|
+
}
|
523
|
+
|
524
|
+
it "should not have any metadata on the event" do
|
525
|
+
expect(event.get(metadata_target)).to be_nil
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
context "with a specified target" do
|
530
|
+
let(:target) { "mytarget" }
|
531
|
+
let(:opts) { default_opts.merge("target" => target) }
|
532
|
+
|
533
|
+
it "should store the event info in the target" do
|
534
|
+
# When events go through the pipeline they are java-ified
|
535
|
+
# this normalizes the payload to java types
|
536
|
+
payload_normalized = LogStash::Json.load(LogStash::Json.dump(entry))
|
537
|
+
expect(event.get(target)).to include(payload_normalized)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
context "with non-200 HTTP response codes" do
|
542
|
+
let(:code) { |example| example.metadata[:http_code] }
|
543
|
+
let(:response_body) { "{}" }
|
544
|
+
|
545
|
+
it "responds to a 500 code", :http_code => 500 do
|
546
|
+
expect(event.to_hash).to include({"Box-Error-Code" => 500})
|
547
|
+
expect(event.get("tags")).to include('_box_response_failure')
|
548
|
+
end
|
549
|
+
it "responds to a 401/Unauthorized code", :http_code => 401 do
|
550
|
+
expect(instance).to have_received(:handle_auth).twice
|
551
|
+
end
|
552
|
+
it "responds to a 400 code", :http_code => 400 do
|
553
|
+
instance.send(:run_once, queue, auth_token, params_event)
|
554
|
+
expect(event.to_hash).to include({"Box-Error-Code" => 400})
|
555
|
+
expect(event.get("tags")).to include('_box_response_failure')
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
describe "stopping" do
|
561
|
+
let(:config) { default_opts }
|
562
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
563
|
+
before do
|
564
|
+
allow_any_instance_of(OpenSSL::PKey::RSA).to receive(:initialize) { true }
|
565
|
+
allow_any_instance_of(klass).to receive(:handle_auth) do |queue, auth_token|
|
566
|
+
auth_token.clear
|
567
|
+
auth_token << token_payload
|
568
|
+
auth_token
|
569
|
+
end
|
570
|
+
allow_any_instance_of(Manticore::Client).to receive_message_chain("client.execute") { raise Manticore::ResolutionFailure }
|
571
|
+
end
|
572
|
+
it_behaves_like "an interruptible input plugin"
|
573
|
+
end
|
574
|
+
|
575
|
+
describe "state file" do
|
576
|
+
|
577
|
+
let(:opts) { default_opts.merge({'state_file_base' => "/tmp/box_test_"}) }
|
578
|
+
subject {klass.new(opts) }
|
579
|
+
let(:state_file_position_0) { "12345678890" }
|
580
|
+
let(:state_file_position_1) { "12345678891" }
|
581
|
+
|
582
|
+
before do
|
583
|
+
allow_any_instance_of(OpenSSL::PKey::RSA).to receive(:initialize) { true }
|
584
|
+
end
|
585
|
+
|
586
|
+
context "when being setup" do
|
587
|
+
|
588
|
+
before do
|
589
|
+
expect(File).to receive(:readable?).with(File.dirname(opts['state_file_base'])) { true }
|
590
|
+
expect(File).to receive(:executable?).with(File.dirname(opts['state_file_base'])) { true }
|
591
|
+
expect(File).to receive(:writable?).with(File.dirname(opts['state_file_base'])) { true }
|
592
|
+
allow(subject.client).to receive_message_chain("client.execute") { raise Manticore::ResolutionFailure }
|
593
|
+
end
|
594
|
+
|
595
|
+
it "creates the initial file correctly" do
|
596
|
+
expect(Dir).to receive(:[]) { [] }
|
597
|
+
expect(File).to receive(:open).with("#{opts['state_file_base']}start","w") {}
|
598
|
+
subject.register
|
599
|
+
end
|
600
|
+
|
601
|
+
it "raises an error if the file cannot be created" do
|
602
|
+
expect(Dir).to receive(:[]) { [] }
|
603
|
+
expect(File).to receive(:open).with("#{opts['state_file_base']}start","w") { raise IOError }
|
604
|
+
expect {subject.register}.to raise_exception(LogStash::ConfigurationError)
|
605
|
+
end
|
606
|
+
it "gets the next_stream_position based on the state file" do
|
607
|
+
allow(File).to receive(:open).with("#{opts['state_file_base']}start","w") { }
|
608
|
+
expect(Dir).to receive(:[]) { [opts['state_file_base'] + state_file_position_0] }
|
609
|
+
subject.register
|
610
|
+
expect(subject.instance_variable_get("@next_stream_position")).to eql(state_file_position_0)
|
611
|
+
end
|
612
|
+
it "uses the latest stream position if there is more than one file" do
|
613
|
+
allow(File).to receive(:open).with("#{opts['state_file_base']}start","w") { }
|
614
|
+
expect(Dir).to receive(:[]) { [opts['state_file_base'] + state_file_position_0, opts['state_file_base'] + state_file_position_1] }
|
615
|
+
subject.register
|
616
|
+
expect(subject.instance_variable_get("@next_stream_position")).to eql(state_file_position_1)
|
617
|
+
end
|
618
|
+
it "does not set next_stream_position if the file is start" do
|
619
|
+
expect(Dir).to receive(:[]) { [opts['state_file_base'] + "start"] }
|
620
|
+
subject.register
|
621
|
+
expect(subject.instance_variable_get(:@next_stream_position)).to be(nil)
|
622
|
+
end
|
623
|
+
end
|
624
|
+
context "when running" do
|
625
|
+
let(:entry) { {"foo" => "bar"} }
|
626
|
+
let(:payload) { {"next_stream_position" => state_file_position_0, "entries" => [entry] } }
|
627
|
+
let(:response_body) { LogStash::Json.dump(payload) }
|
628
|
+
let(:auth_token) { "" }
|
629
|
+
let(:token_payload) { "J2iageHCXuHi4rOL3BXIiEJqUW" }
|
630
|
+
let(:params_event) { {} }
|
631
|
+
|
632
|
+
let(:code) { 200 }
|
633
|
+
|
634
|
+
before(:each) do
|
635
|
+
|
636
|
+
allow(Dir).to receive(:[]) { [opts['state_file_base'] + "start"] }
|
637
|
+
|
638
|
+
subject.register
|
639
|
+
subject.client.stub(%r{#{subject.instance_variable_get("@event_url")}.*},
|
640
|
+
:body => response_body,
|
641
|
+
:code => code
|
642
|
+
)
|
643
|
+
|
644
|
+
allow(subject).to receive(:handle_failure) { subject.instance_variable_set(:@continue, false) }
|
645
|
+
|
646
|
+
allow(subject).to receive(:handle_auth) do |queue, auth_token|
|
647
|
+
auth_token.clear
|
648
|
+
auth_token << token_payload
|
649
|
+
auth_token
|
650
|
+
end
|
651
|
+
|
652
|
+
end
|
653
|
+
|
654
|
+
it "updates the state file after data is fetched" do
|
655
|
+
|
656
|
+
expect(File).to receive(:rename).with(opts['state_file_base'] + "start", opts['state_file_base'] + state_file_position_0) { 0 }
|
657
|
+
subject.send(:run_once, queue,auth_token,params_event) # :run_once is a private method
|
658
|
+
|
659
|
+
end
|
660
|
+
it "leaves the state file alone during a failure" do
|
661
|
+
|
662
|
+
subject.client.clear_stubs!
|
663
|
+
allow(subject.client).to receive_message_chain("client.execute") { raise Manticore::ResolutionFailure }
|
664
|
+
expect(File).not_to receive(:rename).with(opts['state_file_base'] + "start", any_args)
|
665
|
+
subject.send(:run_once, queue, auth_token, params_event) # :run_once is a private method
|
666
|
+
|
667
|
+
|
668
|
+
end
|
669
|
+
|
670
|
+
context "when stop is called" do
|
671
|
+
|
672
|
+
it "saves the state in the file name" do
|
673
|
+
|
674
|
+
# We are still testing the same condition, file renaming.
|
675
|
+
expect(File).to receive(:rename).with(opts['state_file_base'] + "start", opts['state_file_base'] + state_file_position_0) { 0 }
|
676
|
+
|
677
|
+
# Force a sleep to make the thread hang in the failure condition.
|
678
|
+
allow(subject).to receive(:decorate) do
|
679
|
+
subject.instance_variable_set(:continue, false)
|
680
|
+
sleep(30)
|
681
|
+
end
|
682
|
+
|
683
|
+
plugin_thread = Thread.new(subject, queue) { |subject, queue, auth_token, params_event | subject.send(:run, queue) }
|
684
|
+
|
685
|
+
# Sleep for a bit to make sure things are started.
|
686
|
+
sleep 0.5
|
687
|
+
expect(plugin_thread).to be_alive
|
688
|
+
|
689
|
+
subject.do_stop
|
690
|
+
|
691
|
+
# As they say in the logstash thread, why 3?
|
692
|
+
# Because 2 is too short, and 4 is too long.
|
693
|
+
wait(3).for {plugin_thread }.to_not be_alive
|
694
|
+
end
|
695
|
+
end
|
696
|
+
end
|
697
|
+
end
|
698
|
+
end
|