logstash-filter-threats_classifier 1.0.4

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