logstash-filter-sphinx 0.0.3

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