From 396fc7d1641b37329eab9ccaa83608613f6c2f1e Mon Sep 17 00:00:00 2001 From: Marc Guasch Date: Thu, 9 Jan 2025 03:04:19 -0800 Subject: [PATCH] Default to raw api --- CHANGELOG.next.asciidoc | 1 + filebeat/docs/inputs/input-winlog.asciidoc | 60 +- winlogbeat/docs/winlogbeat-options.asciidoc | 21 +- winlogbeat/eventlog/bench_test.go | 22 +- winlogbeat/eventlog/cache.go | 164 ----- winlogbeat/eventlog/config.go | 136 ++++ winlogbeat/eventlog/eventlog.go | 2 - winlogbeat/eventlog/factory.go | 130 ---- winlogbeat/eventlog/factory_other.go | 31 + winlogbeat/eventlog/factory_windows.go | 33 + winlogbeat/eventlog/metrics.go | 151 +++++ winlogbeat/eventlog/wineventlog.go | 711 +++++--------------- winlogbeat/eventlog/wineventlog_raw.go | 390 ----------- winlogbeat/eventlog/wineventlog_test.go | 113 ++-- 14 files changed, 576 insertions(+), 1389 deletions(-) delete mode 100644 winlogbeat/eventlog/cache.go create mode 100644 winlogbeat/eventlog/config.go delete mode 100644 winlogbeat/eventlog/factory.go create mode 100644 winlogbeat/eventlog/factory_other.go create mode 100644 winlogbeat/eventlog/factory_windows.go create mode 100644 winlogbeat/eventlog/metrics.go delete mode 100644 winlogbeat/eventlog/wineventlog_raw.go diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 33feb91cff20..646fa05673d8 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -90,6 +90,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] *Winlogbeat* - Add "event.category" and "event.type" to Sysmon module for EventIDs 8, 9, 19, 20, 27, 28, 255 {pull}35193[35193] +- Default to use raw api and delete older xml implementation. {pull}42275[42275] *Functionbeat* diff --git a/filebeat/docs/inputs/input-winlog.asciidoc b/filebeat/docs/inputs/input-winlog.asciidoc index 6d58c7d8df8d..fb624dc3af58 100644 --- a/filebeat/docs/inputs/input-winlog.asciidoc +++ b/filebeat/docs/inputs/input-winlog.asciidoc @@ -181,40 +181,6 @@ IDs to include (e.g. 4700-4800), and single event IDs to exclude (e.g. -4735). event_id: 4624, 4625, 4700-4800, -4735 -------------------------------------------------------------------------------- -[WARNING] -======================================= -If you specify more than 22 query conditions (event IDs or event ID ranges), some -versions of Windows will prevent {beatname_uc} from reading the event log due to -limits in the query system. If this occurs a similar warning as shown below will -be logged by {beatname_uc}, and it will continue processing data from other event -logs. - -`WARN EventLog[Application] Open() error. No events will be read from this -source. The specified query is invalid.` - -In some cases, the limit may be lower than 22 conditions. For instance, using a -mixture of ranges and single event IDs, along with an additional parameter such -as `ignore older`, results in a limit of 21 conditions. - -If you have more than 22 conditions, you can workaround this Windows limitation -by using a drop_event[drop-event] processor to do the filtering after -{beatname_uc} has received the events from Windows. The filter shown below is -equivalent to `event_id: 903, 1024, 4624` but can be expanded beyond 22 -event IDs. - -[source,yaml] --------------------------------------------------------------------------------- -- type: winlog - name: Security - processors: - - drop_event.when.not.or: - - equals.winlog.event_id: 903 - - equals.winlog.event_id: 1024 - - equals.winlog.event_id: 4624 --------------------------------------------------------------------------------- - -======================================= - [float] ==== `language` @@ -350,6 +316,9 @@ Example: include_xml: true -------------------------------------------------------------------------------- +* This can have a significant impact on performance that can vary depending +on your system specs. + [float] ==== `tags` @@ -434,26 +403,3 @@ stopped. *{vista_and_newer}* Setting `no_more_events` to `stop` is useful when reading from archived event log files where you want to read the whole file then exit. - -[float] -==== `api` - -This selects the event log reader implementation that is used to read events -from the Windows APIs. You should only set this option when testing experimental -features. When the value is set to `wineventlog-experimental` {beatname_uc} will -replace the default event log reader with the **experimental** implementation. -We are evaluating this implementation to see if it can provide increased -performance and reduce CPU usage. *{vista_and_newer}* - -[source,yaml] --------------------------------------------------------------------------------- -- type: winlog - name: ForwardedEvents - api: wineventlog-experimental --------------------------------------------------------------------------------- - -There are a few notable differences in the events: - -* Events that contained data under `winlog.user_data` will now have it under - `winlog.event_data`. -* Setting `include_xml: true` has no effect. diff --git a/winlogbeat/docs/winlogbeat-options.asciidoc b/winlogbeat/docs/winlogbeat-options.asciidoc index 80804e25b2c5..57239aa72d33 100644 --- a/winlogbeat/docs/winlogbeat-options.asciidoc +++ b/winlogbeat/docs/winlogbeat-options.asciidoc @@ -376,6 +376,9 @@ winlogbeat.event_logs: include_xml: true -------------------------------------------------------------------------------- +* This can have a significant impact on performance that can vary depending +on your system specs. + [float] ==== `event_logs.tags` @@ -462,24 +465,6 @@ Setting `no_more_events` to `stop` is useful when reading from archived event log files where you want to read the whole file then exit. There's a complete example of how to read from an `.evtx` file in the <>. -[float] -==== `event_logs.api` - -This selects the event log reader implementation that is used to read events -from the Windows APIs. When the value is set to `wineventlog-raw` Winlogbeat will -replace the default XML event log reader with a more performant implementation. -*{vista_and_newer}* - -[source,yaml] --------------------------------------------------------------------------------- -winlogbeat.event_logs: - - name: ForwardedEvents - api: wineventlog-raw --------------------------------------------------------------------------------- - -* If `include_xml` is `true` the performance will be the same as the default API, -as performance improvements are lost when parsing the XML. - [float] ==== `overwrite_pipelines` diff --git a/winlogbeat/eventlog/bench_test.go b/winlogbeat/eventlog/bench_test.go index 97946a509b3f..91b40a89f445 100644 --- a/winlogbeat/eventlog/bench_test.go +++ b/winlogbeat/eventlog/bench_test.go @@ -58,21 +58,17 @@ func TestBenchmarkRead(t *testing.T) { safeWriteEvent(t, writer, uint32(rand.Int63()%1000), strconv.Itoa(i)+" "+randomSentence(256)) } - for _, api := range []string{winEventLogAPIName, winEventLogExpAPIName} { - t.Run("api="+api, func(t *testing.T) { - for _, includexml := range []bool{true, false} { - for _, batchSize := range []int{10, 100, 500, 1000} { - t.Run(fmt.Sprintf("include_xml=%v/batch_size=%d", includexml, batchSize), func(t *testing.T) { - result := testing.Benchmark(benchmarkEventLog(api, includexml, batchSize)) - outputBenchmarkResults(t, result) - }) - } - } - }) + for _, includexml := range []bool{true, false} { + for _, batchSize := range []int{10, 100, 500, 1000} { + t.Run(fmt.Sprintf("include_xml=%v/batch_size=%d", includexml, batchSize), func(t *testing.T) { + result := testing.Benchmark(benchmarkEventLog(includexml, batchSize)) + outputBenchmarkResults(t, result) + }) + } } } -func benchmarkEventLog(api string, includexml bool, batchSize int) func(b *testing.B) { +func benchmarkEventLog(includexml bool, batchSize int) func(b *testing.B) { return func(b *testing.B) { conf := mapstr.M{ "name": providerName, @@ -81,7 +77,7 @@ func benchmarkEventLog(api string, includexml bool, batchSize int) func(b *testi "include_xml": includexml, } - log := openLog(b, api, nil, conf) + log := openLog(b, nil, conf) defer log.Close() events := 0 diff --git a/winlogbeat/eventlog/cache.go b/winlogbeat/eventlog/cache.go deleted file mode 100644 index 985c28f433c0..000000000000 --- a/winlogbeat/eventlog/cache.go +++ /dev/null @@ -1,164 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//go:build windows - -package eventlog - -// This component of the eventlog package provides a cache for storing Handles -// to event message files. - -import ( - "expvar" - "time" - - "github.com/elastic/beats/v7/libbeat/common" - "github.com/elastic/beats/v7/winlogbeat/sys" - win "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog" - "github.com/elastic/elastic-agent-libs/logp" -) - -// Stats for the message file caches. -var ( - cacheStats = expvar.NewMap("msg_file_cache") -) - -// Constants that control the cache behavior. -const ( - expirationTimeout time.Duration = 2 * time.Minute - janitorInterval time.Duration = 30 * time.Second - initialSize int = 10 -) - -// Function type for loading event message files associated with the given -// event log and source name. -type messageFileLoaderFunc func(eventLogName, sourceName string) sys.MessageFiles - -// Function type for freeing Handles. -type freeHandleFunc func(handle uintptr) error - -// handleCache provides a synchronized cache that holds MessageFiles. -type messageFilesCache struct { - cache *common.Cache - loader messageFileLoaderFunc - freer freeHandleFunc - eventLogName string - - // Cache metrics. - hit func() // Increments number of cache hits. - miss func() // Increments number of cache misses. - size func() // Sets the current cache size. -} - -// newHandleCache creates and returns a new handleCache that has been -// initialized (including starting a periodic janitor goroutine to purge -// expired Handles). -func newMessageFilesCache(eventLogName string, loader messageFileLoaderFunc, - freer freeHandleFunc, -) *messageFilesCache { - size := &expvar.Int{} - cacheStats.Set(eventLogName+"Size", size) - - hc := &messageFilesCache{ - loader: loader, - freer: freer, - eventLogName: eventLogName, - hit: func() { cacheStats.Add(eventLogName+"Hits", 1) }, - miss: func() { cacheStats.Add(eventLogName+"Misses", 1) }, - } - hc.cache = common.NewCacheWithRemovalListener(expirationTimeout, - initialSize, hc.evictionHandler) - hc.cache.StartJanitor(janitorInterval) - hc.size = func() { - s := hc.cache.Size() - size.Set(int64(s)) - debugf("messageFilesCache[%s] size=%d", hc.eventLogName, s) - } - return hc -} - -// get returns a cached MessageFiles for the given sourceName. -// If no item is cached, then one is loaded, stored, and returned. -// Callers should check the MessageFiles.Err value to see if an error occurred -// while loading the message files. -func (hc *messageFilesCache) get(sourceName string) win.EvtHandle { - v := hc.cache.Get(sourceName) - if v == nil { - hc.miss() - - // Handle to event message file for sourceName is not cached. Attempt - // to load the Handles into the cache. - v = hc.loader(hc.eventLogName, sourceName) - - // Store the newly loaded value. Since this code does not lock we must - // check if a value was already loaded. - existing := hc.cache.PutIfAbsent(sourceName, v) - if existing != nil { - // A value was already loaded, so free the handles we just created. - messageFiles, _ := v.(sys.MessageFiles) - hc.freeHandles(messageFiles) - - // Return the existing cached value. - messageFiles, _ = existing.(sys.MessageFiles) - - if messageFiles.Err == nil { - // There is only ever a single handle when using the Windows Event - // Log API. - return win.EvtHandle(messageFiles.Handles[0].Handle) - } - } - hc.size() - } else { - hc.hit() - } - - messageFiles, _ := v.(sys.MessageFiles) - if messageFiles.Err == nil { - // There is only ever a single handle when using the Windows Event - // Log API. - return win.EvtHandle(messageFiles.Handles[0].Handle) - } - return win.NilHandle -} - -// evictionHandler is the callback handler that receives notifications when -// a key-value pair is evicted from the messageFilesCache. -func (hc *messageFilesCache) evictionHandler(k common.Key, v common.Value) { - // Update the size on a different goroutine after the callback completes. - defer func() { go hc.size() }() - - messageFiles, ok := v.(sys.MessageFiles) - if !ok { - return - } - - debugf("messageFilesCache[%s] Evicting messageFiles %+v for sourceName %v.", - hc.eventLogName, messageFiles, k) - hc.freeHandles(messageFiles) -} - -// freeHandles free the event message file Handles so that the modules can -// be unloaded. The Handles are no longer valid after being freed. -func (hc *messageFilesCache) freeHandles(mf sys.MessageFiles) { - for _, fh := range mf.Handles { - err := hc.freer(fh.Handle) - if err != nil { - logp.Warn("messageFilesCache[%s] FreeLibrary error for handle %v", - hc.eventLogName, fh.Handle) - } - } -} diff --git a/winlogbeat/eventlog/config.go b/winlogbeat/eventlog/config.go new file mode 100644 index 000000000000..a4809d51378b --- /dev/null +++ b/winlogbeat/eventlog/config.go @@ -0,0 +1,136 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build windows + +package eventlog + +import ( + "encoding/xml" + "fmt" + "strings" + "time" + + "github.com/joeshaw/multierror" + + conf "github.com/elastic/elastic-agent-libs/config" +) + +type validator interface { + Validate() error +} + +func readConfig(c *conf.C, config interface{}) error { + if err := c.Unpack(config); err != nil { + return fmt.Errorf("failed unpacking config. %v", err) + } + + if v, ok := config.(validator); ok { + if err := v.Validate(); err != nil { + return err + } + } + + return nil +} + +type config struct { + Renderer string `config:"renderer"` // Name of the renderer to use. Optional. + Name string `config:"name"` // Name of the event log or channel or file. + ID string `config:"id"` // Identifier for the event log. + XMLQuery string `config:"xml_query"` // Custom query XML. Must not be used with the keys from eventlog.query. + BatchReadSize int `config:"batch_read_size"` // Maximum number of events that Read will return. + IncludeXML bool `config:"include_xml"` + Forwarded *bool `config:"forwarded"` + SimpleQuery query `config:",inline"` + NoMoreEvents NoMoreEventsAction `config:"no_more_events"` // Action to take when no more events are available - wait or stop. + EventLanguage uint32 `config:"language"` +} + +// query contains parameters used to customize the event log data that is +// queried from the log. +type query struct { + IgnoreOlder time.Duration `config:"ignore_older"` // Ignore records older than this period of time. + EventID string `config:"event_id"` // White-list and black-list of events. + Level string `config:"level"` // Severity level. + Provider []string `config:"provider"` // Provider (source name). +} + +// NoMoreEventsAction defines what action for the reader to take when +// ERROR_NO_MORE_ITEMS is returned by the Windows API. +type NoMoreEventsAction uint8 + +const ( + // Wait for new events. + Wait NoMoreEventsAction = iota + // Stop the reader. + Stop +) + +var noMoreEventsActionNames = map[NoMoreEventsAction]string{ + Wait: "wait", + Stop: "stop", +} + +// Unpack sets the action based on the string value. +func (a *NoMoreEventsAction) Unpack(v string) error { + v = strings.ToLower(v) + for action, name := range noMoreEventsActionNames { + if v == name { + *a = action + return nil + } + } + return fmt.Errorf("invalid no_more_events action: %v", v) +} + +// String returns the name of the action. +func (a NoMoreEventsAction) String() string { return noMoreEventsActionNames[a] } + +// Validate validates the winEventLogConfig data and returns an error describing +// any problems or nil. +func (c *config) Validate() error { + var errs multierror.Errors + + if c.XMLQuery != "" { + if c.ID == "" { + errs = append(errs, fmt.Errorf("event log is missing an 'id'")) + } + + // Check for XML syntax errors. This does not check the validity of the query itself. + if err := xml.Unmarshal([]byte(c.XMLQuery), &struct{}{}); err != nil { + errs = append(errs, fmt.Errorf("invalid xml_query: %w", err)) + } + + switch { + case c.Name != "": + errs = append(errs, fmt.Errorf("xml_query cannot be used with 'name'")) + case c.SimpleQuery.IgnoreOlder != 0: + errs = append(errs, fmt.Errorf("xml_query cannot be used with 'ignore_older'")) + case c.SimpleQuery.Level != "": + errs = append(errs, fmt.Errorf("xml_query cannot be used with 'level'")) + case c.SimpleQuery.EventID != "": + errs = append(errs, fmt.Errorf("xml_query cannot be used with 'event_id'")) + case len(c.SimpleQuery.Provider) != 0: + errs = append(errs, fmt.Errorf("xml_query cannot be used with 'provider'")) + } + } else if c.Name == "" { + errs = append(errs, fmt.Errorf("event log is missing a 'name'")) + } + + return errs.Err() +} diff --git a/winlogbeat/eventlog/eventlog.go b/winlogbeat/eventlog/eventlog.go index 0a06bf13ce97..555b071d6590 100644 --- a/winlogbeat/eventlog/eventlog.go +++ b/winlogbeat/eventlog/eventlog.go @@ -70,7 +70,6 @@ type EventLog interface { type Record struct { winevent.Event File string // Source file when event is from a file. - API string // The event log API type used to read the record. XML string // XML representation of the event. Offset checkpoint.EventLogState // Position of the record within its source stream. } @@ -80,7 +79,6 @@ func (e Record) ToEvent() beat.Event { win := e.Fields() _ = win.Delete("time_created") - _, _ = win.Put("api", e.API) m := mapstr.M{ "winlog": win, diff --git a/winlogbeat/eventlog/factory.go b/winlogbeat/eventlog/factory.go deleted file mode 100644 index 851615bc80c7..000000000000 --- a/winlogbeat/eventlog/factory.go +++ /dev/null @@ -1,130 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package eventlog - -import ( - "errors" - "fmt" - "sort" - "strings" - - conf "github.com/elastic/elastic-agent-libs/config" -) - -// ConfigCommon is the common configuration data used to instantiate a new -// EventLog. Each implementation is free to support additional configuration -// options. -type ConfigCommon struct { - API string `config:"api"` // Name of the API to use. Optional. - Name string `config:"name"` // Name of the event log or channel or file. - ID string `config:"id"` // Identifier for the event log. - XMLQuery string `config:"xml_query"` // Custom query XML. Must not be used with the keys from eventlog.query. -} - -type validator interface { - Validate() error -} - -func readConfig(c *conf.C, config interface{}) error { - if err := c.Unpack(config); err != nil { - return fmt.Errorf("failed unpacking config. %v", err) - } - - if v, ok := config.(validator); ok { - if err := v.Validate(); err != nil { - return err - } - } - - return nil -} - -// Producer produces a new event log instance for reading event log records. -type producer func(*conf.C) (EventLog, error) - -// Channels lists the available channels (event logs). -type channels func() ([]string, error) - -// eventLogInfo is the registration info associated with an event log API. -type eventLogInfo struct { - apiName string - priority int - producer producer - channels func() ([]string, error) -} - -// eventLogs is a map of priorities to eventLogInfo. The lower numbers have -// higher priorities. -var eventLogs = make(map[int]eventLogInfo) - -// Register registers an EventLog API. Only the APIs that are available for the -// runtime OS should be registered. Each API must have a unique priority. -func Register(apiName string, priority int, producer producer, channels channels) { - info, exists := eventLogs[priority] - if exists { - panic(fmt.Sprintf("%s API is already registered with priority %d. "+ - "Cannot register %s", info.apiName, info.priority, apiName)) - } - - eventLogs[priority] = eventLogInfo{ - apiName: apiName, - priority: priority, - producer: producer, - channels: channels, - } -} - -// New creates and returns a new EventLog instance based on the given config -// and the registered EventLog producers. -func New(options *conf.C) (EventLog, error) { - if len(eventLogs) == 0 { - return nil, errors.New("No event log API is available on this system") - } - - var config ConfigCommon - if err := readConfig(options, &config); err != nil { - return nil, err - } - - // A specific API is being requested (usually done for testing). - if config.API != "" { - for _, v := range eventLogs { - debugf("Checking %s", v.apiName) - if strings.EqualFold(v.apiName, config.API) { - debugf("Using %s API for event log %s", v.apiName, config.Name) - e, err := v.producer(options) - return e, err - } - } - - return nil, fmt.Errorf("%s API is not available", config.API) - } - - // Use the API with the highest priority. - keys := make([]int, 0, len(eventLogs)) - for key := range eventLogs { - keys = append(keys, key) - } - sort.Ints(keys) - - eventLog := eventLogs[keys[0]] - debugf("Using highest priority API, %s, for event log %s", - eventLog.apiName, config.Name) - e, err := eventLog.producer(options) - return e, err -} diff --git a/winlogbeat/eventlog/factory_other.go b/winlogbeat/eventlog/factory_other.go new file mode 100644 index 000000000000..83052154507a --- /dev/null +++ b/winlogbeat/eventlog/factory_other.go @@ -0,0 +1,31 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build !windows + +package eventlog + +import ( + "errors" + + conf "github.com/elastic/elastic-agent-libs/config" +) + +// New creates and returns a new EventLog instance based on the given config. +func New(options *conf.C) (EventLog, error) { + return nil, errors.New("only supported on windows platform") +} diff --git a/winlogbeat/eventlog/factory_windows.go b/winlogbeat/eventlog/factory_windows.go new file mode 100644 index 000000000000..aeac75422934 --- /dev/null +++ b/winlogbeat/eventlog/factory_windows.go @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build windows + +package eventlog + +import ( + conf "github.com/elastic/elastic-agent-libs/config" +) + +// New creates and returns a new EventLog instance based on the given config. +func New(options *conf.C) (EventLog, error) { + var config config + if err := readConfig(options, &config); err != nil { + return nil, err + } + return newWinEventLog(options) +} diff --git a/winlogbeat/eventlog/metrics.go b/winlogbeat/eventlog/metrics.go new file mode 100644 index 000000000000..45c1d1edf126 --- /dev/null +++ b/winlogbeat/eventlog/metrics.go @@ -0,0 +1,151 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build windows + +package eventlog + +import ( + "expvar" + "strconv" + "syscall" + "time" + + "github.com/rcrowley/go-metrics" + + "github.com/elastic/beats/v7/libbeat/monitoring/inputmon" + "github.com/elastic/elastic-agent-libs/monitoring" + "github.com/elastic/elastic-agent-libs/monitoring/adapter" +) + +var ( + // dropReasons contains counters for the number of dropped events for each + // reason. + dropReasons = expvar.NewMap("drop_reasons") + + // readErrors contains counters for the read error types that occur. + readErrors = expvar.NewMap("read_errors") +) + +// incrementMetric increments a value in the specified expvar.Map. The key +// should be a windows syscall.Errno or a string. Any other types will be +// reported under the "other" key. +func incrementMetric(v *expvar.Map, key interface{}) { + switch t := key.(type) { + default: + v.Add("other", 1) + case string: + v.Add(t, 1) + case syscall.Errno: + v.Add(strconv.Itoa(int(t)), 1) + } +} + +// inputMetrics handles event log metric reporting. +type inputMetrics struct { + unregister func() + + lastBatch time.Time + + name *monitoring.String // name of the provider being read + events *monitoring.Uint // total number of events received + dropped *monitoring.Uint // total number of discarded events + errors *monitoring.Uint // total number of errors + batchSize metrics.Sample // histogram of the number of events in each non-zero batch + sourceLag metrics.Sample // histogram of the difference between timestamped event's creation and reading + batchPeriod metrics.Sample // histogram of the elapsed time between non-zero batch reads +} + +// newInputMetrics returns an input metric for windows event logs. If id is empty +// a nil inputMetric is returned. +func newInputMetrics(name, id string) *inputMetrics { + if id == "" { + return nil + } + reg, unreg := inputmon.NewInputRegistry("winlog", id, nil) + out := &inputMetrics{ + unregister: unreg, + name: monitoring.NewString(reg, "provider"), + events: monitoring.NewUint(reg, "received_events_total"), + dropped: monitoring.NewUint(reg, "discarded_events_total"), + errors: monitoring.NewUint(reg, "errors_total"), + batchSize: metrics.NewUniformSample(1024), + sourceLag: metrics.NewUniformSample(1024), + batchPeriod: metrics.NewUniformSample(1024), + } + out.name.Set(name) + _ = adapter.NewGoMetrics(reg, "received_events_count", adapter.Accept). + Register("histogram", metrics.NewHistogram(out.batchSize)) + _ = adapter.NewGoMetrics(reg, "source_lag_time", adapter.Accept). + Register("histogram", metrics.NewHistogram(out.sourceLag)) + _ = adapter.NewGoMetrics(reg, "batch_read_period", adapter.Accept). + Register("histogram", metrics.NewHistogram(out.batchPeriod)) + + return out +} + +// log logs metric for the given batch. +func (m *inputMetrics) log(batch []Record) { + if m == nil { + return + } + if len(batch) == 0 { + return + } + + now := time.Now() + if !m.lastBatch.IsZero() { + m.batchPeriod.Update(now.Sub(m.lastBatch).Nanoseconds()) + } + m.lastBatch = now + + m.events.Add(uint64(len(batch))) + m.batchSize.Update(int64(len(batch))) + for _, r := range batch { + m.sourceLag.Update(now.Sub(r.TimeCreated.SystemTime).Nanoseconds()) + } +} + +// logError logs error metrics. Nil errors do not increment the error +// count but the err value is currently otherwise not used. It is included +// to allow easier extension of the metrics to include error stratification. +func (m *inputMetrics) logError(err error) { + if m == nil { + return + } + if err == nil { + return + } + m.errors.Inc() +} + +// logDropped logs dropped event metrics. Nil errors *do* increment the dropped +// count; the value is currently otherwise not used, but is included to allow +// easier extension of the metrics to include error stratification. +func (m *inputMetrics) logDropped(_ error) { + if m == nil { + return + } + m.dropped.Inc() +} + +func (m *inputMetrics) close() { + if m == nil { + return + } + m.unregister() +} diff --git a/winlogbeat/eventlog/wineventlog.go b/winlogbeat/eventlog/wineventlog.go index 4fee3824122c..84e65aa95ab3 100644 --- a/winlogbeat/eventlog/wineventlog.go +++ b/winlogbeat/eventlog/wineventlog.go @@ -20,200 +20,48 @@ package eventlog import ( - "encoding/xml" - "errors" - "expvar" "fmt" "io" + "os" "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/joeshaw/multierror" - "github.com/rcrowley/go-metrics" + + "go.uber.org/multierr" "golang.org/x/sys/windows" - "github.com/elastic/beats/v7/libbeat/common/cfgwarn" - "github.com/elastic/beats/v7/libbeat/monitoring/inputmon" "github.com/elastic/beats/v7/winlogbeat/checkpoint" - "github.com/elastic/beats/v7/winlogbeat/sys" - "github.com/elastic/beats/v7/winlogbeat/sys/winevent" win "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog" conf "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/logp" - "github.com/elastic/elastic-agent-libs/monitoring" - "github.com/elastic/elastic-agent-libs/monitoring/adapter" -) - -var ( - detailSelector = "eventlog_detail" - detailf = logp.MakeDebug(detailSelector) - - // dropReasons contains counters for the number of dropped events for each - // reason. - dropReasons = expvar.NewMap("drop_reasons") - - // readErrors contains counters for the read error types that occur. - readErrors = expvar.NewMap("read_errors") ) -const ( - // renderBufferSize is the size in bytes of the buffer used to render events. - renderBufferSize = 1 << 19 // 512KB, 256K wide characters - // winEventLogApiName is the name used to identify the Windows Event Log API - // as both an event type and an API. - winEventLogAPIName = "wineventlog" - - // eventLoggingAPIName is the name used to identify the Event Logging API - // as both an event type and an API. - eventLoggingAPIName = "eventlogging" - - // metaTTL is the length of time a WinMeta value is valid in the cache. - metaTTL = time.Hour -) - -func init() { - // Register wineventlog API if it is available. - available, _ := win.IsAvailable() - if available { - Register(winEventLogAPIName, 0, newWinEventLog, win.Channels) - Register(eventLoggingAPIName, 1, newEventLogging, win.Channels) - } -} - -type winEventLogConfig struct { - ConfigCommon `config:",inline"` - BatchReadSize int `config:"batch_read_size"` // Maximum number of events that Read will return. - IncludeXML bool `config:"include_xml"` - Forwarded *bool `config:"forwarded"` - SimpleQuery query `config:",inline"` - NoMoreEvents NoMoreEventsAction `config:"no_more_events"` // Action to take when no more events are available - wait or stop. - EventLanguage uint32 `config:"language"` -} - -// query contains parameters used to customize the event log data that is -// queried from the log. -type query struct { - IgnoreOlder time.Duration `config:"ignore_older"` // Ignore records older than this period of time. - EventID string `config:"event_id"` // White-list and black-list of events. - Level string `config:"level"` // Severity level. - Provider []string `config:"provider"` // Provider (source name). -} - -// NoMoreEventsAction defines what action for the reader to take when -// ERROR_NO_MORE_ITEMS is returned by the Windows API. -type NoMoreEventsAction uint8 - -const ( - // Wait for new events. - Wait NoMoreEventsAction = iota - // Stop the reader. - Stop -) - -var noMoreEventsActionNames = map[NoMoreEventsAction]string{ - Wait: "wait", - Stop: "stop", -} - -// Unpack sets the action based on the string value. -func (a *NoMoreEventsAction) Unpack(v string) error { - v = strings.ToLower(v) - for action, name := range noMoreEventsActionNames { - if v == name { - *a = action - return nil - } - } - return fmt.Errorf("invalid no_more_events action: %v", v) -} - -// String returns the name of the action. -func (a NoMoreEventsAction) String() string { return noMoreEventsActionNames[a] } - -// defaultWinEventLogConfig is the default configuration for new wineventlog readers. -var defaultWinEventLogConfig = winEventLogConfig{ - BatchReadSize: 100, -} - -// Validate validates the winEventLogConfig data and returns an error describing -// any problems or nil. -func (c *winEventLogConfig) Validate() error { - var errs multierror.Errors - - if c.XMLQuery != "" { - if c.ID == "" { - errs = append(errs, fmt.Errorf("event log is missing an 'id'")) - } - - // Check for XML syntax errors. This does not check the validity of the query itself. - if err := xml.Unmarshal([]byte(c.XMLQuery), &struct{}{}); err != nil { - errs = append(errs, fmt.Errorf("invalid xml_query: %w", err)) - } - - switch { - case c.Name != "": - errs = append(errs, fmt.Errorf("xml_query cannot be used with 'name'")) - case c.SimpleQuery.IgnoreOlder != 0: - errs = append(errs, fmt.Errorf("xml_query cannot be used with 'ignore_older'")) - case c.SimpleQuery.Level != "": - errs = append(errs, fmt.Errorf("xml_query cannot be used with 'level'")) - case c.SimpleQuery.EventID != "": - errs = append(errs, fmt.Errorf("xml_query cannot be used with 'event_id'")) - case len(c.SimpleQuery.Provider) != 0: - errs = append(errs, fmt.Errorf("xml_query cannot be used with 'provider'")) - } - } else if c.Name == "" { - errs = append(errs, fmt.Errorf("event log is missing a 'name'")) - } - - return errs.Err() -} - -// Validate that winEventLog implements the EventLog interface. -var _ EventLog = &winEventLog{} - -// winEventLog implements the EventLog interface for reading from the Windows +// winEventLogRaw implements the EventLog interface for reading from the Windows // Event Log API. -type winEventLog struct { - config winEventLogConfig - query string - id string // Identifier of this event log. - channelName string // Name of the channel from which to read. - file bool // Reading from file rather than channel. - subscription win.EvtHandle // Handle to the subscription. - maxRead int // Maximum number returned in one Read. - lastRead checkpoint.EventLogState // Record number of the last read event. - - render func(event win.EvtHandle, out io.Writer) error // Function for rendering the event to XML. - message func(event win.EvtHandle) (string, error) // Message fallback function. - renderBuf []byte // Buffer used for rendering event. - outputBuf *sys.ByteBuffer // Buffer for receiving XML - cache *messageFilesCache // Cached mapping of source name to event message file handles. - - winMetaCache // Cached WinMeta tables by provider. - - logPrefix string // String to prefix on log messages. +type winEventLogRaw struct { + config config + query string + id string // Identifier of this event log. + channelName string // Name of the channel from which to read. + file bool // Reading from file rather than channel. + maxRead int // Maximum number returned in one Read. + lastRead checkpoint.EventLogState // Record number of the last read event. + log *logp.Logger + + iterator *win.EventIterator + renderer win.EventRenderer metrics *inputMetrics } -func newEventLogging(options *conf.C) (EventLog, error) { - cfgwarn.Deprecate("8.0.0", fmt.Sprintf("api %s is deprecated and %s will be used instead", eventLoggingAPIName, winEventLogAPIName)) - return newWinEventLog(options) -} - // newWinEventLog creates and returns a new EventLog for reading event logs // using the Windows Event Log. func newWinEventLog(options *conf.C) (EventLog, error) { var xmlQuery string var err error + var isFile bool + var log *logp.Logger - c := defaultWinEventLogConfig - if err = readConfig(options, &c); err != nil { + c := config{BatchReadSize: 512} + if err := readConfig(options, &c); err != nil { return nil, err } @@ -224,9 +72,20 @@ func newWinEventLog(options *conf.C) (EventLog, error) { if c.XMLQuery != "" { xmlQuery = c.XMLQuery + log = logp.NewLogger("wineventlog").With("id", id) } else { + queryLog := c.Name + if info, err := os.Stat(c.Name); err == nil && info.Mode().IsRegular() { + path, err := filepath.Abs(c.Name) + if err != nil { + return nil, err + } + isFile = true + queryLog = "file://" + path + } + xmlQuery, err = win.Query{ - Log: c.Name, + Log: queryLog, IgnoreOlder: c.SimpleQuery.IgnoreOlder, Level: c.SimpleQuery.Level, EventID: c.SimpleQuery.EventID, @@ -235,100 +94,90 @@ func newWinEventLog(options *conf.C) (EventLog, error) { if err != nil { return nil, err } - } - eventMetadataHandle := func(providerName, sourceName string) sys.MessageFiles { - mf := sys.MessageFiles{SourceName: sourceName} - h, err := win.OpenPublisherMetadata(0, sourceName, c.EventLanguage) + log = logp.NewLogger("wineventlog").With("id", id).With("channel", c.Name) + } + + l := &winEventLogRaw{ + config: c, + query: xmlQuery, + id: id, + channelName: c.Name, + file: isFile, + maxRead: c.BatchReadSize, + log: log, + } + + switch c.IncludeXML { + case true: + l.renderer = win.NewXMLRenderer( + win.RenderConfig{ + IsForwarded: l.isForwarded(), + Locale: c.EventLanguage, + }, + win.NilHandle, log) + case false: + l.renderer, err = win.NewRenderer( + win.RenderConfig{ + IsForwarded: l.isForwarded(), + Locale: c.EventLanguage, + }, + win.NilHandle, log) if err != nil { - mf.Err = err - return mf - } - - mf.Handles = []sys.FileHandle{{Handle: uintptr(h)}} - return mf - } - - freeHandle := func(handle uintptr) error { - return win.Close(win.EvtHandle(handle)) - } - - if filepath.IsAbs(c.Name) { - c.Name = filepath.Clean(c.Name) - } - - l := &winEventLog{ - id: id, - config: c, - query: xmlQuery, - channelName: c.Name, - file: filepath.IsAbs(c.Name), - maxRead: c.BatchReadSize, - renderBuf: make([]byte, renderBufferSize), - outputBuf: sys.NewByteBuffer(renderBufferSize), - cache: newMessageFilesCache(id, eventMetadataHandle, freeHandle), - winMetaCache: newWinMetaCache(metaTTL), - logPrefix: fmt.Sprintf("WinEventLog[%s]", id), - } - - // Forwarded events should be rendered using RenderEventXML. It is more - // efficient and does not attempt to use local message files for rendering - // the event's message. - switch { - case l.isForwarded(): - l.render = func(event win.EvtHandle, out io.Writer) error { - return win.RenderEventXML(event, l.renderBuf, out) - } - default: - l.render = func(event win.EvtHandle, out io.Writer) error { - return win.RenderEvent(event, c.EventLanguage, l.renderBuf, l.cache.get, out) - } - l.message = func(event win.EvtHandle) (string, error) { - return win.Message(event, l.renderBuf, l.cache.get) + return nil, err } } return l, nil } -func (l *winEventLog) isForwarded() bool { +func (l *winEventLogRaw) isForwarded() bool { c := l.config return (c.Forwarded != nil && *c.Forwarded) || (c.Forwarded == nil && c.Name == "ForwardedEvents") } // Name returns the name of the event log (i.e. Application, Security, etc.). -func (l *winEventLog) Name() string { +func (l *winEventLogRaw) Name() string { return l.id } // Channel returns the event log's channel name. -func (l *winEventLog) Channel() string { +func (l *winEventLogRaw) Channel() string { return l.channelName } // IsFile returns true if the event log is an evtx file. -func (l *winEventLog) IsFile() bool { +func (l *winEventLogRaw) IsFile() bool { return l.file } -func (l *winEventLog) Open(state checkpoint.EventLogState) error { - var bookmark win.EvtHandle - var err error +func (l *winEventLogRaw) Open(state checkpoint.EventLogState) error { + l.lastRead = state // we need to defer metrics initialization since when the event log // is used from winlog input it would register it twice due to CheckConfig calls if l.metrics == nil { l.metrics = newInputMetrics(l.channelName, l.id) } + + var err error + l.iterator, err = win.NewEventIterator( + win.WithSubscriptionFactory(func() (handle win.EvtHandle, err error) { + return l.open(l.lastRead) + }), + win.WithBatchSize(l.maxRead)) + return err +} + +func (l *winEventLogRaw) open(state checkpoint.EventLogState) (win.EvtHandle, error) { + var bookmark win.Bookmark if len(state.Bookmark) > 0 { - bookmark, err = win.CreateBookmarkFromXML(state.Bookmark) - } else if state.RecordNumber > 0 && l.channelName != "" { - bookmark, err = win.CreateBookmarkFromRecordID(l.channelName, state.RecordNumber) - } - if err != nil { - l.metrics.logError(err) - return err + var err error + bookmark, err = win.NewBookmarkFromXML(state.Bookmark) + if err != nil { + return win.NilHandle, err + } + defer bookmark.Close() } - defer win.Close(bookmark) if l.file { return l.openFile(state, bookmark) @@ -336,54 +185,49 @@ func (l *winEventLog) Open(state checkpoint.EventLogState) error { return l.openChannel(bookmark) } -func (l *winEventLog) openFile(state checkpoint.EventLogState, bookmark win.EvtHandle) error { +func (l *winEventLogRaw) openFile(state checkpoint.EventLogState, bookmark win.Bookmark) (win.EvtHandle, error) { path := l.channelName h, err := win.EvtQuery(0, path, l.query, win.EvtQueryFilePath|win.EvtQueryForwardDirection) if err != nil { - l.metrics.logError(err) - return fmt.Errorf("failed to get handle to event log file %v: %w", path, err) + return win.NilHandle, fmt.Errorf("failed to get handle to event log file %v: %w", path, err) } if bookmark > 0 { - debugf("%s Seeking to bookmark. timestamp=%v bookmark=%v", - l.logPrefix, state.Timestamp, state.Bookmark) + l.log.Debugf("Seeking to bookmark. timestamp=%v bookmark=%v", + state.Timestamp, state.Bookmark) // This seeks to the last read event and strictly validates that the // bookmarked record number exists. - if err = win.EvtSeek(h, 0, bookmark, win.EvtSeekRelativeToBookmark|win.EvtSeekStrict); err == nil { + if err = win.EvtSeek(h, 0, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark|win.EvtSeekStrict); err == nil { // Then we advance past the last read event to avoid sending that // event again. This won't fail if we're at the end of the file. - if seekErr := win.EvtSeek(h, 1, bookmark, win.EvtSeekRelativeToBookmark); seekErr != nil { + if seekErr := win.EvtSeek(h, 1, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark); seekErr != nil { err = fmt.Errorf("failed to seek past bookmarked position: %w", seekErr) } } else { - logp.Warn("%s Failed to seek to bookmarked location in %v (error: %v). "+ + l.log.Warnf("s Failed to seek to bookmarked location in %v (error: %v). "+ "Recovering by reading the log from the beginning. (Did the file "+ - "change since it was last read?)", l.logPrefix, path, err) - l.metrics.logError(err) + "change since it was last read?)", path, err) if seekErr := win.EvtSeek(h, 0, 0, win.EvtSeekRelativeToFirst); seekErr != nil { err = fmt.Errorf("failed to seek to beginning of log: %w", seekErr) } } if err != nil { - l.metrics.logError(err) - return err + return win.NilHandle, err } } - l.subscription = h - return nil + return h, err } -func (l *winEventLog) openChannel(bookmark win.EvtHandle) error { +func (l *winEventLogRaw) openChannel(bookmark win.Bookmark) (win.EvtHandle, error) { // Using a pull subscription to receive events. See: // https://msdn.microsoft.com/en-us/library/windows/desktop/aa385771(v=vs.85).aspx#pull signalEvent, err := windows.CreateEvent(nil, 0, 0, nil) if err != nil { - l.metrics.logError(err) - return err + return win.NilHandle, err } defer windows.CloseHandle(signalEvent) //nolint:errcheck // This is just a resource release. @@ -399,348 +243,129 @@ func (l *winEventLog) openChannel(bookmark win.EvtHandle) error { flags = win.EvtSubscribeStartAtOldestRecord } - debugf("%s using subscription query=%s", l.logPrefix, l.query) - subscriptionHandle, err := win.Subscribe( + l.log.Debugw("Using subscription query.", "winlog.query", l.query) + h, err := win.Subscribe( 0, // Session - nil for localhost signalEvent, - "", // Channel - empty b/c channel is in the query - l.query, // Query - nil means all events - bookmark, // Bookmark - for resuming from a specific event + "", // Channel - empty b/c channel is in the query + l.query, // Query - nil means all events + win.EvtHandle(bookmark), // Bookmark - for resuming from a specific event flags) - switch { - case errors.Is(err, win.ERROR_NOT_FOUND), errors.Is(err, win.ERROR_EVT_QUERY_RESULT_STALE), - errors.Is(err, win.ERROR_EVT_QUERY_RESULT_INVALID_POSITION): - debugf("%s error subscribing (first chance): %v", l.logPrefix, err) + switch err { //nolint:errorlint // This is an errno or nil. + case nil: + return h, nil + case win.ERROR_NOT_FOUND, win.ERROR_EVT_QUERY_RESULT_STALE, win.ERROR_EVT_QUERY_RESULT_INVALID_POSITION: // The bookmarked event was not found, we retry the subscription from the start. - l.metrics.logError(err) incrementMetric(readErrors, err) - subscriptionHandle, err = win.Subscribe(0, signalEvent, "", l.query, 0, win.EvtSubscribeStartAtOldestRecord) - } - - if err != nil { - l.metrics.logError(err) - debugf("%s error subscribing (final): %v", l.logPrefix, err) - return err + return win.Subscribe(0, signalEvent, "", l.query, 0, win.EvtSubscribeStartAtOldestRecord) + default: + return 0, err } - - l.subscription = subscriptionHandle - return nil } -func (l *winEventLog) Read() ([]Record, error) { - handles, _, err := l.eventHandles(l.maxRead) - if err != nil || len(handles) == 0 { - return nil, err - } - +func (l *winEventLogRaw) Read() ([]Record, error) { //nolint:prealloc // Avoid unnecessary preallocation for each reader every second when event log is inactive. var records []Record defer func() { l.metrics.log(records) - for _, h := range handles { - win.Close(h) - } }() - detailf("%s EventHandles returned %d handles", l.logPrefix, len(handles)) - for _, h := range handles { - l.outputBuf.Reset() - err := l.render(h, l.outputBuf) - l.metrics.logError(err) - if err != nil && l.outputBuf.Len() == 0 { - logp.Err("%s Dropping event with rendering error. %v", l.logPrefix, err) + for h, ok := l.iterator.Next(); ok; h, ok = l.iterator.Next() { + record, err := l.processHandle(h) + if err != nil { + l.metrics.logError(err) + l.log.Warnw("Dropping event due to rendering error.", "error", err) l.metrics.logDropped(err) incrementMetric(dropReasons, err) continue } + records = append(records, *record) - r := l.buildRecordFromXML(l.outputBuf.Bytes(), err) - r.Offset = checkpoint.EventLogState{ - Name: l.id, - RecordNumber: r.RecordID, - Timestamp: r.TimeCreated.SystemTime, + // It has read the maximum requested number of events. + if len(records) >= l.maxRead { + return records, nil } - if r.Offset.Bookmark, err = l.createBookmarkFromEvent(h); err != nil { - l.metrics.logError(err) - logp.Warn("%s failed creating bookmark: %v", l.logPrefix, err) - } - if r.Message == "" && l.message != nil { - r.Message, err = l.message(h) - if err != nil { - l.metrics.logError(err) - logp.Warn("%s error salvaging message (event id=%d qualifier=%d provider=%q created at %s will be included without a message): %v", - l.logPrefix, r.EventIdentifier.ID, r.EventIdentifier.Qualifiers, r.Provider.Name, r.TimeCreated.SystemTime, err) - } - } - records = append(records, r) - l.lastRead = r.Offset } - debugf("%s Read() is returning %d records", l.logPrefix, len(records)) - return records, nil -} - -func (l *winEventLog) eventHandles(maxRead int) ([]win.EvtHandle, int, error) { - handles, err := win.EventHandles(l.subscription, maxRead) - switch err { //nolint:errorlint // This is an errno or nil. - case nil: - if l.maxRead > maxRead { - debugf("%s Recovered from RPC_S_INVALID_BOUND error (errno 1734) "+ - "by decreasing batch_read_size to %v", l.logPrefix, maxRead) - } - return handles, maxRead, nil - case win.ERROR_NO_MORE_ITEMS: - detailf("%s No more events", l.logPrefix) - if l.config.NoMoreEvents == Stop { - return nil, maxRead, io.EOF - } - return nil, maxRead, nil - case win.RPC_S_INVALID_BOUND: - incrementMetric(readErrors, err) + // An error occurred while retrieving more events. + if err := l.iterator.Err(); err != nil { l.metrics.logError(err) - if err := l.Close(); err != nil { - return nil, 0, fmt.Errorf("failed to recover from RPC_S_INVALID_BOUND: %w", err) - } - if err := l.Open(l.lastRead); err != nil { - return nil, 0, fmt.Errorf("failed to recover from RPC_S_INVALID_BOUND: %w", err) - } - return l.eventHandles(maxRead / 2) - default: - l.metrics.logError(err) - incrementMetric(readErrors, err) - logp.Warn("%s EventHandles returned error %v", l.logPrefix, err) - return nil, 0, err + return records, err } -} -func (l *winEventLog) buildRecordFromXML(x []byte, recoveredErr error) Record { - includeXML := l.config.IncludeXML - e, err := winevent.UnmarshalXML(x) - if err != nil { - e.RenderErr = append(e.RenderErr, err.Error()) - // Add raw XML to event.original when decoding fails - includeXML = true + // Reader is configured to stop when there are no more events. + if Stop == l.config.NoMoreEvents { + return records, io.EOF } - err = winevent.PopulateAccount(&e.User) - if err != nil { - debugf("%s SID %s account lookup failed. %v", l.logPrefix, - e.User.Identifier, err) - } + return records, nil +} - if e.RenderErrorCode != 0 { - // Convert the render error code to an error message that can be - // included in the "error.message" field. - e.RenderErr = append(e.RenderErr, syscall.Errno(e.RenderErrorCode).Error()) - } else if recoveredErr != nil { - e.RenderErr = append(e.RenderErr, recoveredErr.Error()) - } +func (l *winEventLogRaw) processHandle(h win.EvtHandle) (*Record, error) { + defer h.Close() - // Get basic string values for raw fields. - winevent.EnrichRawValuesWithNames(l.winMeta(e.Provider.Name, l.config.EventLanguage), &e) - if e.Level == "" { - // Fallback on LevelRaw if the Level is not set in the RenderingInfo. - e.Level = win.EventLevel(e.LevelRaw).String() + // NOTE: Render can return an error and a partial event. + evt, xml, err := l.renderer.Render(h) + if evt == nil { + return nil, err + } + if err != nil { + evt.RenderErr = append(evt.RenderErr, err.Error()) } - if logp.IsDebug(detailSelector) { - detailf("%s XML=%s Event=%+v", l.logPrefix, x, e) + r := &Record{ + Event: *evt, } - r := Record{ - API: winEventLogAPIName, - Event: e, + if l.config.IncludeXML { + r.XML = xml } if l.file { r.File = l.id } - if includeXML { - r.XML = string(x) + r.Offset = checkpoint.EventLogState{ + Name: l.id, + RecordNumber: r.RecordID, + Timestamp: r.TimeCreated.SystemTime, } - - return r -} - -func (l *winEventLog) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) { - bmHandle, err := win.CreateBookmarkFromEvent(evtHandle) - if err != nil { - return "", err + if r.Offset.Bookmark, err = l.createBookmarkFromEvent(h); err != nil { + l.metrics.logError(err) + l.log.Warnw("Failed creating bookmark.", "error", err) } - l.outputBuf.Reset() - err = win.RenderBookmarkXML(bmHandle, l.renderBuf, l.outputBuf) - win.Close(bmHandle) - return string(l.outputBuf.Bytes()), err + l.lastRead = r.Offset + return r, nil } -func (l *winEventLog) Reset() error { - debugf("%s Closing handle for reset", l.logPrefix) - return win.Close(l.subscription) -} - -func (l *winEventLog) Close() error { - debugf("%s Closing handle", l.logPrefix) - l.metrics.close() - return win.Close(l.subscription) -} - -// winMetaCache retrieves and caches WinMeta tables by provider name. -// It is a cut down version of the PublisherMetadataStore caching in wineventlog.Renderer. -type winMetaCache struct { - ttl time.Duration - logger *logp.Logger - - mu sync.RWMutex - cache map[string]winMetaCacheEntry -} - -type winMetaCacheEntry struct { - expire time.Time - *winevent.WinMeta -} - -func newWinMetaCache(ttl time.Duration) winMetaCache { - return winMetaCache{cache: make(map[string]winMetaCacheEntry), ttl: ttl, logger: logp.L()} -} - -func (c *winMetaCache) winMeta(provider string, locale uint32) *winevent.WinMeta { - c.mu.RLock() - e, ok := c.cache[provider] - c.mu.RUnlock() - if ok && time.Until(e.expire) > 0 { - return e.WinMeta - } - - // Upgrade lock. - defer c.mu.Unlock() - c.mu.Lock() - - // Did the cache get updated during lock upgrade? - // No need to check expiry here since we must have a new entry - // if there is an entry at all. - if e, ok := c.cache[provider]; ok { - return e.WinMeta - } - - s, err := win.NewPublisherMetadataStore(win.NilHandle, provider, locale, c.logger) +func (l *winEventLogRaw) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) { + bookmark, err := win.NewBookmarkFromEvent(evtHandle) if err != nil { - // Return an empty store on error (can happen in cases where the - // log was forwarded and the provider doesn't exist on collector). - s = win.NewEmptyPublisherMetadataStore(provider, c.logger) - logp.Warn("failed to load publisher metadata for %v (returning an empty metadata store): %v", provider, err) + return "", fmt.Errorf("failed to create new bookmark from event handle: %w", err) } - s.Close() - c.cache[provider] = winMetaCacheEntry{expire: time.Now().Add(c.ttl), WinMeta: &s.WinMeta} - return &s.WinMeta -} + defer bookmark.Close() -// incrementMetric increments a value in the specified expvar.Map. The key -// should be a windows syscall.Errno or a string. Any other types will be -// reported under the "other" key. -func incrementMetric(v *expvar.Map, key interface{}) { - switch t := key.(type) { - default: - v.Add("other", 1) - case string: - v.Add(t, 1) - case syscall.Errno: - v.Add(strconv.Itoa(int(t)), 1) - } + return bookmark.XML() } -// inputMetrics handles event log metric reporting. -type inputMetrics struct { - unregister func() - - lastBatch time.Time - - name *monitoring.String // name of the provider being read - events *monitoring.Uint // total number of events received - dropped *monitoring.Uint // total number of discarded events - errors *monitoring.Uint // total number of errors - batchSize metrics.Sample // histogram of the number of events in each non-zero batch - sourceLag metrics.Sample // histogram of the difference between timestamped event's creation and reading - batchPeriod metrics.Sample // histogram of the elapsed time between non-zero batch reads -} - -// newInputMetrics returns an input metric for windows event logs. If id is empty -// a nil inputMetric is returned. -func newInputMetrics(name, id string) *inputMetrics { - if id == "" { - return nil - } - reg, unreg := inputmon.NewInputRegistry("winlog", id, nil) - out := &inputMetrics{ - unregister: unreg, - name: monitoring.NewString(reg, "provider"), - events: monitoring.NewUint(reg, "received_events_total"), - dropped: monitoring.NewUint(reg, "discarded_events_total"), - errors: monitoring.NewUint(reg, "errors_total"), - batchSize: metrics.NewUniformSample(1024), - sourceLag: metrics.NewUniformSample(1024), - batchPeriod: metrics.NewUniformSample(1024), - } - out.name.Set(name) - _ = adapter.NewGoMetrics(reg, "received_events_count", adapter.Accept). - Register("histogram", metrics.NewHistogram(out.batchSize)) - _ = adapter.NewGoMetrics(reg, "source_lag_time", adapter.Accept). - Register("histogram", metrics.NewHistogram(out.sourceLag)) - _ = adapter.NewGoMetrics(reg, "batch_read_period", adapter.Accept). - Register("histogram", metrics.NewHistogram(out.batchPeriod)) - - return out -} - -// log logs metric for the given batch. -func (m *inputMetrics) log(batch []Record) { - if m == nil { - return - } - if len(batch) == 0 { - return - } - - now := time.Now() - if !m.lastBatch.IsZero() { - m.batchPeriod.Update(now.Sub(m.lastBatch).Nanoseconds()) - } - m.lastBatch = now - - m.events.Add(uint64(len(batch))) - m.batchSize.Update(int64(len(batch))) - for _, r := range batch { - m.sourceLag.Update(now.Sub(r.TimeCreated.SystemTime).Nanoseconds()) - } -} - -// logError logs error metrics. Nil errors do not increment the error -// count but the err value is currently otherwise not used. It is included -// to allow easier extension of the metrics to include error stratification. -func (m *inputMetrics) logError(err error) { - if m == nil { - return - } - if err == nil { - return - } - m.errors.Inc() +func (l *winEventLogRaw) Reset() error { + l.log.Debug("Closing event log reader handles for reset.") + return l.close() } -// logDropped logs dropped event metrics. Nil errors *do* increment the dropped -// count; the value is currently otherwise not used, but is included to allow -// easier extension of the metrics to include error stratification. -func (m *inputMetrics) logDropped(_ error) { - if m == nil { - return - } - m.dropped.Inc() +func (l *winEventLogRaw) Close() error { + l.log.Debug("Closing event log reader handles.") + l.metrics.close() + return l.close() } -func (m *inputMetrics) close() { - if m == nil { - return +func (l *winEventLogRaw) close() error { + if l.iterator == nil { + return l.renderer.Close() } - m.unregister() + return multierr.Combine( + l.iterator.Close(), + l.renderer.Close(), + ) } diff --git a/winlogbeat/eventlog/wineventlog_raw.go b/winlogbeat/eventlog/wineventlog_raw.go deleted file mode 100644 index 7a6917e9b7ad..000000000000 --- a/winlogbeat/eventlog/wineventlog_raw.go +++ /dev/null @@ -1,390 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//go:build windows - -package eventlog - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "go.uber.org/multierr" - "golang.org/x/sys/windows" - - "github.com/elastic/beats/v7/winlogbeat/checkpoint" - win "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog" - conf "github.com/elastic/elastic-agent-libs/config" - "github.com/elastic/elastic-agent-libs/logp" -) - -const ( - // winEventLogExpApiName is the name used to identify the Windows Event Log API - // as both an event type and an API. - winEventLogExpAPIName = "wineventlog-experimental" - // winEventLogRawAPIName is the name used to identify the Windows Event Log API - // as both an event type and an API. - winEventLogRawAPIName = "wineventlog-raw" -) - -func init() { - // Register wineventlog API if it is available. - available, _ := win.IsAvailable() - if available { - Register(winEventLogExpAPIName, 10, newWinEventLogRaw, win.Channels) - Register(winEventLogRawAPIName, 11, newWinEventLogRaw, win.Channels) - } -} - -// winEventLogRaw implements the EventLog interface for reading from the Windows -// Event Log API. -type winEventLogRaw struct { - config winEventLogConfig - query string - id string // Identifier of this event log. - channelName string // Name of the channel from which to read. - file bool // Reading from file rather than channel. - maxRead int // Maximum number returned in one Read. - lastRead checkpoint.EventLogState // Record number of the last read event. - log *logp.Logger - - iterator *win.EventIterator - renderer win.EventRenderer - - metrics *inputMetrics -} - -// newWinEventLogRaw creates and returns a new EventLog for reading event logs -// using the Windows Event Log. -func newWinEventLogRaw(options *conf.C) (EventLog, error) { - var xmlQuery string - var err error - var isFile bool - var log *logp.Logger - - c := winEventLogConfig{BatchReadSize: 512} - if err := readConfig(options, &c); err != nil { - return nil, err - } - - id := c.ID - if id == "" { - id = c.Name - } - - if c.XMLQuery != "" { - xmlQuery = c.XMLQuery - log = logp.NewLogger("wineventlog").With("id", id) - } else { - queryLog := c.Name - if info, err := os.Stat(c.Name); err == nil && info.Mode().IsRegular() { - path, err := filepath.Abs(c.Name) - if err != nil { - return nil, err - } - isFile = true - queryLog = "file://" + path - } - - xmlQuery, err = win.Query{ - Log: queryLog, - IgnoreOlder: c.SimpleQuery.IgnoreOlder, - Level: c.SimpleQuery.Level, - EventID: c.SimpleQuery.EventID, - Provider: c.SimpleQuery.Provider, - }.Build() - if err != nil { - return nil, err - } - - log = logp.NewLogger("wineventlog").With("id", id).With("channel", c.Name) - } - - l := &winEventLogRaw{ - config: c, - query: xmlQuery, - id: id, - channelName: c.Name, - file: isFile, - maxRead: c.BatchReadSize, - log: log, - } - - switch c.IncludeXML { - case true: - l.renderer = win.NewXMLRenderer( - win.RenderConfig{ - IsForwarded: l.isForwarded(), - Locale: c.EventLanguage, - }, - win.NilHandle, log) - case false: - l.renderer, err = win.NewRenderer( - win.RenderConfig{ - IsForwarded: l.isForwarded(), - Locale: c.EventLanguage, - }, - win.NilHandle, log) - if err != nil { - return nil, err - } - } - - return l, nil -} - -func (l *winEventLogRaw) isForwarded() bool { - c := l.config - return (c.Forwarded != nil && *c.Forwarded) || (c.Forwarded == nil && c.Name == "ForwardedEvents") -} - -// Name returns the name of the event log (i.e. Application, Security, etc.). -func (l *winEventLogRaw) Name() string { - return l.id -} - -// Channel returns the event log's channel name. -func (l *winEventLogRaw) Channel() string { - return l.channelName -} - -// IsFile returns true if the event log is an evtx file. -func (l *winEventLogRaw) IsFile() bool { - return l.file -} - -func (l *winEventLogRaw) Open(state checkpoint.EventLogState) error { - l.lastRead = state - // we need to defer metrics initialization since when the event log - // is used from winlog input it would register it twice due to CheckConfig calls - if l.metrics == nil { - l.metrics = newInputMetrics(l.channelName, l.id) - } - - var err error - l.iterator, err = win.NewEventIterator( - win.WithSubscriptionFactory(func() (handle win.EvtHandle, err error) { - return l.open(l.lastRead) - }), - win.WithBatchSize(l.maxRead)) - return err -} - -func (l *winEventLogRaw) open(state checkpoint.EventLogState) (win.EvtHandle, error) { - var bookmark win.Bookmark - if len(state.Bookmark) > 0 { - var err error - bookmark, err = win.NewBookmarkFromXML(state.Bookmark) - if err != nil { - return win.NilHandle, err - } - defer bookmark.Close() - } - - if l.file { - return l.openFile(state, bookmark) - } - return l.openChannel(bookmark) -} - -func (l *winEventLogRaw) openFile(state checkpoint.EventLogState, bookmark win.Bookmark) (win.EvtHandle, error) { - path := l.channelName - - h, err := win.EvtQuery(0, path, l.query, win.EvtQueryFilePath|win.EvtQueryForwardDirection) - if err != nil { - return win.NilHandle, fmt.Errorf("failed to get handle to event log file %v: %w", path, err) - } - - if bookmark > 0 { - l.log.Debugf("Seeking to bookmark. timestamp=%v bookmark=%v", - state.Timestamp, state.Bookmark) - - // This seeks to the last read event and strictly validates that the - // bookmarked record number exists. - if err = win.EvtSeek(h, 0, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark|win.EvtSeekStrict); err == nil { - // Then we advance past the last read event to avoid sending that - // event again. This won't fail if we're at the end of the file. - if seekErr := win.EvtSeek(h, 1, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark); seekErr != nil { - err = fmt.Errorf("failed to seek past bookmarked position: %w", seekErr) - } - } else { - l.log.Warnf("s Failed to seek to bookmarked location in %v (error: %v). "+ - "Recovering by reading the log from the beginning. (Did the file "+ - "change since it was last read?)", path, err) - if seekErr := win.EvtSeek(h, 0, 0, win.EvtSeekRelativeToFirst); seekErr != nil { - err = fmt.Errorf("failed to seek to beginning of log: %w", seekErr) - } - } - - if err != nil { - return win.NilHandle, err - } - } - - return h, err -} - -func (l *winEventLogRaw) openChannel(bookmark win.Bookmark) (win.EvtHandle, error) { - // Using a pull subscription to receive events. See: - // https://msdn.microsoft.com/en-us/library/windows/desktop/aa385771(v=vs.85).aspx#pull - signalEvent, err := windows.CreateEvent(nil, 0, 0, nil) - if err != nil { - return win.NilHandle, err - } - defer windows.CloseHandle(signalEvent) //nolint:errcheck // This is just a resource release. - - var flags win.EvtSubscribeFlag - if bookmark > 0 { - flags = win.EvtSubscribeStartAfterBookmark - if !l.isForwarded() { - // Use EvtSubscribeStrict to detect when the bookmark is missing and be able to - // subscribe again from the beginning. - flags |= win.EvtSubscribeStrict - } - } else { - flags = win.EvtSubscribeStartAtOldestRecord - } - - l.log.Debugw("Using subscription query.", "winlog.query", l.query) - h, err := win.Subscribe( - 0, // Session - nil for localhost - signalEvent, - "", // Channel - empty b/c channel is in the query - l.query, // Query - nil means all events - win.EvtHandle(bookmark), // Bookmark - for resuming from a specific event - flags) - - switch err { //nolint:errorlint // This is an errno or nil. - case nil: - return h, nil - case win.ERROR_NOT_FOUND, win.ERROR_EVT_QUERY_RESULT_STALE, win.ERROR_EVT_QUERY_RESULT_INVALID_POSITION: - // The bookmarked event was not found, we retry the subscription from the start. - incrementMetric(readErrors, err) - return win.Subscribe(0, signalEvent, "", l.query, 0, win.EvtSubscribeStartAtOldestRecord) - default: - return 0, err - } -} - -func (l *winEventLogRaw) Read() ([]Record, error) { - //nolint:prealloc // Avoid unnecessary preallocation for each reader every second when event log is inactive. - var records []Record - defer func() { - l.metrics.log(records) - }() - - for h, ok := l.iterator.Next(); ok; h, ok = l.iterator.Next() { - record, err := l.processHandle(h) - if err != nil { - l.metrics.logError(err) - l.log.Warnw("Dropping event due to rendering error.", "error", err) - l.metrics.logDropped(err) - incrementMetric(dropReasons, err) - continue - } - records = append(records, *record) - - // It has read the maximum requested number of events. - if len(records) >= l.maxRead { - return records, nil - } - } - - // An error occurred while retrieving more events. - if err := l.iterator.Err(); err != nil { - l.metrics.logError(err) - return records, err - } - - // Reader is configured to stop when there are no more events. - if Stop == l.config.NoMoreEvents { - return records, io.EOF - } - - return records, nil -} - -func (l *winEventLogRaw) processHandle(h win.EvtHandle) (*Record, error) { - defer h.Close() - - // NOTE: Render can return an error and a partial event. - evt, xml, err := l.renderer.Render(h) - if evt == nil { - return nil, err - } - if err != nil { - evt.RenderErr = append(evt.RenderErr, err.Error()) - } - - r := &Record{ - API: winEventLogExpAPIName, - Event: *evt, - } - - if l.config.IncludeXML { - r.XML = xml - } - - if l.file { - r.File = l.id - } - - r.Offset = checkpoint.EventLogState{ - Name: l.id, - RecordNumber: r.RecordID, - Timestamp: r.TimeCreated.SystemTime, - } - if r.Offset.Bookmark, err = l.createBookmarkFromEvent(h); err != nil { - l.metrics.logError(err) - l.log.Warnw("Failed creating bookmark.", "error", err) - } - l.lastRead = r.Offset - return r, nil -} - -func (l *winEventLogRaw) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) { - bookmark, err := win.NewBookmarkFromEvent(evtHandle) - if err != nil { - return "", fmt.Errorf("failed to create new bookmark from event handle: %w", err) - } - defer bookmark.Close() - - return bookmark.XML() -} - -func (l *winEventLogRaw) Reset() error { - l.log.Debug("Closing event log reader handles for reset.") - return l.close() -} - -func (l *winEventLogRaw) Close() error { - l.log.Debug("Closing event log reader handles.") - l.metrics.close() - return l.close() -} - -func (l *winEventLogRaw) close() error { - if l.iterator == nil { - return l.renderer.Close() - } - return multierr.Combine( - l.iterator.Close(), - l.renderer.Close(), - ) -} diff --git a/winlogbeat/eventlog/wineventlog_test.go b/winlogbeat/eventlog/wineventlog_test.go index 9aafe31a258d..0290ce48feb4 100644 --- a/winlogbeat/eventlog/wineventlog_test.go +++ b/winlogbeat/eventlog/wineventlog_test.go @@ -58,98 +58,89 @@ const ( func TestWinEventLogConfig_Validate(t *testing.T) { tests := []struct { - In winEventLogConfig + In config WantErr bool Desc string }{ { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery, }, WantErr: false, Desc: "xml query: all good", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery[:len(customXMLQuery)-4], // Malformed XML by truncation. - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery[:len(customXMLQuery)-4], // Malformed XML by truncation. + }, WantErr: true, Desc: "xml query: malformed XML", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - XMLQuery: customXMLQuery, - }, + In: config{ + + XMLQuery: customXMLQuery, }, WantErr: true, Desc: "xml query: missing ID", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - Name: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + Name: "test", + XMLQuery: customXMLQuery, }, WantErr: true, Desc: "xml query: conflicting keys (xml query and name)", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery, SimpleQuery: query{IgnoreOlder: 1}, }, WantErr: true, Desc: "xml query: conflicting keys (xml query and ignore_older)", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery, SimpleQuery: query{Level: "error"}, }, WantErr: true, Desc: "xml query: conflicting keys (xml query and level)", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery, SimpleQuery: query{EventID: "1000"}, }, WantErr: true, Desc: "xml query: conflicting keys (xml query and event_id)", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{ - ID: "test", - XMLQuery: customXMLQuery, - }, + In: config{ + + ID: "test", + XMLQuery: customXMLQuery, SimpleQuery: query{Provider: []string{providerName}}, }, WantErr: true, Desc: "xml query: conflicting keys (xml query and provider)", }, { - In: winEventLogConfig{ - ConfigCommon: ConfigCommon{}, - }, + In: config{}, WantErr: true, Desc: "missing name", }, @@ -166,18 +157,14 @@ func TestWinEventLogConfig_Validate(t *testing.T) { } } -func TestWindowsEventLogAPI(t *testing.T) { - testWindowsEventLog(t, winEventLogAPIName, false) -} - func TestWindowsEventLogAPIRaw(t *testing.T) { // for the raw api using include xml behave differently than not // so we must test both settings - testWindowsEventLog(t, winEventLogRawAPIName, true) - testWindowsEventLog(t, winEventLogRawAPIName, false) + testWindowsEventLog(t, true) + testWindowsEventLog(t, false) } -func testWindowsEventLog(t *testing.T, api string, includeXML bool) { +func testWindowsEventLog(t *testing.T, includeXML bool) { writer, teardown := createLog(t) defer teardown() @@ -191,7 +178,7 @@ func testWindowsEventLog(t *testing.T, api string, includeXML bool) { } openLog := func(t testing.TB, config map[string]interface{}) EventLog { - return openLog(t, api, nil, config) + return openLog(t, nil, config) } t.Run("has_message", func(t *testing.T) { @@ -292,11 +279,6 @@ func testWindowsEventLog(t *testing.T, api string, includeXML bool) { records, err := log.Read() - // This implementation returns the EOF on the next call. - if err == nil && api == winEventLogAPIName { - _, err = log.Read() - } - if assert.Error(t, err, "no_more_events=stop requires io.EOF to be returned") { assert.Equal(t, io.EOF, err) } @@ -321,11 +303,6 @@ func testWindowsEventLog(t *testing.T, api string, includeXML bool) { records, err := log.Read() - // This implementation returns the EOF on the next call. - if err == nil && api == winEventLogAPIName { - _, err = log.Read() - } - if assert.Error(t, err, "no_more_events=stop requires io.EOF to be returned") { assert.Equal(t, io.EOF, err) } @@ -397,21 +374,13 @@ func setLogSize(t testing.TB, provider string, sizeBytes int) { } } -func openLog(t testing.TB, api string, state *checkpoint.EventLogState, config map[string]interface{}) EventLog { +func openLog(t testing.TB, state *checkpoint.EventLogState, config map[string]interface{}) EventLog { cfg, err := conf.NewConfigFrom(config) if err != nil { t.Fatal(err) } - var log EventLog - switch api { - case winEventLogAPIName: - log, err = newWinEventLog(cfg) - case winEventLogRawAPIName: - log, err = newWinEventLogRaw(cfg) - default: - t.Fatalf("Unknown API name: '%s'", api) - } + log, err := newWinEventLog(cfg) if err != nil { t.Fatal(err) }