fluent-plugin-windows-eventlog 0.1.0 → 0.4.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,216 @@
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
+ config_param :render_as_xml, :bool, default: true
39
+ config_param :rate_limit, :integer, default: Winevt::EventLog::Subscribe::RATE_INFINITE
40
+
41
+ config_section :storage do
42
+ config_set_default :usage, "bookmarks"
43
+ config_set_default :@type, DEFAULT_STORAGE_TYPE
44
+ config_set_default :persistent, true
45
+ end
46
+
47
+ config_section :parse do
48
+ config_set_default :@type, 'winevt_xml'
49
+ config_set_default :estimate_current_event, false
50
+ end
51
+
52
+ def initalize
53
+ super
54
+ @chs = []
55
+ @keynames = []
56
+ end
57
+
58
+ def configure(conf)
59
+ super
60
+ @chs = @channels.map {|ch| ch.strip.downcase }.uniq
61
+ @keynames = @keys.map {|k| k.strip }.uniq
62
+ if @keynames.empty?
63
+ @keynames = KEY_MAP.keys
64
+ end
65
+ @keynames.delete('Qualifiers') unless @render_as_xml
66
+ @keynames.delete('EventData') if @parse_description
67
+
68
+ @tag = tag
69
+ @tailing = @read_from_head ? false : true
70
+ @bookmarks_storage = storage_create(usage: "bookmarks")
71
+ @winevt_xml = false
72
+ if @render_as_xml
73
+ @parser = parser_create
74
+ @winevt_xml = @parser.respond_to?(:winevt_xml?) && @parser.winevt_xml?
75
+ class << self
76
+ alias_method :on_notify, :on_notify_xml
77
+ end
78
+ else
79
+ class << self
80
+ alias_method :on_notify, :on_notify_hash
81
+ end
82
+ end
83
+ end
84
+
85
+ def start
86
+ super
87
+
88
+ @chs.each do |ch|
89
+ bookmarkXml = @bookmarks_storage.get(ch) || ""
90
+ subscribe = Winevt::EventLog::Subscribe.new
91
+ bookmark = Winevt::EventLog::Bookmark.new(bookmarkXml)
92
+ subscribe.tail = @tailing
93
+ subscribe.subscribe(ch, "*", bookmark)
94
+ subscribe.render_as_xml = @render_as_xml
95
+ subscribe.rate_limit = @rate_limit
96
+ timer_execute("in_windows_eventlog_#{escape_channel(ch)}".to_sym, @read_interval) do
97
+ on_notify(ch, subscribe)
98
+ end
99
+ end
100
+ end
101
+
102
+ def escape_channel(ch)
103
+ ch.gsub(/[^a-zA-Z0-9]/, '_')
104
+ end
105
+
106
+ def on_notify(ch, subscribe)
107
+ # for safety.
108
+ end
109
+
110
+ def on_notify_xml(ch, subscribe)
111
+ es = Fluent::MultiEventStream.new
112
+ subscribe.each do |xml, message, string_inserts|
113
+ @parser.parse(xml) do |time, record|
114
+ # record.has_key?("EventData") for none parser checking.
115
+ if @winevt_xml
116
+ record["Description"] = message
117
+ record["EventData"] = string_inserts
118
+
119
+ h = {}
120
+ @keynames.each do |k|
121
+ type = KEY_MAP[k][1]
122
+ value = record[KEY_MAP[k][0]]
123
+ h[k]=case type
124
+ when :string
125
+ value.to_s
126
+ when :array
127
+ value.map {|v| v.to_s}
128
+ else
129
+ raise "Unknown value type: #{type}"
130
+ end
131
+ end
132
+ parse_desc(h) if @parse_description
133
+ es.add(Fluent::Engine.now, h)
134
+ else
135
+ record["Description"] = message
136
+ record["EventData"] = string_inserts
137
+ # for none parser
138
+ es.add(Fluent::Engine.now, record)
139
+ end
140
+ end
141
+ end
142
+ router.emit_stream(@tag, es)
143
+ @bookmarks_storage.put(ch, subscribe.bookmark)
144
+ end
145
+
146
+ def on_notify_hash(ch, subscribe)
147
+ es = Fluent::MultiEventStream.new
148
+ subscribe.each do |record, message, string_inserts|
149
+ record["Description"] = message
150
+ record["EventData"] = string_inserts
151
+ h = {}
152
+ @keynames.each do |k|
153
+ type = KEY_MAP[k][1]
154
+ value = record[KEY_MAP[k][0]]
155
+ h[k]=case type
156
+ when :string
157
+ value.to_s
158
+ when :array
159
+ value.map {|v| v.to_s}
160
+ else
161
+ raise "Unknown value type: #{type}"
162
+ end
163
+ end
164
+ parse_desc(h) if @parse_description
165
+ es.add(Fluent::Engine.now, h)
166
+ end
167
+ router.emit_stream(@tag, es)
168
+ @bookmarks_storage.put(ch, subscribe.bookmark)
169
+ end
170
+
171
+ #### These lines copied from in_windows_eventlog plugin:
172
+ #### https://github.com/fluent/fluent-plugin-windows-eventlog/blob/528290d896a885c7721f850943daa3a43a015f3d/lib/fluent/plugin/in_windows_eventlog.rb#L192-L232
173
+ GROUP_DELIMITER = "\r\n\r\n".freeze
174
+ RECORD_DELIMITER = "\r\n\t".freeze
175
+ FIELD_DELIMITER = "\t\t".freeze
176
+ NONE_FIELD_DELIMITER = "\t".freeze
177
+
178
+ def parse_desc(record)
179
+ desc = record.delete("Description".freeze)
180
+ return if desc.nil?
181
+
182
+ elems = desc.split(GROUP_DELIMITER)
183
+ record['DescriptionTitle'] = elems.shift
184
+ elems.each { |elem|
185
+ parent_key = nil
186
+ elem.split(RECORD_DELIMITER).each { |r|
187
+ key, value = if r.index(FIELD_DELIMITER)
188
+ r.split(FIELD_DELIMITER)
189
+ else
190
+ r.split(NONE_FIELD_DELIMITER)
191
+ end
192
+ key.chop! # remove ':' from key
193
+ if value.nil?
194
+ parent_key = to_key(key)
195
+ else
196
+ # parsed value sometimes contain unexpected "\t". So remove it.
197
+ value.strip!
198
+ if parent_key.nil?
199
+ record[to_key(key)] = value
200
+ else
201
+ k = "#{parent_key}.#{to_key(key)}"
202
+ record[k] = value
203
+ end
204
+ end
205
+ }
206
+ }
207
+ end
208
+
209
+ def to_key(key)
210
+ key.downcase!
211
+ key.gsub!(' '.freeze, '_'.freeze)
212
+ key
213
+ end
214
+ ####
215
+ end
216
+ end
@@ -0,0 +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
data/test/helper.rb CHANGED
@@ -1,29 +1,33 @@
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
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
+ require 'fluent/plugin/in_windows_eventlog2'
28
+
29
+ class Test::Unit::TestCase
30
+ end
31
+ require 'fluent/test/helpers'
32
+
33
+ include Fluent::Test::Helpers
@@ -0,0 +1,214 @@
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
+ assert_true d.instance.render_as_xml
29
+ end
30
+
31
+ def test_parse_desc
32
+ d = create_driver
33
+ desc =<<-DESC
34
+ 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
35
+ DESC
36
+ h = {"Description" => desc}
37
+ expected = {"DescriptionTitle" => "A user's local group membership was enumerated.",
38
+ "subject.security_id" => "S-X-Y-XX-WWWWWW-VVVV",
39
+ "subject.account_name" => "Administrator",
40
+ "subject.account_domain" => "DESKTOP-FLUENTTEST",
41
+ "subject.logon_id" => "0x3185B1",
42
+ "user.security_id" => "S-X-Y-XX-WWWWWW-VVVV",
43
+ "user.account_name" => "Administrator",
44
+ "user.account_domain" => "DESKTOP-FLUENTTEST",
45
+ "process_information.process_id" => "0x50b8",
46
+ "process_information.process_name" => "C:\\msys64\\usr\\bin\\make.exe"}
47
+ d.instance.parse_desc(h)
48
+ assert_equal(expected, h)
49
+ end
50
+
51
+ def test_write
52
+ d = create_driver
53
+
54
+ service = Fluent::Plugin::EventService.new
55
+
56
+ d.run(expect_emits: 1) do
57
+ service.run
58
+ end
59
+
60
+ assert(d.events.length >= 1)
61
+ event = d.events.last
62
+ record = event.last
63
+
64
+ assert_equal("Application", record["Channel"])
65
+ assert_equal("65500", record["EventID"])
66
+ assert_equal("4", record["Level"])
67
+ assert_equal("fluent-plugins", record["ProviderName"])
68
+ end
69
+
70
+ CONFIG_KEYS = config_element("ROOT", "", {
71
+ "tag" => "fluent.eventlog",
72
+ "keys" => ["EventID", "Level", "Channel", "ProviderName"]
73
+ }, [
74
+ config_element("storage", "", {
75
+ '@type' => 'local',
76
+ 'persistent' => false
77
+ })
78
+ ])
79
+ def test_write_with_keys
80
+ d = create_driver(CONFIG_KEYS)
81
+
82
+ service = Fluent::Plugin::EventService.new
83
+
84
+ d.run(expect_emits: 1) do
85
+ service.run
86
+ end
87
+
88
+ assert(d.events.length >= 1)
89
+ event = d.events.last
90
+ record = event.last
91
+
92
+ expected = {"EventID" => "65500",
93
+ "Level" => "4",
94
+ "Channel" => "Application",
95
+ "ProviderName" => "fluent-plugins"}
96
+
97
+ assert_equal(expected, record)
98
+ end
99
+
100
+ class HashRendered < self
101
+ def test_write
102
+ d = create_driver(config_element("ROOT", "", {"tag" => "fluent.eventlog",
103
+ "render_as_xml" => false}, [
104
+ config_element("storage", "", {
105
+ '@type' => 'local',
106
+ 'persistent' => false
107
+ })
108
+ ]))
109
+
110
+ service = Fluent::Plugin::EventService.new
111
+
112
+ d.run(expect_emits: 1) do
113
+ service.run
114
+ end
115
+
116
+ assert(d.events.length >= 1)
117
+ event = d.events.last
118
+ record = event.last
119
+
120
+ assert_false(d.instance.render_as_xml)
121
+ assert_equal("Application", record["Channel"])
122
+ assert_equal("65500", record["EventID"])
123
+ assert_equal("4", record["Level"])
124
+ assert_equal("fluent-plugins", record["ProviderName"])
125
+ end
126
+ end
127
+
128
+ class PersistBookMark < self
129
+ TEST_PLUGIN_STORAGE_PATH = File.join( File.dirname(File.dirname(__FILE__)), 'tmp', 'in_windows_eventlog2', 'store' )
130
+ CONFIG2 = config_element("ROOT", "", {"tag" => "fluent.eventlog"}, [
131
+ config_element("storage", "", {
132
+ '@type' => 'local',
133
+ '@id' => 'test-02',
134
+ 'path' => File.join(TEST_PLUGIN_STORAGE_PATH,
135
+ 'json', 'test-02.json'),
136
+ 'persistent' => true,
137
+ })
138
+ ])
139
+
140
+ def setup
141
+ FileUtils.rm_rf(TEST_PLUGIN_STORAGE_PATH)
142
+ FileUtils.mkdir_p(File.join(TEST_PLUGIN_STORAGE_PATH, 'json'))
143
+ FileUtils.chmod_R(0755, File.join(TEST_PLUGIN_STORAGE_PATH, 'json'))
144
+ end
145
+
146
+ def test_write
147
+ d = create_driver(CONFIG2)
148
+
149
+ assert !File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
150
+
151
+ service = Fluent::Plugin::EventService.new
152
+
153
+ d.run(expect_emits: 1) do
154
+ service.run
155
+ end
156
+
157
+ assert(d.events.length >= 1)
158
+ event = d.events.last
159
+ record = event.last
160
+
161
+ prev_id = record["EventRecordID"].to_i
162
+ assert_equal("Application", record["Channel"])
163
+ assert_equal("65500", record["EventID"])
164
+ assert_equal("4", record["Level"])
165
+ assert_equal("fluent-plugins", record["ProviderName"])
166
+
167
+ assert File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
168
+
169
+ d2 = create_driver(CONFIG2)
170
+ d2.run(expect_emits: 1) do
171
+ service.run
172
+ end
173
+
174
+ assert(d2.events.length == 1) # should be tailing after previous context.
175
+ event2 = d2.events.last
176
+ record2 = event2.last
177
+
178
+ curr_id = record2["EventRecordID"].to_i
179
+ assert(curr_id > prev_id)
180
+
181
+ assert File.exist?(File.join(TEST_PLUGIN_STORAGE_PATH, 'json', 'test-02.json'))
182
+ end
183
+ end
184
+
185
+ def test_write_with_none_parser
186
+ d = create_driver(config_element("ROOT", "", {"tag" => "fluent.eventlog"}, [
187
+ config_element("storage", "", {
188
+ '@type' => 'local',
189
+ 'persistent' => false
190
+ }),
191
+ config_element("parse", "", {
192
+ '@type' => 'none',
193
+ }),
194
+ ]))
195
+
196
+ service = Fluent::Plugin::EventService.new
197
+
198
+ d.run(expect_emits: 1) do
199
+ service.run
200
+ end
201
+
202
+ assert(d.events.length >= 1)
203
+ event = d.events.last
204
+ record = event.last
205
+
206
+ assert do
207
+ # record should be {message: <RAW XML EventLog>}.
208
+ record["message"]
209
+ end
210
+
211
+ assert_true(record.has_key?("Description"))
212
+ assert_true(record.has_key?("EventData"))
213
+ end
214
+ end