logstash-filter-empowclassifier 0.3.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ require 'aws-sdk'
2
+
3
+ module LogStash
4
+ module Filters
5
+ module Empow
6
+ class CognitoClient
7
+ include LogStash::Util::Loggable
8
+
9
+ def initialize(username, password, aws_region_name, aws_client_id)
10
+ @logger = self.logger
11
+
12
+ @logger.debug("aws region: #{aws_region_name}")
13
+ @logger.debug("aws aws_client_id: #{aws_client_id}")
14
+ @logger.debug("cognito username: #{username}")
15
+
16
+ @username = username
17
+ @password = password
18
+ @aws_region_name = aws_region_name
19
+ @aws_client_id = aws_client_id
20
+
21
+ Aws.config.update({
22
+ region: @aws_region_name,
23
+ credentials: Aws::Credentials.new('aaaa', 'aaaa')
24
+ })
25
+
26
+ @client = Aws::CognitoIdentityProvider::Client.new
27
+ end
28
+
29
+ def authenticate
30
+ resp = @client.initiate_auth({
31
+ auth_flow: "USER_PASSWORD_AUTH",
32
+ auth_parameters: {
33
+ 'USERNAME': @username,
34
+ 'PASSWORD': @password,
35
+ },
36
+ client_id: @aws_client_id,
37
+ })
38
+
39
+ id_token = resp.authentication_result.id_token
40
+ token_type = resp.authentication_result.token_type
41
+
42
+ token = token_type + " " + id_token
43
+ return id_token
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,128 @@
1
+ require 'elasticsearch'
2
+ require 'hashie'
3
+
4
+ module LogStash; module Filters; module Empow;
5
+ class PersistentKeyValueDB
6
+ #include LogStash::Util::Loggable
7
+
8
+ def initialize(hosts, username, password, index)
9
+ #@logger ||= self.logger
10
+
11
+ #@logger.debug("opening the local classification db")
12
+
13
+ @elastic ||= Elasticsearch::Client.new(:hosts => hosts)
14
+ @index = index
15
+
16
+ create_index index
17
+ end
18
+
19
+ def create_index(index)
20
+ return if @elastic.indices.exists? index: index
21
+
22
+ @elastic.indices.create index: index, body: {
23
+ mappings: {
24
+ _doc: {
25
+ properties: {
26
+ product_type: {
27
+ type: 'keyword'
28
+ },
29
+ product: {
30
+ type: 'keyword'
31
+ },
32
+ term_key: {
33
+ type: 'keyword'
34
+ },
35
+ classification: {
36
+ enabled: false
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ end
43
+
44
+ def query(product_type, product, term)
45
+ #@logger.debug("quering local classification db")
46
+
47
+ # fix nil product
48
+ if product.nil?
49
+ product = 'nil_safe_product_key'
50
+ end
51
+
52
+ response = @elastic.search index: @index, type: '_doc', body: {
53
+ query: {
54
+ bool: {
55
+ must: [
56
+ { term: { product_type: product_type } },
57
+ {
58
+ bool: {
59
+ should: [
60
+ {
61
+ bool: {
62
+ must: [
63
+ { term: { term_key: term } },
64
+ { term: { product: product } }
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ bool: {
70
+ must: {
71
+ term: { term_key: term }
72
+ },
73
+ must_not: {
74
+ exists: { field: 'product' }
75
+ }
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ }
85
+
86
+ mash = Hashie::Mash.new response
87
+
88
+ return nil if mash.hits.hits.first.nil?
89
+
90
+ return mash.hits.hits.first._source.classification
91
+ end
92
+
93
+ def save(doc_id, product_type, product, term, classification)
94
+ #@logger.debug("saving key to local classification db")
95
+
96
+ @elastic.index index: @index, type: '_doc', id: doc_id, body: {
97
+ product_type: product_type,
98
+ product: product,
99
+ term_key: term,
100
+ classification: classification
101
+ }
102
+ end
103
+
104
+ def close
105
+ #@logger.debug("clsoing the local classification db")
106
+ end
107
+ end
108
+
109
+ end; end; end
110
+
111
+ =begin
112
+ db = LogStash::Filters::Empow::PersistentKeyValueDB.new('192.168.3.24:9200', 'user', 'pass', 'key-val-8')
113
+
114
+ db.save("am", "p3", "dummy signature", "v1")
115
+ db.save("am", "p3", "dummy signature 2", "v1")
116
+
117
+ db.save("am", "p1", "dummy", "v1")
118
+ db.save("am", nil, "dummy", "v1")
119
+ p db.query "am", "p1", "h1"
120
+ db.save("am", "p1", "h1", "v1")
121
+ p db.query "am", "p1", "h1"
122
+ p db.query "am", "p1", "h2"
123
+ p db.query "am", "no-such-product", "h1"
124
+ p db.query "am", nil, "h1"
125
+ p db.query "am", nil, "dummy"
126
+
127
+ p db.query "am", "p3", "dummy signature 2"
128
+ =end
@@ -0,0 +1,249 @@
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::EmpowClassifier < LogStash::Filters::Base
14
+
15
+ config_name "empowclassifier"
16
+
17
+ # The mail address used when registering to the classification center.
18
+ config :username, :validate => :string, :required => true
19
+
20
+ # The password for the classification center.
21
+ config :password, :validate => :string, :required => true
22
+
23
+ # user pool id. this parameter needs to be set only when using an entire empow stack, open source users should leave this unchanged.
24
+ config :pool_id, :validate => :string, :default => '8dljcvt4jfif762le0ald6j'
25
+
26
+ # Size of the local response cache
27
+ config :cache_size, :validate => :number, :default => 10000
28
+
29
+ # The maximum number of events that may wait in memory for a classification result from the classification center
30
+ config :max_pending_requests, :validate => :number, :default => 10000
31
+
32
+ # Time to wait in seconds an event will wait for a classification before returning to the pipeline with no result
33
+ config :pending_request_timeout, :validate => :number, :default => 60
34
+
35
+ # Max number of concurrent threads classifying via the classification center
36
+ # These threads mostly wait on I/O during the web request, and aren't cpu intensive.
37
+ # Idle workers are closed after one minute, only one idle worker remains alive for incoming request on peace time.
38
+ config :max_classification_center_workers, :validate => :number, :default => 5
39
+
40
+ # Classfication center bulk request size
41
+ config :bulk_request_size, :validate => :number, :default => 50
42
+
43
+ # Seconds to wait for batch to fill up before querying the classification center.
44
+ config :bulk_request_interval, :validate => :number, :default => 2
45
+
46
+ # Max number of times each request will be query the classification center.
47
+ config :max_query_retries, :validate => :number, :default => 5
48
+
49
+ # Seconds to wait before reclassifying an in-progress request. In progress response will occur when the classification center is processing a new threat.
50
+ config :time_between_queries, :validate => :number, :default => 10
51
+
52
+ # Allows renaimg the log field containing the log's product type. Possible values are AM for Anti-Malware and IDS for Intrusion Detection systems.
53
+ # For example, if our log contained a 'log_type' field (instead of the expected product_type field),
54
+ # We would configure the plugin as follows:
55
+ # [source,ruby]
56
+ # filter {
57
+ # empowclassifier {
58
+ # username => "happy"
59
+ # password => "festivus"
60
+ # product_type_field => "log_type"
61
+ # }
62
+ # }
63
+ config :product_type_field, :validate => :string, :default => "product_type"
64
+
65
+ # Allows renaimg the log field containing the log's product name.
66
+ # Assuming our log contained a 'product' field (instead of the expected product_name field),
67
+ # We would configure the plugin as follows:
68
+ # [source,ruby]
69
+ # filter {
70
+ # empowclassifier {
71
+ # username => "happy"
72
+ # password => "festivus"
73
+ # product_type_field => "product"
74
+ # }
75
+ # }
76
+ config :product_name_field, :validate => :string, :default => "product_name"
77
+ config :threat_field, :validate => :string, :default => "threat"
78
+
79
+ # Configs the name of the field used to indicate whether the source described in the log was within the internal network.
80
+ # Example:
81
+ # [source,ruby]
82
+ # filter {
83
+ # empowclassifier {
84
+ # ...
85
+ # src_internal_field => "internal_src"
86
+ # }
87
+ # }
88
+ config :src_internal_field, :validate => :string, :default => "is_src_internal"
89
+
90
+ # Configs the name of the field used to indicate whether the destination described in the log was within the internal network.
91
+ # Example:
92
+ # [source,ruby]
93
+ # filter {
94
+ # empowclassifier {
95
+ # ...
96
+ # dst_internal_field => "internal_dst"
97
+ # }
98
+ # }
99
+ config :dst_internal_field, :validate => :string, :default => "is_dst_internal"
100
+
101
+ # changes the api root for customers of the commercial empow stack
102
+ config :base_url, :validate => :string, :default => ""
103
+
104
+ config :async_local_cache, :validate => :boolean, :default => true
105
+
106
+ # elastic config params
107
+ ########################
108
+
109
+ config :elastic_hosts, :validate => :array
110
+
111
+ # The index or alias to write to
112
+ config :elastic_index, :validate => :string, :default => "empow-intent-db"
113
+
114
+ config :elastic_user, :validate => :string
115
+ config :elastic_password, :validate => :password
116
+
117
+ # failure tags
118
+ ###############
119
+ config :tag_on_product_type_failure, :validate => :array, :default => ['_empow_no_product_type']
120
+ config :tag_on_signature_failure, :validate => :array, :default => ['_empow_no_signature']
121
+ config :tag_on_timeout, :validate => :array, :default => ['_empow_classifer_timeout']
122
+ config :tag_on_error, :validate => :array, :default => ['_empow_classifer_error']
123
+
124
+ CLASSIFICATION_URL = "https://s0apxz9wik.execute-api.us-east-2.amazonaws.com" #"https://intent.cloud.empow.co"
125
+ CACHE_TTL = (24*60*60)
126
+
127
+ public
128
+ def register
129
+ @logger.info("registering empow classifcation plugin")
130
+
131
+ validate_params()
132
+
133
+ local_db = create_local_database
134
+
135
+ local_classifier = LogStash::Filters::Empow::LocalClassifier.new(@cache_size, CACHE_TTL, @async_local_cache, local_db)
136
+
137
+ base_url = get_effective_url()
138
+ online_classifier = LogStash::Filters::Empow::ClassificationCenterClient.new(@username, @password, @pool_id, base_url)
139
+
140
+ 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)
141
+
142
+ field_handler = LogStash::Filters::Empow::FieldHandler.new(@product_type_field, @product_name_field, @threat_field, @src_internal_field, @dst_internal_field)
143
+
144
+ @plugin_core ||= LogStash::Filters::Empow::PluginLogic.new(classifer, field_handler, @pending_request_timeout, @max_pending_requests, @tag_on_timeout, @tag_on_error)
145
+
146
+ @logger.info("empow classifcation plugin registered")
147
+ end # def register
148
+
149
+ private
150
+ def get_effective_url
151
+ if (@base_url.nil? or @base_url.strip == 0)
152
+ return CLASSIFICATION_URL
153
+ end
154
+
155
+ return CLASSIFICATION_URL
156
+ end
157
+
158
+ private
159
+ def validate_params
160
+ raise ArgumentError, 'threat field cannot be empty' if LogStash::Filters::Empow::Utils.is_blank_string(@threat_field)
161
+
162
+ raise ArgumentError, 'bulk_request_size must be an positive number between 1 and 1000' if (@bulk_request_size < 1 or @bulk_request_size > 1000)
163
+
164
+ raise ArgumentError, 'bulk_request_interval must be an greater or equal to 1' if (@bulk_request_interval < 1)
165
+ end
166
+
167
+ def close
168
+ @logger.info("closing the empow classifcation plugin")
169
+
170
+ @plugin_core.close
171
+
172
+ @logger.info("empow classifcation plugin closed")
173
+ end
174
+
175
+ def periodic_flush
176
+ true
177
+ end
178
+
179
+ public def flush(options = {})
180
+ @logger.debug("entered flush")
181
+
182
+ events_to_flush = []
183
+
184
+ begin
185
+ parked_events = @plugin_core.flush(options)
186
+
187
+ parked_events.each do |event|
188
+ # need to clone as original event was canceled
189
+ cloned_event = event.clone
190
+ events_to_flush << cloned_event
191
+ end
192
+
193
+ rescue StandardError => e
194
+ @logger.error("encountered an exception while processing flush. #{e}")
195
+ end
196
+
197
+ @logger.debug("flush ended", :flushed_event_count => events_to_flush.length)
198
+
199
+ return events_to_flush
200
+ end
201
+
202
+ public def filter(event)
203
+ res = event
204
+
205
+ begin
206
+ res = @plugin_core.classify(event)
207
+
208
+ if res.nil?
209
+ event.cancel # don't stream this event just yet ...
210
+ return
211
+ end
212
+
213
+ # event was classified and returned, not some overflow event
214
+ if res.equal? event
215
+ filter_matched(event)
216
+ return
217
+ end
218
+
219
+ # got here with a parked event
220
+ res = res.clone
221
+ filter_matched(res)
222
+
223
+ @logger.debug("filter matched for overflow event", :event => res)
224
+
225
+ rescue StandardError => e
226
+ @logger.error("encountered an exception while classifying", :error => e, :event => event, :backtrace => e.backtrace)
227
+
228
+ @tag_on_error.each{|tag| event.tag(tag)}
229
+
230
+ return res
231
+ end
232
+ end # def filter
233
+
234
+ private def create_local_database
235
+ # if no elastic host has been configured, no local db should be used
236
+ if @elastic_hosts.nil?
237
+ @logger.info("no local persisted cache is configured")
238
+ return nil
239
+ end
240
+
241
+ begin
242
+ return LogStash::Filters::Empow::PersistentKeyValueDB.new(:elastic_hosts, :elastic_user, :elastic_password, :elastic_index)
243
+ rescue StandardError => e
244
+ @logger.error("caught an exception while trying to configured persisted cache", e)
245
+ end
246
+
247
+ return nil
248
+ end
249
+ end
@@ -0,0 +1,127 @@
1
+ require_relative "classification-request"
2
+ require_relative "utils"
3
+
4
+ class LogStash::Filters::Empow::FieldHandler
5
+
6
+ IDS = "IDS"
7
+ AM = "AM"
8
+ CUSTOM = "CUSTOM"
9
+
10
+ public
11
+ def initialize(product_type_field, product_name_field, threat_field, src_internal_field, dst_internal_field)
12
+ @product_type_field = product_type_field
13
+ @product_name_field = product_name_field
14
+
15
+ if threat_field.nil? || threat_field.strip.length == 0
16
+ raise ArgumentError, 'threat field cannot be empty'
17
+ end
18
+
19
+ @threat_field = '[' + threat_field + ']'
20
+
21
+ @ids_signature_field = @threat_field + '[signature]'
22
+ @malware_name_field = @threat_field + '[malware_name]'
23
+
24
+ @src_internal_field = @threat_field + '[' + src_internal_field + ']'
25
+ @dst_internal_field = @threat_field + '[' + dst_internal_field + ']'
26
+
27
+ @hash_field = @threat_field + '[hash]'
28
+ end
29
+
30
+ public
31
+ def event_to_classification_request(event)
32
+ product_type = event.get(@product_type_field)
33
+ product = event.get(@product_name_field)
34
+ is_src_internal = event.get(@src_internal_field)
35
+ is_dst_internal = event.get(@dst_internal_field)
36
+
37
+ if product_type.nil?
38
+ LogStash::Filters::Empow::Utils.add_error(event, "missing_product_type")
39
+ return nil
40
+ end
41
+
42
+ is_src_internal = LogStash::Filters::Empow::Utils.convert_to_boolean(is_src_internal)
43
+
44
+ if is_src_internal.nil?
45
+ is_src_internal = true
46
+ LogStash::Filters::Empow::Utils.add_warn(event, 'src_internal_wrong_value')
47
+ end
48
+
49
+ is_dst_internal = LogStash::Filters::Empow::Utils.convert_to_boolean(is_dst_internal)
50
+
51
+ if is_dst_internal.nil?
52
+ is_dst_internal = true
53
+ LogStash::Filters::Empow::Utils.add_warn(event, 'dst_internal_wrong_value')
54
+ end
55
+
56
+ case product_type
57
+ when IDS
58
+ return nil if !is_valid_ids_request(product, event)
59
+ when AM
60
+ return nil if !is_valid_antimalware_request(product, event)
61
+ else # others are resolved in the cloud
62
+ return nil if !is_valid_product(product, event)
63
+ end
64
+
65
+ original_threat = event.get(@threat_field)
66
+
67
+ threat = copy_threat(original_threat)
68
+
69
+ if (threat.nil?)
70
+ LogStash::Filters::Empow::Utils.add_error(event, "missing_threat_field")
71
+ return nil
72
+ end
73
+
74
+ threat['is_src_internal'] = is_src_internal
75
+ threat['is_dst_internal'] = is_dst_internal
76
+
77
+ return LogStash::Filters::Empow::ClassificationRequest.new(product_type, product, threat)
78
+ end
79
+
80
+ private
81
+ def copy_threat(threat)
82
+ return nil if (threat.nil? or threat.size == 0)
83
+
84
+ res = Hash.new
85
+
86
+ threat.each do |k, v|
87
+ res[k] = v
88
+ end
89
+
90
+ return res
91
+ end
92
+
93
+ private
94
+ def is_valid_ids_request(product, event)
95
+ sid = event.get(@ids_signature_field)
96
+
97
+ if sid.nil? || sid.strip.length == 0
98
+ LogStash::Filters::Empow::Utils.add_error(event, "missing_ids_signature")
99
+ return false
100
+ end
101
+
102
+ return is_valid_product(product, event)
103
+ end
104
+
105
+ private
106
+ def is_valid_product(product, event)
107
+ if (product.nil? or product.strip.length == 0)
108
+ LogStash::Filters::Empow::Utils.add_error(event, "missing_product_name")
109
+ return false
110
+ end
111
+
112
+ return true
113
+ end
114
+
115
+ private
116
+ def is_valid_antimalware_request(product, event)
117
+ malware_name = event.get(@malware_name_field)
118
+ malware_hash = event.get(@hash_field)
119
+
120
+ if malware_hash.nil? and (malware_name.nil? or product.nil?)
121
+ LogStash::Filters::Empow::Utils.add_error(event, "anti_malware_missing_hash_or_name")
122
+ return false
123
+ end
124
+
125
+ return true
126
+ end
127
+ end