logstash-input-box_enterprise 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 46bc9c2e0e9ce97c7b006ef796c9b7c9eb9ff4c7
|
4
|
+
data.tar.gz: 4650dba0a79d54e40e36b6f5a4180171fc250c3e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7b75b18a4a08cc9792d69fa6058b5683ea5ae8156e3cf9a806a9e644b48d6ed1263363ffe54ccc948bfd9bace352f95ce7bc7880a9cff362a21063c1cc0f148c
|
7
|
+
data.tar.gz: 71bc121295ea12a04795ff8052ef047c23b65155937eb4f24e6373abedd73475edb07a4899be62b81155d3c6b081c6304319e2ddd350b76a1d8a1257e3493bca
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTORS
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
The following is a list of people who have contributed ideas, code, bug
|
2
|
+
reports, or in general have helped logstash along its way.
|
3
|
+
|
4
|
+
Contributors:
|
5
|
+
* -
|
6
|
+
|
7
|
+
Note: If you've sent us patches, bug reports, or otherwise contributed to
|
8
|
+
Logstash, and you aren't on the list above and want to be, please let us know
|
9
|
+
and we'll make sure you're here. Contributions from folks like you are what make
|
10
|
+
open source awesome.
|
data/DEVELOPER.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
you may not use this file except in compliance with the License.
|
3
|
+
You may obtain a copy of the License at
|
4
|
+
|
5
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
See the License for the specific language governing permissions and
|
11
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Logstash Plugin
|
2
|
+
|
3
|
+
This is a plugin for [Logstash](https://github.com/elastic/logstash).
|
4
|
+
|
5
|
+
It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
|
6
|
+
|
7
|
+
## Documentation
|
8
|
+
|
9
|
+
Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
|
10
|
+
|
11
|
+
- For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
|
12
|
+
- For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
|
13
|
+
|
14
|
+
## Need Help?
|
15
|
+
|
16
|
+
Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
|
17
|
+
|
18
|
+
## Developing
|
19
|
+
|
20
|
+
### 1. Plugin Developement and Testing
|
21
|
+
|
22
|
+
#### Code
|
23
|
+
- To get started, you'll need JRuby with the Bundler gem installed.
|
24
|
+
|
25
|
+
- Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
|
26
|
+
|
27
|
+
- Install dependencies
|
28
|
+
```sh
|
29
|
+
bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
#### Test
|
33
|
+
|
34
|
+
- Update your dependencies
|
35
|
+
|
36
|
+
```sh
|
37
|
+
bundle install
|
38
|
+
```
|
39
|
+
|
40
|
+
- Run tests
|
41
|
+
|
42
|
+
```sh
|
43
|
+
bundle exec rspec
|
44
|
+
```
|
45
|
+
|
46
|
+
### 2. Running your unpublished Plugin in Logstash
|
47
|
+
|
48
|
+
#### 2.1 Run in a local Logstash clone
|
49
|
+
|
50
|
+
- Edit Logstash `Gemfile` and add the local plugin path, for example:
|
51
|
+
```ruby
|
52
|
+
gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
|
53
|
+
```
|
54
|
+
- Install plugin
|
55
|
+
```sh
|
56
|
+
bin/logstash-plugin install --no-verify
|
57
|
+
```
|
58
|
+
- Run Logstash with your plugin
|
59
|
+
```sh
|
60
|
+
bin/logstash -e 'filter {awesome {}}'
|
61
|
+
```
|
62
|
+
At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
|
63
|
+
|
64
|
+
#### 2.2 Run in an installed Logstash
|
65
|
+
|
66
|
+
You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
|
67
|
+
|
68
|
+
- Build your plugin gem
|
69
|
+
```sh
|
70
|
+
gem build logstash-filter-awesome.gemspec
|
71
|
+
```
|
72
|
+
- Install the plugin from the Logstash home
|
73
|
+
```sh
|
74
|
+
bin/logstash-plugin install /your/local/plugin/logstash-filter-awesome.gem
|
75
|
+
```
|
76
|
+
- Start Logstash and proceed to test the plugin
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
|
81
|
+
|
82
|
+
Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
|
83
|
+
|
84
|
+
It is more important to the community that you are able to contribute.
|
85
|
+
|
86
|
+
For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
|
@@ -0,0 +1,619 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/inputs/base"
|
3
|
+
require "logstash/namespace"
|
4
|
+
require "rufus/scheduler"
|
5
|
+
require "socket" # for Socket.gethostname
|
6
|
+
require "logstash/plugin_mixins/http_client"
|
7
|
+
require "manticore"
|
8
|
+
require "openssl"
|
9
|
+
require "jwt"
|
10
|
+
require "json"
|
11
|
+
require "cgi"
|
12
|
+
|
13
|
+
MAX_FILE_SIZE = 10 * 2**20
|
14
|
+
|
15
|
+
|
16
|
+
class LogStash::Inputs::BoxEnterprise < LogStash::Inputs::Base
|
17
|
+
include LogStash::PluginMixins::HttpClient
|
18
|
+
|
19
|
+
config_name "box_enterprise"
|
20
|
+
|
21
|
+
# If undefined, Logstash will complain, even if codec is unused.
|
22
|
+
default :codec, "json"
|
23
|
+
|
24
|
+
# Set how many messages you want to pull with each request
|
25
|
+
#
|
26
|
+
# The default, `100`, means to fetch 100 events at a time.
|
27
|
+
# The maximum value is 500.
|
28
|
+
config :chunk_size, :validate => :number, :default => 100
|
29
|
+
|
30
|
+
# Schedule of when to periodically poll from the urls
|
31
|
+
# Format: A hash with
|
32
|
+
# + key: "cron" | "every" | "in" | "at"
|
33
|
+
# + value: string
|
34
|
+
# Examples:
|
35
|
+
# a) { "every" => "1h" }
|
36
|
+
# b) { "cron" => "* * * * * UTC" }
|
37
|
+
# See: rufus/scheduler for details about different schedule options and value string format
|
38
|
+
config :schedule, :validate => :hash, :required => true
|
39
|
+
|
40
|
+
# The Oauth2 Client ID used to access the Box.com API
|
41
|
+
#
|
42
|
+
# Format: A string wtih the client ID
|
43
|
+
config :client_id, :validate => :string, :required => true
|
44
|
+
|
45
|
+
# The Oauth2 Client secret to pull the events
|
46
|
+
# Here to allow it via enviornment variable
|
47
|
+
#
|
48
|
+
# Format: A string with the client secret to send
|
49
|
+
config :client_secret_env, :validate => :string
|
50
|
+
|
51
|
+
# The Oauth2 Client secret to pull events, stored in a file
|
52
|
+
#
|
53
|
+
# Format: A valid path containing the client_secret
|
54
|
+
config :client_secret_file, :validate => :path
|
55
|
+
|
56
|
+
# The Box.com Enterprise ID associated with the customer.
|
57
|
+
# Used in the JWT
|
58
|
+
#
|
59
|
+
# Format: String
|
60
|
+
config :enterprise_id, :validate => :string, :required => true
|
61
|
+
|
62
|
+
# The kid for the jwt that corresponds to the uploaded key.
|
63
|
+
# The corresponding private key must be accessible by this file.
|
64
|
+
#
|
65
|
+
# Format: String
|
66
|
+
config :kid, :validate => :string, :required => true
|
67
|
+
|
68
|
+
# The algorithim that the private/public key pair used
|
69
|
+
# Supported values: RS256, RS384, or RS512
|
70
|
+
#
|
71
|
+
# Format: String containing one of the above values
|
72
|
+
config :algo, :validate => :string, :default => "RS256"
|
73
|
+
|
74
|
+
# The file where the private key is stored.
|
75
|
+
#
|
76
|
+
# Format: Filepath
|
77
|
+
config :private_key_file, :validate => :path, :required => true
|
78
|
+
|
79
|
+
# The private key password stored in an envionrment.
|
80
|
+
# WARNING: Avoid storing the private key password directly in this file.
|
81
|
+
# This method is provided solely to add the key password via environment variable.
|
82
|
+
# This will contain the password for the private key for the Box.com instance.
|
83
|
+
#
|
84
|
+
# Format: File path
|
85
|
+
config :private_key_pass_env, :validate => :string
|
86
|
+
|
87
|
+
# The file in which the password for private key
|
88
|
+
# WARNING: This file should be VERY carefully monitored.
|
89
|
+
# This will contain the private_key which can have a lot access to your Box.com instance.
|
90
|
+
# It cannot be stressed enough how important it is to protect this file.
|
91
|
+
#
|
92
|
+
# Format: File path
|
93
|
+
config :private_key_pass_file, :validate => :path
|
94
|
+
|
95
|
+
# The base filename to store the pointer to the current location in the logs
|
96
|
+
# This file will be renamed with each new reference to limit loss of this data
|
97
|
+
# The location will need at least write and execute privs for the logstash user
|
98
|
+
# This parameter is not required, however on start logstash will ship all logs to your SIEM.
|
99
|
+
#
|
100
|
+
# Format: Filepath
|
101
|
+
# This is not the filepath of the file itself, but rather the base name of the file
|
102
|
+
config :state_file_base, :validate => :string
|
103
|
+
|
104
|
+
# The events to filter on using Box.com event filter
|
105
|
+
# This an API filter, not local, fetch only events that are needed
|
106
|
+
#
|
107
|
+
# Format: Comma separated string with the events to filter on
|
108
|
+
# Use: https://docs.box.com/reference#section-enterprise-events
|
109
|
+
config :event_type, :validate => :string
|
110
|
+
|
111
|
+
# The date and time after which to fetch events
|
112
|
+
#
|
113
|
+
# Format: string with a RFC 3339 formatted date (e.g. 2016-10-09T22:25:06-07:00)
|
114
|
+
config :created_after, :validate => :string
|
115
|
+
|
116
|
+
# The date and time before which to fetch events
|
117
|
+
#
|
118
|
+
# Format: string with a RFC 3339 formatted date (e.g. 2016-10-09T22:25:06-07:00)
|
119
|
+
config :created_before, :validate => :string
|
120
|
+
|
121
|
+
# Define the target field for placing the received data.
|
122
|
+
# If this setting is omitted, the data will be stored at the root (top level) of the event.
|
123
|
+
#
|
124
|
+
# Format: String
|
125
|
+
config :target, :validate => :string
|
126
|
+
|
127
|
+
# If you'd like to work with the request/response metadata.
|
128
|
+
# Set this value to the name of the field you'd like to store a nested
|
129
|
+
# hash of metadata.
|
130
|
+
#
|
131
|
+
# Format: String
|
132
|
+
config :metadata_target, :validate => :string, :default => '@metadata'
|
133
|
+
|
134
|
+
public
|
135
|
+
Schedule_types = %w(cron every at in)
|
136
|
+
def register
|
137
|
+
|
138
|
+
algo_types = %w(RS256 RS384 RS512)
|
139
|
+
msg_invalid_algo = "Invalid config. Algo string must contain " +
|
140
|
+
"exactly one of the following strings - RS256, RS384, or RS512"
|
141
|
+
raise LogStash::ConfigurationError, msg_invalid_algo unless algo_types.include?(@algo)
|
142
|
+
|
143
|
+
unless (@chunk_size > 0 and @chunk_size <= 500)
|
144
|
+
raise LogStash::ConfigurationError, "Chunk size must be between 1 and 500"
|
145
|
+
end
|
146
|
+
|
147
|
+
if (@created_after)
|
148
|
+
begin
|
149
|
+
@created_after = DateTime.parse(@created_after).rfc3339()
|
150
|
+
rescue ArgumentError => e
|
151
|
+
raise LogStash::ConfigurationError, "created_after must be of the form " +
|
152
|
+
"yyyy-MM-dd’‘T’‘HH:mm:ssZZ, e.g. 2013-01-01T12:00:00-07:00."
|
153
|
+
end
|
154
|
+
@created_after = CGI.escape(@created_after)
|
155
|
+
end
|
156
|
+
|
157
|
+
if (@created_before)
|
158
|
+
begin
|
159
|
+
@created_before = DateTime.parse(@created_before).rfc3339()
|
160
|
+
rescue ArgumentError => e
|
161
|
+
raise LogStash::ConfigurationError, "created_before must be of the form " +
|
162
|
+
"yyyy-MM-dd’‘T’‘HH:mm:ssZZ, e.g. 2013-01-01T12:00:00-07:00."
|
163
|
+
end
|
164
|
+
@created_before = CGI.escape(@created_before)
|
165
|
+
end
|
166
|
+
|
167
|
+
@event_type = CGI.escape(@event_type) if @event_type
|
168
|
+
|
169
|
+
if (@private_key_pass_env and @private_key_pass_file)
|
170
|
+
raise LogStash::ConfigurationError, "Both private_key_file and private_key_env cannot be set. Please select one for use."
|
171
|
+
end
|
172
|
+
unless (@private_key_pass_env or @private_key_pass_file)
|
173
|
+
raise LogStash::ConfigurationError, "Both private_key_file and private_key_env cannot be empty. Please select one for use."
|
174
|
+
end
|
175
|
+
|
176
|
+
if (@private_key_pass_file)
|
177
|
+
begin
|
178
|
+
if (File.size(@private_key_pass_file) > MAX_FILE_SIZE)
|
179
|
+
raise LogStash::ConfigurationError, "The private key password file is too large to map"
|
180
|
+
else
|
181
|
+
@private_key_pass = File.read(@private_key_pass_file).chomp
|
182
|
+
@logger.info("Successfully opened private_key_pass_file",:private_key_pass_file => @private_key_pass_file)
|
183
|
+
end
|
184
|
+
rescue LogStash::ConfigurationError
|
185
|
+
raise
|
186
|
+
# Some clean up magic to cover the stuff below.
|
187
|
+
# This will keep me from stomping on signal interrupts and ctrl+c
|
188
|
+
rescue SignalException, Interrupt, SyntaxError
|
189
|
+
raise
|
190
|
+
rescue Exception => e
|
191
|
+
# This is currently a bug in logstash, confirmed here:
|
192
|
+
# https://discuss.elastic.co/t/logstash-configurationerror-but-configurationok-logstash-2-4-0/65727/2
|
193
|
+
# Will need to determine the best way to handle this
|
194
|
+
# Rather than testing all error conditions, this can just display them.
|
195
|
+
# Should figure out a way to display this in a better fashion.
|
196
|
+
raise LogStash::ConfigurationError, e.inspect
|
197
|
+
end
|
198
|
+
else
|
199
|
+
@private_key_pass = @private_key_pass_env
|
200
|
+
end
|
201
|
+
|
202
|
+
if (@client_secret_env and @client_secret_file)
|
203
|
+
raise LogStash::ConfigurationError, "Both client_secret_file and client_secret_env cannot be set. Please select one for use."
|
204
|
+
end
|
205
|
+
unless (@client_secret_env or @client_secret_file)
|
206
|
+
raise LogStash::ConfigurationError, "Both client_secret_file and client_secret_env cannot be empty. Please select one for use."
|
207
|
+
end
|
208
|
+
|
209
|
+
if (@client_secret_file)
|
210
|
+
begin
|
211
|
+
if (File.size(@client_secret_file) > MAX_FILE_SIZE)
|
212
|
+
raise LogStash::ConfigurationError, "The client secret file is too large to map"
|
213
|
+
else
|
214
|
+
@client_secret = File.read(@client_secret_file).chomp
|
215
|
+
@logger.info("Successfully opened client_secret_file",:client_secret_file => @client_secret_file)
|
216
|
+
end
|
217
|
+
rescue LogStash::ConfigurationError
|
218
|
+
raise
|
219
|
+
# Some clean up magic to cover the stuff below.
|
220
|
+
# This will keep me from stomping on signal interrupts and ctrl+c
|
221
|
+
rescue SignalException, Interrupt, SyntaxError
|
222
|
+
raise
|
223
|
+
rescue Exception => e
|
224
|
+
# This is currently a bug in logstash, confirmed here:
|
225
|
+
# https://discuss.elastic.co/t/logstash-configurationerror-but-configurationok-logstash-2-4-0/65727/2
|
226
|
+
# Will need to determine the best way to handle this
|
227
|
+
# Rather than testing all error conditions, this can just display them.
|
228
|
+
# Should figure out a way to display this in a better fashion.
|
229
|
+
raise LogStash::ConfigurationError, e.inspect
|
230
|
+
end
|
231
|
+
else
|
232
|
+
@client_secret = @client_secret_env
|
233
|
+
end
|
234
|
+
|
235
|
+
if (File.size(@private_key_file) < MAX_FILE_SIZE)
|
236
|
+
begin
|
237
|
+
@private_key = OpenSSL::PKey::RSA.new(File.read(@private_key_file),@private_key_pass)
|
238
|
+
rescue SignalException, Interrupt, SyntaxError
|
239
|
+
raise
|
240
|
+
# This is currently a bug in logstash, confirmed here:
|
241
|
+
# https://discuss.elastic.co/t/logstash-configurationerror-but-configurationok-logstash-2-4-0/65727/2
|
242
|
+
# Will need to determine the best way to handle this
|
243
|
+
# Rather than testing all error conditions, this can just display them.
|
244
|
+
# Should figure out a way to display this in a better fashion.
|
245
|
+
rescue Exception => e
|
246
|
+
raise LogStash::ConfigurationError, e.inspect
|
247
|
+
end
|
248
|
+
else
|
249
|
+
raise LogStash::ConfigurationError, "The private key file appears to be too big to be mapped."
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
if (@state_file_base)
|
255
|
+
dir_name = File.dirname(@state_file_base)
|
256
|
+
## Generally the state file directory will have the correct permissions
|
257
|
+
## so check for that case first.
|
258
|
+
if (File.readable?(dir_name) and File.executable?(dir_name) and
|
259
|
+
File.writable?(dir_name))
|
260
|
+
@state_file = Dir[@state_file_base + "*"].sort.last
|
261
|
+
else
|
262
|
+
## Build one message for the rest of the issues
|
263
|
+
access_message = "Could not access the state file dir" +
|
264
|
+
"#{dir_name} for the following reasons: "
|
265
|
+
unless (File.readable?(dir_name))
|
266
|
+
access_message << "Cannot read #{dir_name}."
|
267
|
+
end
|
268
|
+
unless (File.executable?(dir_name))
|
269
|
+
access_message << "Cannot list directory or perform special" +
|
270
|
+
"operations on #{dir_name}."
|
271
|
+
end
|
272
|
+
unless (File.writable?(dir_name))
|
273
|
+
access_message << "Cannot write to #{dir_name}."
|
274
|
+
end
|
275
|
+
access_message << "Please provide the appopriate permissions."
|
276
|
+
raise LogStash::ConfigurationError, access_message
|
277
|
+
end
|
278
|
+
|
279
|
+
# There is a state file so get the state data from it.
|
280
|
+
if (@state_file)
|
281
|
+
@next_stream_position = @state_file.slice(/(?<state_file>#{@state_file_base})(?<state>[0-9]+)/,'state')
|
282
|
+
# If not create the state file
|
283
|
+
else
|
284
|
+
begin
|
285
|
+
@state_file = @state_file_base + "start"
|
286
|
+
@logger.info("Created base state_file", :state_file => @state_file)
|
287
|
+
# 'touch' a file to keep the conditional from happening later
|
288
|
+
File.open(@state_file, "w") {}
|
289
|
+
# Some clean up magic to cover the stuff below.
|
290
|
+
# This will keep me from stomping on signal interrupts and ctrl+c
|
291
|
+
rescue SignalException, Interrupt, SyntaxError
|
292
|
+
raise
|
293
|
+
rescue Exception => e
|
294
|
+
raise LogStash::ConfigurationError, "Could not create #{@statefile}. " +
|
295
|
+
"Error: #{e.inspect}."
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
# The auth URL from box that leverages oauth
|
302
|
+
@auth_url = "https://api.box.com/oauth2/token"
|
303
|
+
@event_url = "https://api.box.com/2.0/events"
|
304
|
+
@host = Socket.gethostname
|
305
|
+
|
306
|
+
# Generate a random alpha-numeric string that is 128 chars long
|
307
|
+
jti = (0...128).map { (('a'..'z').to_a + ('A'..'Z').to_a)[rand(52)] }.join
|
308
|
+
@payload = { :iss => @client_id, # The Client ID of the service that created the JWT assertion.
|
309
|
+
:sub => @enterprise_id, # enterprise_id for a token specific to an enterprise when creating and managing app users.
|
310
|
+
:box_sub_type => 'enterprise', # “enterprise” or “user” depending on the type of token being requested in the sub claim.
|
311
|
+
:aud => @auth_url, # Always “https://api.box.com/oauth2/token” for OAuth2 token requests
|
312
|
+
:jti => jti, # A unique identifier specified by the client for this JWT. This is a unique string that is at least 16 characters and at most 128 characters.
|
313
|
+
:exp => 0 } # The unix time as to when this JWT will expire.
|
314
|
+
## This can be set to a maximum value of 60 seconds beyond the issue time. Note: It is recommended to set this value to less than the maximum allowed 60 seconds.
|
315
|
+
## Note: It is recommended to set this value to less than the maximum allowed 60 seconds.
|
316
|
+
|
317
|
+
@header = { :kid => @kid, # Public Key ID generated by Box and provided upon submission of a Public Key. Identifies which Public Key a client is using.
|
318
|
+
:alg => @algo, # The algorithm used to verify the signature. Values may only be set to: “RS256″, “RS384″, or “RS512.
|
319
|
+
:typ => "JWT" } # Type of token. Default is “JWT” to specify a JSON Web Token (JWT).
|
320
|
+
|
321
|
+
@logger.debug("JWT created", :jsot => @payload)
|
322
|
+
|
323
|
+
end # def register
|
324
|
+
|
325
|
+
def run(queue)
|
326
|
+
|
327
|
+
auth_token = "" ## Empty string to treat as a reference to pass around functions
|
328
|
+
msg_invalid_schedule = "Invalid config. schedule hash must contain " +
|
329
|
+
"exactly one of the following keys - cron, at, every or in"
|
330
|
+
|
331
|
+
raise LogStash::ConfigurationError, msg_invalid_schedule if @schedule.keys.length !=1
|
332
|
+
schedule_type = @schedule.keys.first
|
333
|
+
schedule_value = @schedule[schedule_type]
|
334
|
+
raise LogStash::ConfigurationError, msg_invalid_schedule unless Schedule_types.include?(schedule_type)
|
335
|
+
|
336
|
+
@scheduler = Rufus::Scheduler.new(:max_work_threads => 1)
|
337
|
+
|
338
|
+
#as of v3.0.9, :first_in => :now doesn't work. Use the following workaround instead
|
339
|
+
opts = schedule_type == "every" ? { :first_in => 0.01 } : {}
|
340
|
+
opts[:overlap] = false;
|
341
|
+
|
342
|
+
params_event = {:stream_type => "admin_logs"}
|
343
|
+
params_event[:limit] = @chunk_size
|
344
|
+
params_event[:created_after] = @created_after if @created_after
|
345
|
+
params_event[:created_before] = @created_before if @created_before
|
346
|
+
params_event[:event_type] = @event_type if @event_type
|
347
|
+
|
348
|
+
@scheduler.send(schedule_type, schedule_value, opts) { run_once(queue,auth_token,params_event) }
|
349
|
+
|
350
|
+
@scheduler.join
|
351
|
+
|
352
|
+
end # def run
|
353
|
+
private
|
354
|
+
def run_once(queue,auth_token,params_event)
|
355
|
+
|
356
|
+
run_fetcher(queue,auth_token,params_event)
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
def run_fetcher(queue,auth_token,params_event)
|
361
|
+
|
362
|
+
@continue = true
|
363
|
+
|
364
|
+
if auth_token.nil? or auth_token.empty?
|
365
|
+
handle_auth(queue, auth_token)
|
366
|
+
end
|
367
|
+
|
368
|
+
begin
|
369
|
+
|
370
|
+
#loop_count = 0
|
371
|
+
while @continue and !stop?
|
372
|
+
|
373
|
+
if @next_stream_position
|
374
|
+
params_event[:stream_position] = @next_stream_position
|
375
|
+
end
|
376
|
+
|
377
|
+
@logger.debug("Calling URL",
|
378
|
+
:event_url => @event_url,
|
379
|
+
:params => params_event,
|
380
|
+
:auth_set => auth_token.length > 0)
|
381
|
+
|
382
|
+
|
383
|
+
started = Time.now
|
384
|
+
client.async.get(@event_url, params: params_event, headers: {"Authorization" => "Bearer #{auth_token}"}).
|
385
|
+
on_success { |response | handle_success(queue, response, auth_token, @event_url, Time.now - started) }.
|
386
|
+
on_failure { |exception | handle_failure(queue, exception, @event_url, Time.now - started) }
|
387
|
+
|
388
|
+
client.execute!
|
389
|
+
|
390
|
+
#puts loop_count
|
391
|
+
#loop_count += 1
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
# Some clean up magic to cover the stuff below.
|
396
|
+
# This will keep me from stomping on signal interrupts and ctrl+c
|
397
|
+
rescue SignalException, Interrupt, SyntaxError
|
398
|
+
raise
|
399
|
+
|
400
|
+
rescue Exception => e
|
401
|
+
@logger.fatal("Could not call URL",
|
402
|
+
:url => @event_url,
|
403
|
+
:params => params_event,
|
404
|
+
:auth_set => auth_token.length > 0,
|
405
|
+
:exception => e.inspect)
|
406
|
+
raise
|
407
|
+
ensure
|
408
|
+
if (@state_file_base && @state_file != "#{@state_file_base}#{@next_stream_position ||= 'start'}" )
|
409
|
+
begin
|
410
|
+
#puts "Old state file: #{@state_file}"
|
411
|
+
File.rename(@state_file,@state_file_base + @next_stream_position)
|
412
|
+
rescue SignalException, Interrupt, SyntaxError
|
413
|
+
raise
|
414
|
+
rescue Exception => e
|
415
|
+
@logger.fatal("Could not rename file",
|
416
|
+
:old_file => @state_file,
|
417
|
+
:new_file => @state_file_base + @next_stream_position,
|
418
|
+
:exception => e.inspect)
|
419
|
+
raise
|
420
|
+
end
|
421
|
+
@state_file = @state_file_base + @next_stream_position
|
422
|
+
#puts "New state file: #{@state_file}"
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
def handle_auth(queue, auth_token)
|
431
|
+
|
432
|
+
@logger.debug("Authenticating to box.com")
|
433
|
+
## clear out the old auth token if it exists
|
434
|
+
auth_token.clear
|
435
|
+
|
436
|
+
@payload[:exp] = Time.now.to_i + 30
|
437
|
+
|
438
|
+
@logger.debug("Created JWT json",
|
439
|
+
:json => @payload)
|
440
|
+
token = JWT.encode(@payload, @private_key, @algo, @header)
|
441
|
+
|
442
|
+
response = client.post(@auth_url, params: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", client_secret: @client_secret, client_id: @client_id, assertion: token}).
|
443
|
+
on_failure { |exception | handle_failure(queue, exception, @auth_url, nil) }
|
444
|
+
|
445
|
+
begin
|
446
|
+
if response.body.length > 0 and response.code == 200
|
447
|
+
@logger.info("Successfully authenticated to box.com")
|
448
|
+
auth_token << JSON.parse(response.body)["access_token"]
|
449
|
+
#puts auth_token ## TODO: Remove testing code
|
450
|
+
else
|
451
|
+
@continue = false
|
452
|
+
handle_unknown_error(queue, response, nil, nil)
|
453
|
+
end
|
454
|
+
rescue NoMethodError => e
|
455
|
+
@continue = false
|
456
|
+
end
|
457
|
+
|
458
|
+
|
459
|
+
end # end handle_auth
|
460
|
+
|
461
|
+
private
|
462
|
+
def handle_success(queue, response,auth_token, requested_url, exec_time)
|
463
|
+
|
464
|
+
case response.code
|
465
|
+
when 200
|
466
|
+
if response.body.length > 0
|
467
|
+
response_hash = JSON.parse(response.body)
|
468
|
+
#puts "current stream position #{@next_stream_position ||= 'nil'}"
|
469
|
+
#puts "Next stream position #{response_hash["next_stream_position"]}"
|
470
|
+
begin
|
471
|
+
if (@next_stream_position && @next_stream_position == response_hash.fetch("next_stream_position") )
|
472
|
+
@continue = false
|
473
|
+
response_hash["entries"] = []
|
474
|
+
end
|
475
|
+
|
476
|
+
@next_stream_position = response_hash.fetch("next_stream_position")
|
477
|
+
#puts response_hash["chunk_size"]
|
478
|
+
rescue KeyError
|
479
|
+
@logger.error("Could not parse next_stream_position out of the response",:response_body => response_hash)
|
480
|
+
@continue = false
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
else
|
485
|
+
@continue = false
|
486
|
+
response_hash = {"entries" => {} }
|
487
|
+
end
|
488
|
+
|
489
|
+
#response_hash["entries"].each do |entry|
|
490
|
+
@codec.decode(response_hash["entries"].to_json) do |decoded|
|
491
|
+
#event = @target ? LogStash::Event.new(@target => entry) : LogStash::Event.new(entry)
|
492
|
+
event = @target ? LogStash::Event.new(@target => decoded.to_hash) : decoded
|
493
|
+
apply_metadata(event,requested_url, response, exec_time)
|
494
|
+
decorate(event)
|
495
|
+
queue << event
|
496
|
+
end
|
497
|
+
|
498
|
+
when 401
|
499
|
+
|
500
|
+
@logger.warn("Auth failed, calling handle_auth to reauthenticate.")
|
501
|
+
handle_auth(queue, auth_token)
|
502
|
+
|
503
|
+
else
|
504
|
+
|
505
|
+
@continue = false
|
506
|
+
handle_unknown_error(queue,response,requested_url, exec_time)
|
507
|
+
|
508
|
+
end
|
509
|
+
|
510
|
+
end # end handle_success
|
511
|
+
|
512
|
+
def handle_unknown_error(queue,response, requested_url, exec_time)
|
513
|
+
@continue = false
|
514
|
+
|
515
|
+
event_hash = {
|
516
|
+
"Box-Plugin-Status" => "Box.com server error",
|
517
|
+
"Box-Error-Headers" => response.headers,
|
518
|
+
"Box-Error-Code" => response.code,
|
519
|
+
"Box=Error-Msg" => JSON.parse(response.body)["message"],
|
520
|
+
"Box-Error-Raw-Msg" => response.body
|
521
|
+
}
|
522
|
+
|
523
|
+
event = @target ? LogStash::Event.new(@target => event_hash) : LogStash::Event.new(event_hash)
|
524
|
+
event.tag("_box_response_failure")
|
525
|
+
apply_metadata(event,requested_url, response, exec_time)
|
526
|
+
decorate(event)
|
527
|
+
queue << event
|
528
|
+
|
529
|
+
return nil
|
530
|
+
|
531
|
+
end
|
532
|
+
|
533
|
+
|
534
|
+
def handle_failure(queue, exception, requested_url, exec_time)
|
535
|
+
@continue = false
|
536
|
+
@logger.warn("Client connection error",
|
537
|
+
:exception => exception.inspect)
|
538
|
+
|
539
|
+
|
540
|
+
event_message_hash = {
|
541
|
+
"Box-Plugin-Status" => "Client Connection error",
|
542
|
+
"Connection-Error" => exception.message,
|
543
|
+
"backtrace" => exception.backtrace
|
544
|
+
}
|
545
|
+
|
546
|
+
event_hash = {"http_request_failure" => event_message_hash }
|
547
|
+
|
548
|
+
event = @target ? LogStash::Event.new(@target => event_hash) : LogStash::Event.new(event_hash)
|
549
|
+
event.tag("_http_request_failure")
|
550
|
+
apply_metadata(event,requested_url, nil, exec_time)
|
551
|
+
decorate(event)
|
552
|
+
queue << event
|
553
|
+
|
554
|
+
return nil
|
555
|
+
end
|
556
|
+
|
557
|
+
private
|
558
|
+
def apply_metadata(event, requested_url, response=nil, exec_time=nil)
|
559
|
+
return unless @metadata_target
|
560
|
+
|
561
|
+
m = {}
|
562
|
+
|
563
|
+
m = {
|
564
|
+
"host" => @host,
|
565
|
+
"url" => requested_url,
|
566
|
+
"runtime_seconds" => exec_time
|
567
|
+
}
|
568
|
+
|
569
|
+
if response
|
570
|
+
m["code"] = response.code
|
571
|
+
m["response_headers"] = response.headers
|
572
|
+
m["response_message"] = response.message
|
573
|
+
m["retry_count"] = response.times_retried
|
574
|
+
end
|
575
|
+
|
576
|
+
event.set(@metadata_target,m)
|
577
|
+
|
578
|
+
end
|
579
|
+
|
580
|
+
|
581
|
+
public
|
582
|
+
def stop
|
583
|
+
# nothing to do in this case so it is not necessary to define stop
|
584
|
+
# examples of common "stop" tasks:
|
585
|
+
# * close sockets (unblocking blocking reads/accepts)
|
586
|
+
# * cleanup temporary files
|
587
|
+
# * terminate spawned threads
|
588
|
+
begin
|
589
|
+
@scheduler.stop
|
590
|
+
rescue NoMethodError => e
|
591
|
+
unless (e.message == "undefined method `stop' for nil:NilClass")
|
592
|
+
raise
|
593
|
+
end
|
594
|
+
rescue Exception => e
|
595
|
+
@logger.warn("Undefined error", :exception => e.inspect)
|
596
|
+
raise
|
597
|
+
ensure
|
598
|
+
if (@state_file_base && @state_file != "#{@state_file_base}#{@next_stream_position ||= 'start'}" )
|
599
|
+
|
600
|
+
begin
|
601
|
+
#puts "Old state file: #{@state_file}"
|
602
|
+
File.rename(@state_file,@state_file_base + @next_stream_position)
|
603
|
+
rescue SignalException, Interrupt, SyntaxError
|
604
|
+
raise
|
605
|
+
rescue Exception => e
|
606
|
+
@logger.fatal("Could not rename file",
|
607
|
+
:old_file => @state_file,
|
608
|
+
:new_file => @state_file_base + @next_stream_position,
|
609
|
+
:exception => e.inspect)
|
610
|
+
raise
|
611
|
+
|
612
|
+
end
|
613
|
+
@state_file = @state_file_base + @next_stream_position
|
614
|
+
#puts "New state file: #{@state_file}"
|
615
|
+
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end # class LogStash::Inputs::BoxEnterprise
|