winlogbeat/eventlog/wineventlog.go (297 lines of code) (raw):
// 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"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"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"
wininfo "github.com/elastic/go-sysinfo/providers/windows"
)
// winEventLog implements the EventLog interface for reading from the Windows
// Event Log API.
type winEventLog 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
}
// newWinEventLog creates and returns a new EventLog for reading event logs
// using the Windows Event Log.
func newWinEventLog(options *conf.C) (EventLog, error) {
var err error
c := config{BatchReadSize: 512}
if err := readConfig(options, &c); err != nil {
return nil, err
}
id := c.ID
if id == "" {
id = c.Name
}
l := &winEventLog{
config: c,
id: id,
channelName: c.Name,
maxRead: c.BatchReadSize,
log: logp.NewLogger("wineventlog").With("id", id),
}
if c.XMLQuery != "" {
if l.skipQueryFilters() {
l.log.Warn("you are using a custom XML query with Windows Server 2025 and forwarded events, " +
"this is not recommended due to a known issue with that can crash the Event Log service if using" +
" query filters. Please use a custom query without filters or use the default query")
}
l.query = c.XMLQuery
} else {
l.log = l.log.With("channel", c.Name)
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
}
l.file = true
queryLog = "file://" + path
}
winQuery := win.Query{
Log: queryLog,
}
if !l.skipQueryFilters() {
winQuery.IgnoreOlder = c.SimpleQuery.IgnoreOlder
winQuery.Level = c.SimpleQuery.Level
winQuery.EventID = c.SimpleQuery.EventID
winQuery.Provider = c.SimpleQuery.Provider
} else {
l.log.Warn("skipping query filters for Windows Server 2025 due to known issue" +
" with Event Log API and forwarded events")
}
l.query, err = winQuery.Build()
if err != nil {
return nil, err
}
}
switch c.IncludeXML {
case true:
l.renderer = win.NewXMLRenderer(
win.RenderConfig{
IsForwarded: l.isForwarded(),
Locale: c.EventLanguage,
},
win.NilHandle, l.log)
case false:
l.renderer, err = win.NewRenderer(
win.RenderConfig{
IsForwarded: l.isForwarded(),
Locale: c.EventLanguage,
},
win.NilHandle, l.log)
if err != nil {
return nil, err
}
}
return l, nil
}
func (l *winEventLog) 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 {
return l.id
}
// Channel returns the event log's channel name.
func (l *winEventLog) Channel() string {
return l.channelName
}
// IsFile returns true if the event log is an evtx file.
func (l *winEventLog) IsFile() bool {
return l.file
}
func (l *winEventLog) 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 *winEventLog) 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 *winEventLog) 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 *winEventLog) 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 *winEventLog) 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 *winEventLog) 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{
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 *winEventLog) 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 *winEventLog) Reset() error {
l.log.Debug("Closing event log reader handles for reset.")
return l.close()
}
func (l *winEventLog) Close() error {
l.log.Debug("Closing event log reader handles.")
l.metrics.close()
return l.close()
}
func (l *winEventLog) close() error {
if l.iterator == nil {
return l.renderer.Close()
}
return errors.Join(
l.iterator.Close(),
l.renderer.Close(),
)
}
// FIXME: Windows Server 2025 has a bug in the Windows Event Log API that causes
// the Event Log Service to crash when using some combinations of filters with
// forwarded events. This is a workaround to skip the query filters for
// Windows Server 2025 in such scenarios.
func (l *winEventLog) skipQueryFilters() bool {
if l.config.Bypass2025Workaround {
return false
}
osinfo, err := wininfo.OperatingSystem()
if err != nil {
l.log.Warnf("failed to get OS info: %v", err)
return false
}
return l.isForwarded() && strings.Contains(osinfo.Name, "2025")
}