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.
@@ -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