logstash-input-box_enterprise 0.1.0

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