logstash-filter-sphinx 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,725 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+
5
+
6
+ require 'pg'
7
+ require 'redis'
8
+ require 'connection_pool'
9
+ require 'ipaddress'
10
+
11
+
12
+ class SphinxDataAccessor
13
+
14
+ def initialize(config)
15
+
16
+ @redis_user_conn = Redis.new(:host => config["redis_host"], :port => config["redis_port"], :db => config["redis_user_db"])
17
+ @redis_record_conn = Redis.new(:host => config["redis_host"], :port => config["redis_port"], :db => config["redis_record_db"])
18
+ @redis_host_conn = Redis.new(:host => config["redis_host"], :port => config["redis_port"], :db => config["redis_host_db"])
19
+ @pg_conn = ConnectionPool::Wrapper.new(size: 8, timeout: 3) { PG::connect(:host => config["pg_host"], :user => config["pg_user"], :password => config["pg_password"], :dbname => config["pg_dbname"]) }
20
+
21
+ end
22
+
23
+ public
24
+ def get_user(access_id, access_key)
25
+
26
+ # sanity check for user inputs
27
+ if access_id.nil? || access_key.nil?
28
+ return nil
29
+ end
30
+
31
+ if access_id.strip == '' || access_key == ''
32
+ return nil
33
+ end
34
+
35
+ # check redis first
36
+ user = get_user_from_redis(access_id, access_key)
37
+ return user if user
38
+
39
+ user = get_user_from_pg(access_id, access_key)
40
+
41
+ if user
42
+ set_user_in_redis(access_id, access_key, user)
43
+ end
44
+
45
+ return user
46
+ end
47
+
48
+
49
+ public
50
+ def add_host(user_id, hostname)
51
+
52
+ # check redis cache first
53
+ host = get_host_from_redis(user_id, hostname)
54
+ return if host
55
+
56
+ # create a host entry if not in the database
57
+ begin
58
+ host = create_host(user_id, hostname)
59
+ insert_host_into_pg(host)
60
+
61
+ set_host_in_redis(user_id, hostname, host)
62
+
63
+ rescue => e
64
+ puts e.message
65
+
66
+ end
67
+
68
+
69
+
70
+ end
71
+
72
+
73
+ public
74
+ def get_record(md5)
75
+
76
+
77
+ # check the redis cache
78
+ record = get_record_from_redis(md5)
79
+ if record
80
+
81
+ # Return it only if the record contains reputation meta data.
82
+ # We learn this by checking the existence of the 'reputation_timestamp' key
83
+ # which is only set by the backend after checking with VT (or other data source)
84
+ if record["reputation_timestamp"]
85
+ puts "#{md5}: Cache hit with data"
86
+ return record
87
+ else
88
+ puts "#{md5}: Cache hit with no data"
89
+ return nil
90
+ end
91
+
92
+ end
93
+
94
+
95
+ # we couldn't find it in the cache. Check the db
96
+ record = get_record_from_pg(md5)
97
+
98
+ if record
99
+
100
+
101
+ # Return it only if the record contains reputation meta data.
102
+ # We learn this by checking the existence of the 'reputation_timestamp' key
103
+ # which is only set by the backend after checking with VT (or other data source)
104
+ if record["reputation_timestamp"]
105
+ puts "#{md5}: DB hit with data"
106
+
107
+ # cache it in redis
108
+ set_record_in_redis(md5, record)
109
+ return record
110
+
111
+ else
112
+ puts "#{md5}: DB hit with no data"
113
+
114
+ empty_record = create_new_record(md5)
115
+ set_record_in_redis(md5, empty_record)
116
+ return nil
117
+ end
118
+
119
+ else
120
+
121
+ puts "#{md5}: NO hit. Inserting a new record into DB and Cache"
122
+
123
+ # Insert this md5 entry into the reference_hash table with a blank reputation timestamp.
124
+ # This way the backend can update this entry accordingly
125
+ record = create_new_record(md5)
126
+ insert_record_into_pg(record)
127
+
128
+ # Insert this into
129
+ set_record_in_redis(md5, record)
130
+
131
+ # NOTE: this new record is not returned to the user as it contains no reputation meta data.
132
+ return nil
133
+
134
+ end
135
+
136
+ return nil
137
+
138
+ end
139
+
140
+ private
141
+ def redis_host_db_key(user_id, hostname)
142
+ "#{user_id}-#{hostname}".strip
143
+ end
144
+
145
+ private
146
+ def get_host_from_redis(user_id, hostname)
147
+ begin
148
+
149
+ key = redis_host_db_key(user_id, hostname)
150
+
151
+ host = @redis_host_conn.hgetall(key) #hgetall returns all fields and values of the hash stored at key
152
+
153
+ return host if host != {}
154
+
155
+ rescue => e
156
+ puts e.message
157
+ end
158
+
159
+ nil
160
+ end
161
+
162
+ private
163
+ def create_host(user_id, hostname)
164
+ {'user_id' => user_id, 'hostname' => hostname}
165
+ end
166
+
167
+ private
168
+ def set_host_in_redis(user_id, hostname, host)
169
+ begin
170
+ key = redis_host_db_key(user_id, hostname)
171
+ @redis_host_conn.mapped_hmset(key, host)
172
+ rescue => e
173
+ puts e.message
174
+ end
175
+ nil
176
+ end
177
+
178
+ private
179
+ def redis_user_db_key(access_id, access_key)
180
+ "#{access_id}-#{access_key}"
181
+ end
182
+
183
+
184
+ private
185
+ def get_user_from_redis(access_id, access_key)
186
+ key = redis_user_db_key(access_id, access_key)
187
+
188
+ begin
189
+
190
+ user = @redis_user_conn.hgetall(key) #hgetall returns all fields and values of the hash stored at key
191
+ return user if user != {}
192
+
193
+ rescue => e
194
+ puts e.message
195
+ end
196
+
197
+ nil
198
+ end
199
+
200
+
201
+
202
+
203
+
204
+ private
205
+ def get_user_from_pg(access_id, access_key)
206
+
207
+ begin
208
+ #TODO
209
+ # @pg_conn.prepare('stmt1', "SELECT * FROM reference_hashes WHERE md5 = $1 LIMIT 1")
210
+ # result = @pg_conn.exec_prepared('stmt1', [md5])
211
+ result = @pg_conn.exec("SELECT users.* from users, data_forwarder_keys WHERE users.id = data_forwarder_keys.user_id AND access_id = '#{access_id}' AND access_key = '#{access_key}' LIMIT 1")
212
+ row = result.first
213
+
214
+ if row
215
+ user = {
216
+ "email" => row['email'],
217
+ 'id' => row["id"],
218
+ }
219
+ return user
220
+ end
221
+
222
+ return nil
223
+
224
+ rescue => e
225
+ puts e.message
226
+ end
227
+
228
+ nil
229
+ end
230
+
231
+ private
232
+ def set_user_in_redis(access_id, access_key, user)
233
+ key = redis_user_db_key(access_id, access_key)
234
+
235
+ begin
236
+ @redis_user_conn.mapped_hmset(key, user)
237
+ rescue => e
238
+ puts e.message
239
+ end
240
+ nil
241
+ end
242
+
243
+ private
244
+ def create_new_host(user_id, hostname)
245
+ {"user_id" => user_id, "hostname" => hostname}
246
+ end
247
+
248
+ def insert_host_into_pg(host)
249
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
250
+ sql = "INSERT INTO hosts (user_id, hostname, created_at, updated_at) VALUES ('#{host['user_id']}', '#{host['hostname']}', '#{timestamp}', '#{timestamp}')"
251
+ result = @pg_conn.exec(sql)
252
+
253
+ nil
254
+ end
255
+
256
+
257
+
258
+ private
259
+ def set_record_in_redis(md5, record)
260
+
261
+ begin
262
+
263
+ @redis_record_conn.mapped_hmset(md5, record)
264
+
265
+ rescue => e
266
+ puts e.message
267
+ end
268
+
269
+ end
270
+
271
+
272
+
273
+ private
274
+ def create_new_record(md5)
275
+ # {"md5" => md5, "wtf_timestamp" => @wtf_ts}
276
+ {"md5" => md5}
277
+ end
278
+
279
+ private
280
+ def insert_record_into_pg(record)
281
+
282
+ md5 = record["md5"]
283
+
284
+ begin
285
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
286
+ sql = "INSERT INTO reference_hashes (md5, source, created_at, updated_at) VALUES ('#{md5}', 'wtf', '#{timestamp}', '#{timestamp}')"
287
+ result = @pg_conn.exec(sql)
288
+
289
+ rescue => e
290
+ puts e.message
291
+ end
292
+
293
+ nil
294
+ end
295
+
296
+
297
+ private
298
+ def get_record_from_redis(md5)
299
+
300
+ begin
301
+
302
+ record = @redis_record_conn.hgetall(md5) #hgetall returns all fields and values of the hash stored at key
303
+
304
+ return record if record != {}
305
+
306
+ rescue => e
307
+ puts e.message
308
+ end
309
+
310
+ nil
311
+ end
312
+
313
+ private
314
+ def get_record_from_pg(md5)
315
+
316
+ begin
317
+
318
+ #TODO
319
+ # @pg_conn.prepare('stmt1', "SELECT * FROM reference_hashes WHERE md5 = $1 LIMIT 1")
320
+ # result = @pg_conn.exec_prepared('stmt1', [md5])
321
+ result = @pg_conn.exec("SELECT * FROM reference_hashes WHERE md5 = '#{md5}' LIMIT 1")
322
+ row = result.first
323
+
324
+
325
+ if row
326
+
327
+ record = {
328
+ "md5" => row['md5'],
329
+ 'source' => row["source"],
330
+ 'reputation' => row["reputation"],
331
+ 'vt_score' => row["vt_score"],
332
+ 'vt_total' => row["vt_total"],
333
+ 'vt_sub_score' => row["vt_sub_score"],
334
+ 'vt_scan_date' => row["vt_scan_date"],
335
+ 'has_vulnerability' => row["has_vulnerability"],
336
+ 'has_verified_signature' => row["has_verified_signature"],
337
+ 'signing_vendor' => row["signing_vendor"],
338
+ 'reputation_timestamp' => row["reputation_timestamp"]
339
+ }
340
+
341
+ return record
342
+
343
+ end
344
+
345
+ return nil
346
+
347
+ rescue => e
348
+
349
+ puts e.message
350
+
351
+ end
352
+
353
+ nil
354
+
355
+ end
356
+
357
+
358
+
359
+ end
360
+
361
+
362
+ class SphinxEventFilterFactory
363
+
364
+ public
365
+ def initialize(config)
366
+ @config = config
367
+ @event_filter_base = SphinxEventFilter.new(config)
368
+ @event_filter_windows = SphinxWindowsEventFilter.new(config)
369
+ @event_filter_windows_sysmon = SphinxWindowsSysmonEventFilter.new(config)
370
+ @event_filter_linux = SphinxLinuxEventFilter.new(config) #TODO
371
+ @event_filter_mac = SphinxMacEventFilter.new(config) #TODO
372
+ end
373
+
374
+ public
375
+ def get_filter(event)
376
+
377
+ platform = event["SphinxPlatform"]
378
+
379
+ case platform
380
+ when 'windows'
381
+ return get_windows_filter(event)
382
+ when 'linux'
383
+ return @event_filter_linux
384
+ when 'mac'
385
+ return @event_filter_mac
386
+ end
387
+
388
+ nil
389
+ end
390
+
391
+ private
392
+ def get_windows_filter(event)
393
+
394
+ event_source = event['SourceName']
395
+
396
+ if event_source == 'Microsoft-Windows-Sysmon'
397
+ return @event_filter_windows_sysmon
398
+ end
399
+
400
+
401
+ return @event_filter_windows
402
+ end
403
+
404
+ end
405
+
406
+
407
+
408
+ class SphinxEventFilter
409
+
410
+ SPHINX_FILTER_VERSION = 1
411
+ SPHINX_FILTER_NAME = 'SphinxEventFilter'
412
+
413
+ def initialize(config)
414
+
415
+ @data_accessor = SphinxDataAccessor.new(config)
416
+
417
+ end
418
+
419
+
420
+ def apply(event)
421
+ raise "Not implemented"
422
+ end
423
+
424
+
425
+ def finalize(event)
426
+ event['SphinxFilterVersion'] = self.class::SPHINX_FILTER_VERSION
427
+ event['SphinxFilterName'] = self.class::SPHINX_FILTER_NAME
428
+ end
429
+
430
+ end
431
+
432
+ class SphinxLinuxEventFilter < SphinxEventFilter
433
+
434
+ SPHINX_FILTER_VERSION = 1
435
+ SPHINX_FILTER_NAME = 'LinuxEventFilter'
436
+ end
437
+
438
+ class SphinxMacEventFilter < SphinxEventFilter
439
+ SPHINX_FILTER_VERSION = 1
440
+ SPHINX_FILTER_NAME = 'MacEventFilter'
441
+ end
442
+
443
+
444
+ class SphinxWindowsEventFilter < SphinxEventFilter
445
+ SPHINX_FILTER_VERSION = 1
446
+ SPHINX_FILTER_NAME = 'WindowsEventFilter'
447
+
448
+ def apply(event)
449
+
450
+
451
+ end
452
+ end
453
+
454
+ class SphinxWindowsSysmonEventFilter < SphinxWindowsEventFilter
455
+ SPHINX_FILTER_VERSION = 1
456
+ SPHINX_FILTER_NAME = 'SysmonEventFilter'
457
+
458
+ def apply(event)
459
+
460
+
461
+ case event['EventID'].to_i
462
+
463
+ # process creation
464
+ when 1
465
+ add_process_name(event)
466
+ add_reputation_data(event)
467
+
468
+ # file creation
469
+ when 2
470
+ add_process_name(event)
471
+ add_target_file_name(event)
472
+ add_reputation_data(event)
473
+
474
+ # network conn
475
+ when 3
476
+ extend_ipaddress(event)
477
+ add_process_name(event)
478
+
479
+ # driver load
480
+ when 6
481
+ add_file_name(event)
482
+ add_reputation_data(event)
483
+
484
+
485
+ # dll load
486
+ when 7
487
+ add_process_name(event)
488
+ add_file_name(event)
489
+ add_reputation_data(event)
490
+
491
+ # remote thread
492
+ when 8
493
+ #TODO
494
+
495
+ end
496
+
497
+ nil
498
+
499
+ end
500
+
501
+
502
+ def extend_ipaddress(event)
503
+
504
+ # src ip
505
+ begin
506
+ ip_str = event['SourceIp']
507
+ ip_addr = IPAddress(ip_str)
508
+
509
+ if ip_addr.ipv6?
510
+ event['SourceIpv6'] = ip_addr.address
511
+ else
512
+ event['SourceIpv4'] = ip_addr.address
513
+ end
514
+
515
+ rescue => e
516
+ puts e.message
517
+ end
518
+
519
+ # dst ip
520
+ begin
521
+ ip_str = event['DestinationIp']
522
+ ip_addr = IPAddress(ip_str)
523
+
524
+ if ip_addr.ipv6?
525
+ event['DestinationIpv6'] = ip_addr.address
526
+ else
527
+ event['DestinationIpv4'] = ip_addr.address
528
+ end
529
+
530
+ rescue => e
531
+ puts e.message
532
+ end
533
+
534
+
535
+ end
536
+
537
+
538
+
539
+ def add_target_file_name(event)
540
+
541
+ image = event['TargetFilename']
542
+ file_name = File.basename(image.gsub("\\","/"))
543
+ event['FileName'] = file_name
544
+
545
+ nil
546
+ end
547
+
548
+ def add_file_name(event)
549
+
550
+ image = event['ImageLoaded']
551
+ file_name = File.basename(image.gsub("\\","/"))
552
+ event['FileName'] = file_name
553
+
554
+ nil
555
+ end
556
+
557
+ def add_process_name(event)
558
+
559
+ image = event['Image']
560
+ process_name = File.basename(image.gsub("\\","/"))
561
+ event['ProcessName'] = process_name
562
+
563
+ nil
564
+ end
565
+
566
+ def add_reputation_data(event)
567
+
568
+ # downcase hash
569
+ md5 = get_downcase_hash(event)
570
+ return nil if (md5.nil? || (md5.strip == ""))
571
+
572
+ event['Hash'] = md5
573
+
574
+ data = @data_accessor.get_record(md5)
575
+
576
+
577
+ if data
578
+
579
+ event['reputation'] = data['reputation']
580
+ event['source'] = data['source']
581
+ event['reputation_timestamp'] = data["reputation_timestamp"]
582
+ event['vt_score'] = data["vt_score"]
583
+ event['vt_total'] = data["vt_total"]
584
+ event['vt_sub_score'] = data["vt_sub_score"]
585
+ event['vt_scan_date'] = data["vt_scan_date"]
586
+ event['has_vulnerability'] = data["has_vulnerability"]
587
+ event['has_verified_signature'] = data["has_verified_signature"]
588
+ event['signing_vendor'] = data["signing_vendor"]
589
+
590
+ end
591
+
592
+ nil
593
+ end
594
+
595
+ def get_downcase_hash(event)
596
+
597
+ if event['Hash']
598
+ return event['Hash'].downcase
599
+
600
+ elsif event['Hashes']
601
+ return event['Hashes'][4,32].downcase #NOTE hardcoded for MD5
602
+ end
603
+
604
+ nil
605
+ end
606
+
607
+
608
+ end
609
+
610
+
611
+
612
+
613
+ # This example filter will replace the contents of the default
614
+ # message field with whatever you specify in the configuration.
615
+ #
616
+ # It is only intended to be used as an example.
617
+ class LogStash::Filters::Sphinx < LogStash::Filters::Base
618
+
619
+ # Setting the config_name here is required. This is how you
620
+ # configure this filter from your Logstash config.
621
+ #
622
+ # filter {
623
+ # example {
624
+ # message => "My message..."
625
+ # }
626
+ # }
627
+ #
628
+ config_name "sphinx"
629
+
630
+ config :pg_host, :required => false, :default => 'localhost'
631
+ config :pg_port, :required => false, :default => 5432
632
+ config :pg_user, :required => true
633
+ config :pg_password, :required => true
634
+ config :pg_dbname, :required => true
635
+
636
+ config :redis_host, :required => false, :default => 'localhost'
637
+ config :redis_port, :required => false, :default => 6379
638
+ config :redis_user_db, :required => false, :default => 1
639
+ config :redis_host_db, :required => false, :default => 2
640
+ config :redis_record_db, :required => false, :default => 3
641
+
642
+
643
+
644
+ public
645
+ def register
646
+
647
+ @data_accessor = SphinxDataAccessor.new(@config)
648
+ @event_filter_factory = SphinxEventFilterFactory.new(@config)
649
+ @logger.debug("Registered sphinx plugin", :type => @type, :config => @config)
650
+
651
+
652
+ end # def register
653
+
654
+
655
+ public
656
+ def filter(event)
657
+
658
+
659
+
660
+ begin
661
+
662
+ # drop nxlog related events
663
+ if is_nxlog_event?(event)
664
+ event.cancel
665
+ return
666
+ end
667
+
668
+
669
+ # auth key check
670
+ user = get_user(event)
671
+ if user
672
+ event['SphinxUserId'] = user["id"]
673
+ else
674
+ event.cancel
675
+ return
676
+ end
677
+
678
+ # insert host to database
679
+ hostname = event["Hostname"]
680
+ add_host_to_db(user['id'], hostname)
681
+
682
+ # remove access_key
683
+ remove_data_forwarder_credential(event)
684
+
685
+ # get event filter
686
+ event_filter = @event_filter_factory.get_filter(event)
687
+
688
+ # apply the filter
689
+ event_filter.apply(event)
690
+ event_filter.finalize(event)
691
+
692
+ rescue => e
693
+ @logger.error("SphinxPlugin: #{e.message}")
694
+ end
695
+
696
+ # filter_matched should go in the last line of our successful code
697
+ filter_matched(event)
698
+ end # def filter
699
+
700
+ private
701
+ def add_host_to_db(user_id, hostname)
702
+ @data_accessor.add_host(user_id, hostname)
703
+ nil
704
+ end
705
+
706
+ private
707
+ def get_user(event)
708
+ access_id = event['SphinxAccessId']
709
+ access_key = event['SphinxAccessKey']
710
+ return @data_accessor.get_user(access_id, access_key)
711
+ end
712
+
713
+
714
+ private
715
+ def is_nxlog_event?(event)
716
+ event['SourceName'] == 'nxlog-ce'
717
+ end
718
+
719
+ private
720
+ def remove_data_forwarder_credential(event)
721
+ event.remove('SphinxAccessId')
722
+ event.remove('SphinxAccessKey')
723
+ end
724
+
725
+ end # class LogStash::Filters::Example