logstash-filter-empowclassifier 0.3.15

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,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