fluent-plugin-windows-eventlog 0.2.2 → 0.3.0

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