fluent-plugin-windows-eventlog 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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