fluent-plugin-windows-eventlog 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ require 'winevt'
2
+ require 'fluent/plugin/input'
3
+ require 'fluent/plugin'
4
+
5
+ module Fluent::Plugin
6
+ class WindowsEventLog2Input < Input
7
+ Fluent::Plugin.register_input('windows_eventlog2', self)
8
+
9
+ helpers :timer, :storage, :parser
10
+
11
+ DEFAULT_STORAGE_TYPE = 'local'
12
+ KEY_MAP = {"ProviderName" => ["ProviderName", :string],
13
+ "ProviderGUID" => ["ProviderGUID", :string],
14
+ "EventID" => ["EventID", :string],
15
+ "Qualifiers" => ["Qualifiers", :string],
16
+ "Level" => ["Level", :string],
17
+ "Task" => ["Task", :string],
18
+ "Opcode" => ["Opcode", :string],
19
+ "Keywords" => ["Keywords", :string],
20
+ "TimeCreated" => ["TimeCreated", :string],
21
+ "EventRecordID" => ["EventRecordID", :string],
22
+ "ActivityID" => ["ActivityID", :string],
23
+ "RelatedActivityID" => ["RelatedActivityID", :string],
24
+ "ThreadID" => ["ThreadID", :string],
25
+ "Channel" => ["Channel", :string],
26
+ "Computer" => ["Computer", :string],
27
+ "UserID" => ["UserID", :string],
28
+ "Version" => ["Version", :string],
29
+ "Description" => ["Description", :string],
30
+ "EventData" => ["EventData", :array]}
31
+
32
+ config_param :tag, :string
33
+ config_param :read_interval, :time, default: 2
34
+ config_param :channels, :array, default: ['application']
35
+ config_param :keys, :array, default: []
36
+ config_param :read_from_head, :bool, default: false
37
+ config_param :parse_description, :bool, default: false
38
+
39
+ config_section :storage do
40
+ config_set_default :usage, "bookmarks"
41
+ config_set_default :@type, DEFAULT_STORAGE_TYPE
42
+ config_set_default :persistent, true
43
+ end
44
+
45
+ config_section :parse do
46
+ config_set_default :@type, 'winevt_xml'
47
+ config_set_default :estimate_current_event, false
48
+ end
49
+
50
+ def initalize
51
+ super
52
+ @chs = []
53
+ @keynames = []
54
+ end
55
+
56
+ def configure(conf)
57
+ super
58
+ @chs = @channels.map {|ch| ch.strip.downcase }.uniq
59
+ @keynames = @keys.map {|k| k.strip }.uniq
60
+ if @keynames.empty?
61
+ @keynames = KEY_MAP.keys
62
+ end
63
+ @keynames.delete('EventData') if @parse_description
64
+
65
+ @tag = tag
66
+ @tailing = @read_from_head ? false : true
67
+ @bookmarks_storage = storage_create(usage: "bookmarks")
68
+ @parser = parser_create
69
+ end
70
+
71
+ def start
72
+ super
73
+
74
+ @chs.each do |ch|
75
+ bookmarkXml = @bookmarks_storage.get(ch) || ""
76
+ subscribe = Winevt::EventLog::Subscribe.new
77
+ bookmark = Winevt::EventLog::Bookmark.new(bookmarkXml)
78
+ subscribe.tail = @tailing
79
+ subscribe.subscribe(ch, "*", bookmark)
80
+ timer_execute("in_windows_eventlog_#{escape_channel(ch)}".to_sym, @read_interval) do
81
+ on_notify(ch, subscribe)
82
+ end
83
+ end
84
+ end
85
+
86
+ def escape_channel(ch)
87
+ ch.gsub(/[^a-zA-Z0-9]/, '_')
88
+ end
89
+
90
+ def on_notify(ch, subscribe)
91
+ es = Fluent::MultiEventStream.new
92
+ subscribe.each do |xml, message, string_inserts|
93
+ @parser.parse(xml) do |time, record|
94
+ # record.has_key?("EventData") for none parser checking.
95
+ if record.has_key?("EventData")
96
+ record["Description"] = message
97
+ record["EventData"] = string_inserts
98
+
99
+ h = {}
100
+ @keynames.each do |k|
101
+ type = KEY_MAP[k][1]
102
+ value = record[KEY_MAP[k][0]]
103
+ h[k]=case type
104
+ when :string
105
+ value.to_s
106
+ when :array
107
+ value.map {|v| v.to_s}
108
+ else
109
+ raise "Unknown value type: #{type}"
110
+ end
111
+ end
112
+ parse_desc(h) if @parse_description
113
+ es.add(Fluent::Engine.now, h)
114
+ else
115
+ # for none parser
116
+ es.add(Fluent::Engine.now, record)
117
+ end
118
+ end
119
+ end
120
+ router.emit_stream(@tag, es)
121
+ @bookmarks_storage.put(ch, subscribe.bookmark)
122
+ end
123
+
124
+ #### These lines copied from in_windows_eventlog plugin:
125
+ #### https://github.com/fluent/fluent-plugin-windows-eventlog/blob/528290d896a885c7721f850943daa3a43a015f3d/lib/fluent/plugin/in_windows_eventlog.rb#L192-L232
126
+ GROUP_DELIMITER = "\r\n\r\n".freeze
127
+ RECORD_DELIMITER = "\r\n\t".freeze
128
+ FIELD_DELIMITER = "\t\t".freeze
129
+ NONE_FIELD_DELIMITER = "\t".freeze
130
+
131
+ def parse_desc(record)
132
+ desc = record.delete("Description".freeze)
133
+ return if desc.nil?
134
+
135
+ elems = desc.split(GROUP_DELIMITER)
136
+ record['DescriptionTitle'] = elems.shift
137
+ elems.each { |elem|
138
+ parent_key = nil
139
+ elem.split(RECORD_DELIMITER).each { |r|
140
+ key, value = if r.index(FIELD_DELIMITER)
141
+ r.split(FIELD_DELIMITER)
142
+ else
143
+ r.split(NONE_FIELD_DELIMITER)
144
+ end
145
+ key.chop! # remove ':' from key
146
+ if value.nil?
147
+ parent_key = to_key(key)
148
+ else
149
+ # parsed value sometimes contain unexpected "\t". So remove it.
150
+ value.strip!
151
+ if parent_key.nil?
152
+ record[to_key(key)] = value
153
+ else
154
+ k = "#{parent_key}.#{to_key(key)}"
155
+ record[k] = value
156
+ end
157
+ end
158
+ }
159
+ }
160
+ end
161
+
162
+ def to_key(key)
163
+ key.downcase!
164
+ key.gsub!(' '.freeze, '_'.freeze)
165
+ key
166
+ end
167
+ ####
168
+ end
169
+ end
@@ -0,0 +1,34 @@
1
+ require 'fluent/plugin/parser'
2
+ require 'nokogiri'
3
+
4
+ module Fluent::Plugin
5
+ class WinevtXMLparser < Parser
6
+ Fluent::Plugin.register_parser('winevt_xml', self)
7
+
8
+ def parse(text)
9
+ record = {}
10
+ doc = Nokogiri::XML(text)
11
+ system_elem = doc/'Event'/'System'
12
+ record["ProviderName"] = (system_elem/"Provider").attribute("Name").text rescue nil
13
+ record["ProviderGUID"] = (system_elem/"Provider").attribute("Guid").text rescue nil
14
+ record["EventID"] = (system_elem/'EventID').text rescue nil
15
+ record["Qualifiers"] = (system_elem/'EventID').attribute("Qualifiers").text rescue nil
16
+ record["Level"] = (system_elem/'Level').text rescue nil
17
+ record["Task"] = (system_elem/'Task').text rescue nil
18
+ record["Opcode"] = (system_elem/'Opcode').text rescue nil
19
+ record["Keywords"] = (system_elem/'Keywords').text rescue nil
20
+ record["TimeCreated"] = (system_elem/'TimeCreated').attribute("SystemTime").text rescue nil
21
+ record["EventRecordID"] = (system_elem/'EventRecordID').text rescue nil
22
+ record["ActivityID"] = (system_elem/'ActivityID').text rescue nil
23
+ record["RelatedActivityID"] = (system_elem/'Correlation').attribute("ActivityID").text rescue nil
24
+ record["ThreadID"] = (system_elem/'Execution').attribute("ThreadID").text rescue nil
25
+ record["Channel"] = (system_elem/'Channel').text rescue nil
26
+ record["Computer"] = (system_elem/"Computer").text rescue nil
27
+ record["UserID"] = (system_elem/'Security').attribute("UserID").text rescue nil
28
+ record["Version"] = (system_elem/'Version').text rescue nil
29
+ record["EventData"] = [] # These parameters are processed in winevt_c.
30
+ time = @estimate_current_event ? Fluent::EventTime.now : nil
31
+ yield time, record
32
+ end
33
+ end
34
+ end
@@ -0,0 +1 @@
1
+ <Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'><System><Provider Name='Microsoft-Windows-Security-Auditing' Guid='{54849625-5478-4994-A5BA-3E3B0328C30D}'/><EventID>4624</EventID><Version>2</Version><Level>0</Level><Task>12544</Task><Opcode>0</Opcode><Keywords>0x8020000000000000</Keywords><TimeCreated SystemTime='2019-06-13T09:21:23.345889600Z'/><EventRecordID>80688</EventRecordID><Correlation ActivityID='{587F0743-1F71-0006-5007-7F58711FD501}'/><Execution ProcessID='912' ThreadID='24708'/><Channel>Security</Channel><Computer>Fluentd-Developing-Windows</Computer><Security/></System><EventData><Data Name='SubjectUserSid'>S-1-5-18</Data><Data Name='SubjectUserName'>Fluentd-Developing-Windows$</Data><Data Name='SubjectDomainName'>WORKGROUP</Data><Data Name='SubjectLogonId'>0x3e7</Data><Data Name='TargetUserSid'>S-1-5-18</Data><Data Name='TargetUserName'>SYSTEM</Data><Data Name='TargetDomainName'>NT AUTHORITY</Data><Data Name='TargetLogonId'>0x3e7</Data><Data Name='LogonType'>5</Data><Data Name='LogonProcessName'>Advapi </Data><Data Name='AuthenticationPackageName'>Negotiate</Data><Data Name='WorkstationName'>-</Data><Data Name='LogonGuid'>{00000000-0000-0000-0000-000000000000}</Data><Data Name='TransmittedServices'>-</Data><Data Name='LmPackageName'>-</Data><Data Name='KeyLength'>0</Data><Data Name='ProcessId'>0x344</Data><Data Name='ProcessName'>C:\Windows\System32\services.exe</Data><Data Name='IpAddress'>-</Data><Data Name='IpPort'>-</Data><Data Name='ImpersonationLevel'>%%1833</Data><Data Name='RestrictedAdminMode'>-</Data><Data Name='TargetOutboundUserName'>-</Data><Data Name='TargetOutboundDomainName'>-</Data><Data Name='VirtualAccount'>%%1843</Data><Data Name='TargetLinkedLogonId'>0x0</Data><Data Name='ElevatedToken'>%%1842</Data></EventData></Event>
@@ -1,47 +1,47 @@
1
- require 'win32/eventlog'
2
-
3
- class EventLog
4
- def initialize
5
- @logger = Win32::EventLog.new
6
- @app_source = "fluent-plugins"
7
- end
8
-
9
- def info(event_id, message)
10
- @logger.report_event(
11
- source: @app_source,
12
- event_type: Win32::EventLog::INFO_TYPE,
13
- event_id: event_id,
14
- data: message
15
- )
16
- end
17
-
18
- def warn(event_id, message)
19
- @logger.report_event(
20
- source: @app_source,
21
- event_type: Win32::EventLog::WARN_TYPE,
22
- event_id: event_id,
23
- data: message
24
- )
25
- end
26
-
27
- def crit(event_id, message)
28
- @logger.report_event(
29
- source: @app_source,
30
- event_type: Win32::EventLog::ERROR_TYPE,
31
- event_id: event_id,
32
- data: message
33
- )
34
- end
35
-
36
- end
37
-
38
- module Fluent
39
- module Plugin
40
- class EventService
41
- def run
42
- eventlog = EventLog.new()
43
- eventlog.info(65500, "Hi, from fluentd-plugins!! at " + Time.now.strftime("%Y/%m/%d %H:%M:%S "))
44
- end
45
- end
46
- end
47
- end
1
+ require 'win32/eventlog'
2
+
3
+ class EventLog
4
+ def initialize
5
+ @logger = Win32::EventLog.new
6
+ @app_source = "fluent-plugins"
7
+ end
8
+
9
+ def info(event_id, message)
10
+ @logger.report_event(
11
+ source: @app_source,
12
+ event_type: Win32::EventLog::INFO_TYPE,
13
+ event_id: event_id,
14
+ data: message
15
+ )
16
+ end
17
+
18
+ def warn(event_id, message)
19
+ @logger.report_event(
20
+ source: @app_source,
21
+ event_type: Win32::EventLog::WARN_TYPE,
22
+ event_id: event_id,
23
+ data: message
24
+ )
25
+ end
26
+
27
+ def crit(event_id, message)
28
+ @logger.report_event(
29
+ source: @app_source,
30
+ event_type: Win32::EventLog::ERROR_TYPE,
31
+ event_id: event_id,
32
+ data: message
33
+ )
34
+ end
35
+
36
+ end
37
+
38
+ module Fluent
39
+ module Plugin
40
+ class EventService
41
+ def run
42
+ eventlog = EventLog.new()
43
+ eventlog.info(65500, "Hi, from fluentd-plugins!! at " + Time.now.strftime("%Y/%m/%d %H:%M:%S "))
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,32 +1,35 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- begin
4
- Bundler.setup(:default, :development)
5
- rescue Bundler::BundlerError => e
6
- $stderr.puts e.message
7
- $stderr.puts "Run `bundle install` to install missing gems"
8
- exit e.status_code
9
- end
10
- require 'test/unit'
11
-
12
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
- $LOAD_PATH.unshift(File.dirname(__FILE__))
14
- require 'fluent/test'
15
- unless ENV.has_key?('VERBOSE')
16
- nulllogger = Object.new
17
- nulllogger.instance_eval {|obj|
18
- def method_missing(method, *args)
19
- # pass
20
- end
21
- }
22
- $log = nulllogger
23
- end
24
-
25
- require 'fluent/test/driver/input'
26
- require 'fluent/plugin/in_windows_eventlog'
27
-
28
- class Test::Unit::TestCase
29
- end
30
- require 'fluent/test/helpers'
31
-
32
- include Fluent::Test::Helpers
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'fluent/test'
15
+ unless ENV.has_key?('VERBOSE')
16
+ nulllogger = Object.new
17
+ nulllogger.instance_eval {|obj|
18
+ def method_missing(method, *args)
19
+ # pass
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/test/driver/input'
26
+ require 'fluent/test/driver/parser'
27
+ require 'fluent/plugin/in_windows_eventlog'
28
+ require 'fluent/plugin/in_windows_eventlog2'
29
+ require 'fluent/plugin/parser_winevt_xml'
30
+
31
+ class Test::Unit::TestCase
32
+ end
33
+ require 'fluent/test/helpers'
34
+
35
+ include Fluent::Test::Helpers
@@ -0,0 +1,182 @@
1
+ require 'helper'
2
+ require 'fileutils'
3
+ require 'generate-windows-event'
4
+
5
+ class WindowsEventLog2InputTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ Fluent::Test.setup
9
+ end
10
+
11
+ CONFIG = config_element("ROOT", "", {"tag" => "fluent.eventlog"}, [
12
+ config_element("storage", "", {
13
+ '@type' => 'local',
14
+ 'persistent' => false
15
+ })
16
+ ])
17
+
18
+ def create_driver(conf = CONFIG)
19
+ Fluent::Test::Driver::Input.new(Fluent::Plugin::WindowsEventLog2Input).configure(conf)
20
+ end
21
+
22
+ def test_configure
23
+ d = create_driver CONFIG
24
+ assert_equal 'fluent.eventlog', d.instance.tag
25
+ assert_equal 2, d.instance.read_interval
26
+ assert_equal ['application'], d.instance.channels
27
+ assert_false d.instance.read_from_head
28
+ end
29
+
30
+ def test_parse_desc
31
+ d = create_driver
32
+ desc =<<-DESC
33
+ A user's local group membership was enumerated.\r\n\r\nSubject:\r\n\tSecurity ID:\t\tS-X-Y-XX-WWWWWW-VVVV\r\n\tAccount Name:\t\tAdministrator\r\n\tAccount Domain:\t\tDESKTOP-FLUENTTEST\r\n\tLogon ID:\t\t0x3185B1\r\n\r\nUser:\r\n\tSecurity ID:\t\tS-X-Y-XX-WWWWWW-VVVV\r\n\tAccount Name:\t\tAdministrator\r\n\tAccount Domain:\t\tDESKTOP-FLUENTTEST\r\n\r\nProcess Information:\r\n\tProcess ID:\t\t0x50b8\r\n\tProcess Name:\t\tC:\\msys64\\usr\\bin\\make.exe
34
+ DESC
35
+ h = {"Description" => desc}
36
+ expected = {"DescriptionTitle" => "A user's local group membership was enumerated.",
37
+ "subject.security_id" => "S-X-Y-XX-WWWWWW-VVVV",
38
+ "subject.account_name" => "Administrator",
39
+ "subject.account_domain" => "DESKTOP-FLUENTTEST",
40
+ "subject.logon_id" => "0x3185B1",
41
+ "user.security_id" => "S-X-Y-XX-WWWWWW-VVVV",
42
+ "user.account_name" => "Administrator",
43
+ "user.account_domain" => "DESKTOP-FLUENTTEST",
44
+ "process_information.process_id" => "0x50b8",
45
+ "process_information.process_name" => "C:\\msys64\\usr\\bin\\make.exe"}
46
+ d.instance.parse_desc(h)
47
+ assert_equal(expected, h)
48
+ end
49
+
50
+ def test_write
51
+ d = create_driver
52
+
53
+ service = Fluent::Plugin::EventService.new
54
+
55
+ d.run(expect_emits: 1) do
56
+ service.run
57
+ end
58
+
59
+ assert(d.events.length >= 1)
60
+ event = d.events.last
61
+ record = event.last
62
+
63
+ assert_equal("Application", record["Channel"])
64
+ assert_equal("65500", record["EventID"])
65
+ assert_equal("4", record["Level"])
66
+ assert_equal("fluent-plugins", record["ProviderName"])
67
+ end
68
+
69
+ CONFIG_KEYS = config_element("ROOT", "", {
70
+ "tag" => "fluent.eventlog",
71
+ "keys" => ["EventID", "Level", "Channel", "ProviderName"]
72
+ }, [
73
+ config_element("storage", "", {
74
+ '@type' => 'local',
75
+ 'persistent' => false
76
+ })
77
+ ])
78
+ def test_write_with_keys
79
+ d = create_driver(CONFIG_KEYS)
80
+
81
+ service = Fluent::Plugin::EventService.new
82
+
83
+ d.run(expect_emits: 1) do
84
+ service.run
85
+ end
86
+
87
+ assert(d.events.length >= 1)
88
+ event = d.events.last
89
+ record = event.last
90
+
91
+ expected = {"EventID" => "65500",
92
+ "Level" => "4",
93
+ "Channel" => "Application",
94
+ "ProviderName" => "fluent-plugins"}
95
+
96
+ assert_equal(expected, record)
97
+ end
98
+
99
+ class PersistBookMark < self
100
+ TEST_PLUGIN_STORAGE_PATH = File.join( File.dirname(File.dirname(__FILE__)), 'tmp', 'in_windows_eventlog2', 'store' )
101
+ CONFIG2 = config_element("ROOT", "", {"tag" => "fluent.eventlog"}, [
102
+ config_element("storage", "", {
103
+ '@type' => 'local',
104
+ '@id' => 'test-02',
105
+ 'path' => File.join(TEST_PLUGIN_STORAGE_PATH,
106
+ 'json', 'test-02.json'),
107
+ 'persistent' => true,
108
+ })
109
+ ])
110
+
111
+ def setup
112
+ FileUtils.rm_rf(TEST_PLUGIN_STORAGE_PATH)
113
+ FileUtils.mkdir_p(File.join(TEST_PLUGIN_STORAGE_PATH, 'json'))
114
+ FileUtils.chmod_R(0755, File.join(TEST_PLUGIN_STORAGE_PATH, 'json'))
115
+ end
116
+
117
+ def test_write
118
+ d = create_driver(CONFIG2)
119
+
120
+ assert !File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
121
+
122
+ service = Fluent::Plugin::EventService.new
123
+
124
+ d.run(expect_emits: 1) do
125
+ service.run
126
+ end
127
+
128
+ assert(d.events.length >= 1)
129
+ event = d.events.last
130
+ record = event.last
131
+
132
+ prev_id = record["EventRecordID"].to_i
133
+ assert_equal("Application", record["Channel"])
134
+ assert_equal("65500", record["EventID"])
135
+ assert_equal("4", record["Level"])
136
+ assert_equal("fluent-plugins", record["ProviderName"])
137
+
138
+ assert File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
139
+
140
+ d2 = create_driver(CONFIG2)
141
+ d2.run(expect_emits: 1) do
142
+ service.run
143
+ end
144
+
145
+ assert(d2.events.length == 1) # should be tailing after previous context.
146
+ event2 = d2.events.last
147
+ record2 = event2.last
148
+
149
+ curr_id = record2["EventRecordID"].to_i
150
+ assert(curr_id > prev_id)
151
+
152
+ assert File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
153
+ end
154
+ end
155
+
156
+ def test_write_with_none_parser
157
+ d = create_driver(config_element("ROOT", "", {"tag" => "fluent.eventlog"}, [
158
+ config_element("storage", "", {
159
+ '@type' => 'local',
160
+ 'persistent' => false
161
+ }),
162
+ config_element("parse", "", {
163
+ '@type' => 'none',
164
+ }),
165
+ ]))
166
+
167
+ service = Fluent::Plugin::EventService.new
168
+
169
+ d.run(expect_emits: 1) do
170
+ service.run
171
+ end
172
+
173
+ assert(d.events.length >= 1)
174
+ event = d.events.last
175
+ record = event.last
176
+
177
+ assert do
178
+ # record should be {message: <RAW XML EventLog>}.
179
+ record["message"]
180
+ end
181
+ end
182
+ end