logstash-filter-threats_classifier 1.0.4

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,36 @@
1
+ module LogStash
2
+ module Filters
3
+ module Empow
4
+ class AbstractResponse
5
+ attr_reader :response, :is_successful, :is_final
6
+
7
+ def initialize(response, is_successful, is_final)
8
+ @response = response
9
+ @is_successful = is_successful
10
+ @is_final = is_final
11
+ end
12
+ end
13
+
14
+ class FailureResponse < AbstractResponse
15
+ def initialize(response)
16
+ super(response, false, true)
17
+ end
18
+ end
19
+
20
+ class UnauthorizedReponse < FailureResponse
21
+ end
22
+
23
+ class SuccessfulResponse < AbstractResponse
24
+ def initialize(response)
25
+ super(response, true, true)
26
+ end
27
+ end
28
+
29
+ class InProgressResponse < AbstractResponse
30
+ def initialize(response)
31
+ super(response, true, false)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,230 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "elasticsearch"
4
+
5
+ require_relative "elastic-db"
6
+ require_relative "local-classifier"
7
+ require_relative "classifier"
8
+ require_relative "center-client"
9
+ require_relative "plugin-logic"
10
+ require_relative "utils"
11
+
12
+ #
13
+ class LogStash::Filters::Threats_Classifier < LogStash::Filters::Base
14
+
15
+ config_name "threats_classifier"
16
+
17
+ # The username (typically your email address), to access the classification center
18
+ config :username, :validate => :string, :required => true
19
+
20
+ # The password to access the classification center
21
+ config :password, :validate => :string, :required => true
22
+
23
+ # Set this value only if using the complete empow stack; leave unchanged if using the empow Elastic open source plugin or module
24
+ config :authentication_hash, :validate => :string, :default => '131n94ktfg7lj8hlpnnbkuiql1'
25
+
26
+ # The number of responses cached locally
27
+ config :cache_size, :validate => :number, :default => 10000
28
+
29
+ # Max number of requests pending response from the classification center
30
+ config :max_pending_requests, :validate => :number, :default => 10000
31
+
32
+ # Timeout for response from classification center (seconds)
33
+ config :pending_request_timeout, :validate => :number, :default => 60
34
+
35
+ # Maximum number of concurrent threads (workers) classifying logs using the classification center
36
+ config :max_classification_center_workers, :validate => :number, :default => 5
37
+
38
+ # Classification center bulk request size (requests)
39
+ config :bulk_request_size, :validate => :number, :default => 50
40
+
41
+ # Time (seconds) to wait for batch to fill on classifciation center, before querying for the response
42
+ config :bulk_request_interval, :validate => :number, :default => 2
43
+
44
+ # Max number of classification center request retries
45
+ config :max_query_retries, :validate => :number, :default => 5
46
+
47
+ # Time (seconds) to wait between queries to the classification center for the final response to a request; the classification center will return an 'in-progress' response if queried before the final response is ready
48
+ config :time_between_queries, :validate => :number, :default => 10
49
+
50
+ # The name of the product type field in the log
51
+ # Example: If the log used log_type for the product type, configure the plugin like this:
52
+ # [source,ruby]
53
+ # filter {
54
+ # empowclassifier {
55
+ # username => "happy"
56
+ # password => "festivus"
57
+ # product_type_field => "log_type"
58
+ # }
59
+ # }
60
+ config :product_type_field, :validate => :string, :default => "product_type"
61
+
62
+ # The name of the product name field in the log
63
+ # Example: If the log used product for the product name, configure the plugin like this:
64
+ # [source,ruby]
65
+ # filter {
66
+ # empowclassifier {
67
+ # username => "happy"
68
+ # password => "festivus"
69
+ # product_name_field => "product"
70
+ # }
71
+ # }
72
+ config :product_name_field, :validate => :string, :default => "product_name"
73
+
74
+ # The name of the field containing the terms sent to the classification center
75
+ config :threat_field, :validate => :string, :default => "threat"
76
+
77
+ # Indicates whether the source field is internal to the user’s network (for example, an internal host/mail/user/app)
78
+ config :src_internal_field, :validate => :string, :default => "is_src_internal"
79
+
80
+ # Indicates whether the dest field is internal to the user’s network (for example, an internal host/mail/user/app)
81
+ config :dst_internal_field, :validate => :string, :default => "is_dst_internal"
82
+
83
+ # changes the api root for customers of the commercial empow stack
84
+ config :base_url, :validate => :string, :default => ""
85
+
86
+ config :async_local_cache, :validate => :boolean, :default => true
87
+
88
+ # elastic config params
89
+ ########################
90
+
91
+ config :elastic_hosts, :validate => :array
92
+
93
+ # The index or alias to write to
94
+ config :elastic_index, :validate => :string, :default => "empow-intent-db"
95
+
96
+ config :elastic_user, :validate => :string
97
+ config :elastic_password, :validate => :password
98
+
99
+ # failure tags
100
+ ###############
101
+ config :tag_on_product_type_failure, :validate => :array, :default => ['_empow_no_product_type']
102
+ config :tag_on_signature_failure, :validate => :array, :default => ['_empow_no_signature']
103
+ config :tag_on_timeout, :validate => :array, :default => ['_empow_classifier_timeout']
104
+ config :tag_on_error, :validate => :array, :default => ['_empow_classifier_error']
105
+
106
+ CLASSIFICATION_URL = 'https://intent.cloud.empow.co'
107
+ CACHE_TTL = (24*60*60)
108
+
109
+ public
110
+ def register
111
+ @logger.info("registering empow classifcation plugin")
112
+
113
+ validate_params()
114
+
115
+ local_db = create_local_database
116
+
117
+ local_classifier = LogStash::Filters::Empow::LocalClassifier.new(@cache_size, CACHE_TTL, @async_local_cache, local_db)
118
+
119
+ base_url = get_effective_url()
120
+ online_classifier = LogStash::Filters::Empow::ClassificationCenterClient.new(@username, @password, @authentication_hash, base_url)
121
+
122
+ classifer = LogStash::Filters::Empow::Classifier.new(online_classifier, local_classifier, @max_classification_center_workers, @bulk_request_size, @bulk_request_interval, @max_query_retries, @time_between_queries)
123
+
124
+ field_handler = LogStash::Filters::Empow::FieldHandler.new(@product_type_field, @product_name_field, @threat_field, @src_internal_field, @dst_internal_field)
125
+
126
+ @plugin_core ||= LogStash::Filters::Empow::PluginLogic.new(classifer, field_handler, @pending_request_timeout, @max_pending_requests, @tag_on_timeout, @tag_on_error)
127
+
128
+ @logger.info("empow classifcation plugin registered")
129
+ end # def register
130
+
131
+ private
132
+ def get_effective_url
133
+ if (@base_url.nil? or @base_url.strip == 0)
134
+ return CLASSIFICATION_URL
135
+ end
136
+
137
+ return CLASSIFICATION_URL
138
+ end
139
+
140
+ private
141
+ def validate_params
142
+ raise ArgumentError, 'threat field cannot be empty' if LogStash::Filters::Empow::Utils.is_blank_string(@threat_field)
143
+
144
+ raise ArgumentError, 'bulk_request_size must be an positive number between 1 and 1000' if (@bulk_request_size < 1 or @bulk_request_size > 1000)
145
+
146
+ raise ArgumentError, 'bulk_request_interval must be an greater or equal to 1' if (@bulk_request_interval < 1)
147
+ end
148
+
149
+ def close
150
+ @logger.info("closing the empow classifcation plugin")
151
+
152
+ @plugin_core.close
153
+
154
+ @logger.info("empow classifcation plugin closed")
155
+ end
156
+
157
+ def periodic_flush
158
+ true
159
+ end
160
+
161
+ public def flush(options = {})
162
+ @logger.debug("entered flush")
163
+
164
+ events_to_flush = []
165
+
166
+ begin
167
+ parked_events = @plugin_core.flush(options)
168
+
169
+ parked_events.each do |event|
170
+ event.uncancel
171
+
172
+ events_to_flush << event
173
+ end
174
+
175
+ rescue StandardError => e
176
+ @logger.error("encountered an exception while processing flush", :error => e)
177
+ end
178
+
179
+ @logger.debug("flush ended", :flushed_event_count => events_to_flush.length)
180
+
181
+ return events_to_flush
182
+ end
183
+
184
+ public def filter(event)
185
+ res = event
186
+
187
+ begin
188
+ res = @plugin_core.classify(event)
189
+
190
+ if res.nil?
191
+ return
192
+ end
193
+
194
+ # event was classified and returned, not some overflow event
195
+ if res.equal? event
196
+ filter_matched(event)
197
+
198
+ return
199
+ end
200
+
201
+ # got here with a parked event
202
+ filter_matched(res)
203
+
204
+ @logger.debug("filter matched for overflow event", :event => res)
205
+
206
+ yield res
207
+
208
+ rescue StandardError => e
209
+ @logger.error("encountered an exception while classifying", :error => e, :event => event, :backtrace => e.backtrace)
210
+
211
+ @tag_on_error.each{|tag| event.tag(tag)}
212
+ end
213
+ end # def filter
214
+
215
+ private def create_local_database
216
+ # if no elastic host has been configured, no local db should be used
217
+ if @elastic_hosts.nil?
218
+ @logger.info("no local persisted cache is configured")
219
+ return nil
220
+ end
221
+
222
+ begin
223
+ return LogStash::Filters::Empow::PersistentKeyValueDB.new(:elastic_hosts, :elastic_user, :elastic_password, :elastic_index)
224
+ rescue StandardError => e
225
+ @logger.error("caught an exception while trying to configured persisted cache", e)
226
+ end
227
+
228
+ return nil
229
+ end
230
+ end
@@ -0,0 +1,46 @@
1
+ module LogStash; module Filters; module Empow
2
+
3
+ class Utils
4
+ TRUTHY_VALUES = [true, 1, '1']
5
+ FALSEY_VALUES = [false, 0, '0']
6
+
7
+ def self.is_blank_string(txt)
8
+ return (txt.nil? or txt.strip.length == 0)
9
+ end
10
+
11
+ def self.convert_to_boolean(val)
12
+ return nil if val.nil?
13
+
14
+ return true if TRUTHY_VALUES.include?(val)
15
+
16
+ return false if FALSEY_VALUES.include?(val)
17
+
18
+ return true if (val.is_a?(String) and val.downcase.strip == 'true')
19
+
20
+ return false if (val.is_a?(String) and val.downcase.strip == 'false')
21
+
22
+ return nil
23
+ end
24
+
25
+ def self.add_error(event, msg)
26
+ tag_empow_messages(event, msg, 'empow_errors')
27
+ end
28
+
29
+ def self.add_warn(event, msg)
30
+ tag_empow_messages(event, msg, 'empow_warnings')
31
+ end
32
+
33
+ private
34
+ def self.tag_empow_messages(event, msg, block)
35
+ messages = event.get(block)
36
+
37
+ # using arrayinstead of set, as set raises a logstash exception:
38
+ # No enum constant org.logstash.bivalues.BiValues.ORG_JRUBY_RUBYOBJECTVAR0
39
+ messages ||= Array.new
40
+ messages << msg
41
+
42
+ event.set(block, messages.uniq)
43
+ end
44
+ end
45
+
46
+ end; end; end
@@ -0,0 +1,38 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-filter-threats_classifier'
3
+ s.version = '1.0.4'
4
+ s.licenses = ['Apache-2.0']
5
+ s.summary = 'Returns classification information for attacks from the empow classification center, based on information in log strings'
6
+ #s.description = 'Write a longer description or delete this line.'
7
+ s.homepage = 'http://www.empow.co'
8
+ s.authors = ['empow', 'Assaf Abulafia', 'Rami Cohen']
9
+ s.email = ''
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
+
16
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
+
18
+ # Special flag to let us know this is actually a logstash plugin
19
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "filter" }
20
+
21
+ # Gem dependencies
22
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
23
+ s.add_runtime_dependency 'rest-client', '~> 1.8', '>= 1.8.0'
24
+ s.add_runtime_dependency 'lru_redux', '~> 1.1', '>= 1.1.0'
25
+ s.add_runtime_dependency 'json', '~> 1.8', '>= 1.8'
26
+ #s.add_runtime_dependency 'rufus-scheduler'
27
+ s.add_runtime_dependency 'hashie'
28
+ #s.add_runtime_dependency "murmurhash3"
29
+
30
+ s.add_development_dependency 'aws-sdk', '~> 3'
31
+
32
+ s.add_development_dependency 'logstash-devutils'
33
+ # s.add_runtime_dependency 'jwt', '~> 2.1', '>= 2.1.0'
34
+ s.add_development_dependency "timecop", "~> 0.7"
35
+ s.add_development_dependency "webmock", "~> 1.22", ">= 1.21.0"
36
+
37
+ s.add_development_dependency 'elasticsearch'
38
+ end
@@ -0,0 +1,92 @@
1
+ require_relative '../spec_helper'
2
+ require "logstash/filters/classifier"
3
+ require "logstash/filters/local-classifier"
4
+ require "logstash/filters/classification-request"
5
+ require "logstash/filters/center-client"
6
+ require "logstash/filters/response"
7
+ require 'timecop'
8
+
9
+ describe LogStash::Filters::Empow::Classification::BulkProcessor do
10
+ #empow_user, empow_password, cache_size, ttl, async_local_db, elastic_hosts, elastic_index, elastic_username, elastic_password
11
+ let(:time_between_attempts) { 1 }
12
+ let(:batch_size) { 10 }
13
+ let(:max_retries) { 5 }
14
+
15
+ describe "test with mocked classifiers" do
16
+ it "single failed log" do
17
+
18
+ Timecop.freeze(Time.now)
19
+
20
+ req1 = "request1"
21
+ val1 = {}
22
+ val1[:retries] = 1
23
+ val1[:task] = nil
24
+ val1[:request] = req1
25
+ val1[:last_executed] = Time.at(310953600)
26
+
27
+ requests = Hash.new
28
+ requests[req1] = val1
29
+
30
+ local_classifier = instance_double(LogStash::Filters::Empow::LocalClassifier)
31
+ allow(local_classifier).to receive(:classify).and_return(nil)
32
+ allow(local_classifier).to receive(:close)
33
+
34
+ center_result = {}
35
+ center_result[req1] = LogStash::Filters::Empow::FailureReponse.new("failure1")
36
+
37
+ online_classifer = instance_double(LogStash::Filters::Empow::ClassificationCenterClient)
38
+ allow(online_classifer).to receive(:classify).and_return(center_result)
39
+
40
+ bulk_processor = described_class.new(max_retries, batch_size, time_between_attempts, requests, online_classifer, local_classifier)
41
+
42
+ expect(online_classifer).to receive(:classify)
43
+ expect(local_classifier).to receive(:add_to_cache)
44
+
45
+ bulk_processor.execute
46
+
47
+ #expect(local_classifier).to receive(:add_to_cache)
48
+
49
+ # expect(res).to be_nil
50
+ #save_to_cache_and_db
51
+
52
+ expect(requests[req1]).to be_nil
53
+
54
+ #Timecop.freeze(Time.now + time_between_attempts)
55
+ #Timecop.freeze(Time.now + 1 + time_between_attempts)
56
+ end
57
+
58
+ it "single successful log" do
59
+
60
+ Timecop.freeze(Time.now)
61
+
62
+ req1 = "request1"
63
+ val1 = {}
64
+ val1[:retries] = 1
65
+ val1[:task] = nil
66
+ val1[:request] = req1
67
+ val1[:last_executed] = Time.at(310953600)
68
+
69
+ requests = Hash.new
70
+ requests[req1] = val1
71
+
72
+ local_classifier = instance_double(LogStash::Filters::Empow::LocalClassifier)
73
+ allow(local_classifier).to receive(:classify).and_return(nil)
74
+ allow(local_classifier).to receive(:close)
75
+
76
+ center_result = {}
77
+ center_result[req1] = LogStash::Filters::Empow::SuccessfulReponse.new("result1")
78
+
79
+ online_classifer = instance_double(LogStash::Filters::Empow::ClassificationCenterClient)
80
+ allow(online_classifer).to receive(:classify).and_return(center_result)
81
+
82
+ bulk_processor = described_class.new(max_retries, batch_size, time_between_attempts, requests, online_classifer, local_classifier)
83
+
84
+ expect(online_classifer).to receive(:classify)
85
+ expect(local_classifier).to receive(:save_to_cache_and_db)
86
+
87
+ bulk_processor.execute
88
+
89
+ expect(requests[req1]).to be_nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../spec_helper'
2
+ require 'timecop'
3
+ require "logstash/filters/classifier-cache"
4
+
5
+ describe LogStash::Filters::Empow::ClassifierCache do
6
+
7
+ describe "initialize signaure test" do
8
+ it "test expiration by cache default ttl" do
9
+ cache = described_class.new(5, 60)
10
+
11
+ expect(cache.classify("k")).to be_nil
12
+
13
+ Timecop.freeze(Time.now)
14
+
15
+ cache.put("k", "v", Time.now + 24*60*60)
16
+
17
+ Timecop.freeze(Time.now + 59)
18
+
19
+ expect(cache.classify("k")).to eq("v")
20
+
21
+ Timecop.freeze(Time.now + 61)
22
+
23
+ expect(cache.classify("k")).to be_nil
24
+ end
25
+
26
+ it "test expiration by entry ttl" do
27
+ cache = described_class.new(5, 60)
28
+
29
+ expect(cache.classify("k")).to be_nil
30
+
31
+ Timecop.freeze(Time.now)
32
+
33
+ cache.put("k", "v", Time.now + 30)
34
+
35
+ Timecop.freeze(Time.now + 29)
36
+
37
+ expect(cache.classify("k")).to eq("v")
38
+
39
+ Timecop.freeze(Time.now + 31)
40
+
41
+ expect(cache.classify("k")).to be_nil
42
+ end
43
+ end
44
+ end