From 35fb4a2a1480ccd20078970c375aaf66b0923529 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 7 Aug 2024 16:13:56 -0400 Subject: [PATCH] [receiver/windowseventlogreceiver] Add remote log collection support (#33601) **Description:** This PR adds remote log collection for the windows event log receiver. A config supports a single server configuration, multiple servers with multiple credentials configuration and multiple servers with single credentials configuration. **Link to tracking Issue:** #33100 **Testing:** Added relevant test to the test files that existed. This was tested on a single remote and with a valid / invalid remote to ensure successful collection of logs. Gather local logs was also tested to make sure old functionality is consistent too. **Documentation:** Updated Read me documentation --------- Co-authored-by: Daniel Jaglowski --- ...owseventlogreceiver-remote-collection.yaml | 28 ++++ pkg/stanza/operator/input/windows/api.go | 23 +++ .../operator/input/windows/config_all.go | 9 ++ .../operator/input/windows/config_windows.go | 13 +- pkg/stanza/operator/input/windows/input.go | 138 +++++++++++++++--- .../operator/input/windows/input_test.go | 129 ++++++++++++++++ pkg/stanza/operator/input/windows/raw.go | 1 + .../operator/input/windows/subscription.go | 20 ++- pkg/stanza/operator/input/windows/xml.go | 5 + pkg/stanza/operator/input/windows/xml_test.go | 123 ++++++++++++++++ receiver/windowseventlogreceiver/README.md | 22 +++ .../receiver_windows_test.go | 4 +- 12 files changed, 487 insertions(+), 28 deletions(-) create mode 100644 .chloggen/windowseventlogreceiver-remote-collection.yaml create mode 100644 pkg/stanza/operator/input/windows/input_test.go diff --git a/.chloggen/windowseventlogreceiver-remote-collection.yaml b/.chloggen/windowseventlogreceiver-remote-collection.yaml new file mode 100644 index 000000000000..5e2b217c60d3 --- /dev/null +++ b/.chloggen/windowseventlogreceiver-remote-collection.yaml @@ -0,0 +1,28 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: windowseventlogreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add remote collection support to Stanza operator windows pkg to support remote log collect for the Windows Event Log receiver. + + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [33100] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/pkg/stanza/operator/input/windows/api.go b/pkg/stanza/operator/input/windows/api.go index eb2ab3ec44ff..aa31e141907b 100644 --- a/pkg/stanza/operator/input/windows/api.go +++ b/pkg/stanza/operator/input/windows/api.go @@ -24,8 +24,17 @@ var ( updateBookmarkProc SyscallProc = api.NewProc("EvtUpdateBookmark") openPublisherMetadataProc SyscallProc = api.NewProc("EvtOpenPublisherMetadata") formatMessageProc SyscallProc = api.NewProc("EvtFormatMessage") + openSessionProc SyscallProc = api.NewProc("EvtOpenSession") ) +type EvtRpcLogin struct { + Server *uint16 + User *uint16 + Domain *uint16 + Password *uint16 + Flags uint32 +} + // SyscallProc is a syscall procedure. type SyscallProc interface { Call(...uintptr) (uintptr, uintptr, error) @@ -38,6 +47,8 @@ const ( EvtSubscribeStartAtOldestRecord uint32 = 2 // EvtSubscribeStartAfterBookmark is a flag that will subscribe to all events that begin after a bookmark. EvtSubscribeStartAfterBookmark uint32 = 3 + // EvtRpcLoginClass is a flag that indicates the login class. + EvtRpcLoginClass uint32 = 1 ) const ( @@ -65,6 +76,8 @@ const ( EvtRenderBookmark uint32 = 2 ) +var evtSubscribeFunc = evtSubscribe + // evtSubscribe is the direct syscall implementation of EvtSubscribe (https://docs.microsoft.com/en-us/windows/win32/api/winevt/nf-winevt-evtsubscribe) func evtSubscribe(session uintptr, signalEvent windows.Handle, channelPath *uint16, query *uint16, bookmark uintptr, context uintptr, callback uintptr, flags uint32) (uintptr, error) { handle, _, err := subscribeProc.Call(session, uintptr(signalEvent), uintptr(unsafe.Pointer(channelPath)), uintptr(unsafe.Pointer(query)), bookmark, context, callback, uintptr(flags)) @@ -147,3 +160,13 @@ func evtFormatMessage(publisherMetadata uintptr, event uintptr, messageID uint32 return bufferUsed, nil } + +// evtOpenSession is the direct syscall implementation of EvtOpenSession (https://learn.microsoft.com/en-us/windows/win32/api/winevt/nf-winevt-evtopensession) +func evtOpenSession(loginClass uint32, login *EvtRpcLogin, timeout uint32, flags uint32) (windows.Handle, error) { + r0, _, e1 := openSessionProc.Call(uintptr(loginClass), uintptr(unsafe.Pointer(login)), uintptr(timeout), uintptr(flags)) + handle := windows.Handle(r0) + if handle == 0 { + return handle, e1 + } + return handle, nil +} diff --git a/pkg/stanza/operator/input/windows/config_all.go b/pkg/stanza/operator/input/windows/config_all.go index 3fe9e71ee352..a40ade8b6870 100644 --- a/pkg/stanza/operator/input/windows/config_all.go +++ b/pkg/stanza/operator/input/windows/config_all.go @@ -35,4 +35,13 @@ type Config struct { PollInterval time.Duration `mapstructure:"poll_interval,omitempty"` Raw bool `mapstructure:"raw,omitempty"` ExcludeProviders []string `mapstructure:"exclude_providers,omitempty"` + Remote RemoteConfig `mapstructure:"remote,omitempty"` +} + +// RemoteConfig is the configuration for a remote server. +type RemoteConfig struct { + Server string `mapstructure:"server"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Domain string `mapstructure:"domain,omitempty"` } diff --git a/pkg/stanza/operator/input/windows/config_windows.go b/pkg/stanza/operator/input/windows/config_windows.go index eb1324bc7d07..8b33bac9c5e1 100644 --- a/pkg/stanza/operator/input/windows/config_windows.go +++ b/pkg/stanza/operator/input/windows/config_windows.go @@ -36,7 +36,12 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro return nil, fmt.Errorf("the `start_at` field must be set to `beginning` or `end`") } - return &Input{ + if (c.Remote.Server != "" || c.Remote.Username != "" || c.Remote.Password != "") && // any not empty + (c.Remote.Server == "" || c.Remote.Username == "" || c.Remote.Password == "") { // any empty + return nil, fmt.Errorf("remote configuration must have non-empty `username` and `password`") + } + + input := &Input{ InputOperator: inputOperator, buffer: NewBuffer(), channel: c.Channel, @@ -45,5 +50,9 @@ func (c *Config) Build(set component.TelemetrySettings) (operator.Operator, erro pollInterval: c.PollInterval, raw: c.Raw, excludeProviders: c.ExcludeProviders, - }, nil + remote: c.Remote, + } + input.startRemoteSession = input.defaultStartRemoteSession + + return input, nil } diff --git a/pkg/stanza/operator/input/windows/input.go b/pkg/stanza/operator/input/windows/input.go index 9196ad29e7a2..d935cc08bba4 100644 --- a/pkg/stanza/operator/input/windows/input.go +++ b/pkg/stanza/operator/input/windows/input.go @@ -7,11 +7,14 @@ package windows // import "github.com/open-telemetry/opentelemetry-collector-con import ( "context" + "errors" "fmt" "sync" "time" + "go.opentelemetry.io/collector/component" "go.uber.org/zap" + "golang.org/x/sys/windows" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/helper" @@ -20,19 +23,79 @@ import ( // Input is an operator that creates entries using the windows event log api. type Input struct { helper.InputOperator - bookmark Bookmark - subscription Subscription - buffer Buffer - channel string - maxReads int - startAt string - raw bool - excludeProviders []string - pollInterval time.Duration - persister operator.Persister - publisherCache publisherCache - cancel context.CancelFunc - wg sync.WaitGroup + bookmark Bookmark + buffer Buffer + channel string + maxReads int + startAt string + raw bool + excludeProviders []string + pollInterval time.Duration + persister operator.Persister + publisherCache publisherCache + cancel context.CancelFunc + wg sync.WaitGroup + subscription Subscription + remote RemoteConfig + remoteSessionHandle windows.Handle + startRemoteSession func() error +} + +// newInput creates a new Input operator. +func newInput(settings component.TelemetrySettings) *Input { + basicConfig := helper.NewBasicConfig("windowseventlog", "input") + basicOperator, _ := basicConfig.Build(settings) + + input := &Input{ + InputOperator: helper.InputOperator{ + WriterOperator: helper.WriterOperator{ + BasicOperator: basicOperator, + }, + }, + } + input.startRemoteSession = input.defaultStartRemoteSession + return input +} + +// defaultStartRemoteSession starts a remote session for reading event logs from a remote server. +func (i *Input) defaultStartRemoteSession() error { + if i.remote.Server == "" { + return nil + } + + login := EvtRpcLogin{ + Server: windows.StringToUTF16Ptr(i.remote.Server), + User: windows.StringToUTF16Ptr(i.remote.Username), + Password: windows.StringToUTF16Ptr(i.remote.Password), + } + + sessionHandle, err := evtOpenSession(EvtRpcLoginClass, &login, 0, 0) + if err != nil { + return fmt.Errorf("failed to open session for server %s: %w", i.remote.Server, err) + } + i.remoteSessionHandle = sessionHandle + return nil +} + +// stopRemoteSession stops the remote session if it is active. +func (i *Input) stopRemoteSession() error { + if i.remoteSessionHandle != 0 { + if err := evtClose(uintptr(i.remoteSessionHandle)); err != nil { + return fmt.Errorf("failed to close remote session handle for server %s: %w", i.remote.Server, err) + } + i.remoteSessionHandle = 0 + } + return nil +} + +// isRemote checks if the input is configured for remote access. +func (i *Input) isRemote() bool { + return i.remote.Server != "" +} + +// isNonTransientError checks if the error is likely non-transient. +func isNonTransientError(err error) bool { + return errors.Is(err, windows.ERROR_EVT_CHANNEL_NOT_FOUND) || errors.Is(err, windows.ERROR_ACCESS_DENIED) } // Start will start reading events from a subscription. @@ -42,10 +105,15 @@ func (i *Input) Start(persister operator.Persister) error { i.persister = persister + if i.isRemote() { + if err := i.startRemoteSession(); err != nil { + return fmt.Errorf("failed to start remote session for server %s: %w", i.remote.Server, err) + } + } + i.bookmark = NewBookmark() offsetXML, err := i.getBookmarkOffset(ctx) if err != nil { - i.Logger().Error("Failed to open bookmark, continuing without previous bookmark", zap.Error(err)) _ = i.persister.Delete(ctx, i.channel) } @@ -55,15 +123,31 @@ func (i *Input) Start(persister operator.Persister) error { } } - i.subscription = NewSubscription() - if err := i.subscription.Open(i.channel, i.startAt, i.bookmark); err != nil { - return fmt.Errorf("failed to open subscription: %w", err) + i.publisherCache = newPublisherCache() + + subscription := NewLocalSubscription() + if i.isRemote() { + subscription = NewRemoteSubscription(i.remote.Server) } - i.publisherCache = newPublisherCache() + if err := subscription.Open(i.startAt, uintptr(i.remoteSessionHandle), i.channel, i.bookmark); err != nil { + if isNonTransientError(err) { + if i.isRemote() { + return fmt.Errorf("failed to open subscription for remote server %s: %w", i.remote.Server, err) + } + return fmt.Errorf("failed to open local subscription: %w", err) + } + if i.isRemote() { + i.Logger().Warn("Transient error opening subscription for remote server, continuing", zap.String("server", i.remote.Server), zap.Error(err)) + } else { + i.Logger().Warn("Transient error opening local subscription, continuing", zap.Error(err)) + } + } + i.subscription = subscription i.wg.Add(1) go i.readOnInterval(ctx) + return nil } @@ -84,7 +168,7 @@ func (i *Input) Stop() error { return fmt.Errorf("failed to close publishers: %w", err) } - return nil + return i.stopRemoteSession() } // readOnInterval will read events with respect to the polling interval. @@ -112,6 +196,15 @@ func (i *Input) readToEnd(ctx context.Context) { return default: if count := i.read(ctx); count == 0 { + if i.isRemote() { + if err := i.startRemoteSession(); err != nil { + i.Logger().Error("Failed to re-establish remote session", zap.String("server", i.remote.Server), zap.Error(err)) + return + } + if err := i.subscription.Open(i.startAt, uintptr(i.remoteSessionHandle), i.channel, i.bookmark); err != nil { + i.Logger().Error("Failed to re-open subscription for remote server", zap.String("server", i.remote.Server), zap.Error(err)) + } + } return } } @@ -139,6 +232,8 @@ func (i *Input) read(ctx context.Context) int { // processEvent will process and send an event retrieved from windows event log. func (i *Input) processEvent(ctx context.Context, event Event) { + remoteServer := i.remote.Server + if i.raw { if len(i.excludeProviders) > 0 { simpleEvent, err := event.RenderSimple(i.buffer) @@ -159,6 +254,7 @@ func (i *Input) processEvent(ctx context.Context, event Event) { i.Logger().Error("Failed to render raw event", zap.Error(err)) return } + rawEvent.RemoteServer = remoteServer i.sendEventRaw(ctx, rawEvent) return } @@ -167,6 +263,7 @@ func (i *Input) processEvent(ctx context.Context, event Event) { i.Logger().Error("Failed to render simple event", zap.Error(err)) return } + simpleEvent.RemoteServer = remoteServer for _, excludeProvider := range i.excludeProviders { if simpleEvent.Provider.Name == excludeProvider { @@ -192,7 +289,7 @@ func (i *Input) processEvent(ctx context.Context, event Event) { i.sendEvent(ctx, simpleEvent) return } - + formattedEvent.RemoteServer = remoteServer i.sendEvent(ctx, formattedEvent) } @@ -210,6 +307,7 @@ func (i *Input) sendEvent(ctx context.Context, eventXML EventXML) { i.Write(ctx, entry) } +// sendEventRaw will send EventRaw as an entry to the operator's output. func (i *Input) sendEventRaw(ctx context.Context, eventRaw EventRaw) { body := eventRaw.parseBody() entry, err := i.NewEntry(body) diff --git a/pkg/stanza/operator/input/windows/input_test.go b/pkg/stanza/operator/input/windows/input_test.go new file mode 100644 index 000000000000..98ae98dac649 --- /dev/null +++ b/pkg/stanza/operator/input/windows/input_test.go @@ -0,0 +1,129 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows + +package windows // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/input/windows" + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/component" + "go.uber.org/zap" + "golang.org/x/sys/windows" + + "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/testutil" +) + +func newTestInput() *Input { + return newInput(component.TelemetrySettings{ + Logger: zap.NewNop(), + }) +} + +// TestInputStart_LocalSubscriptionError ensures the input correctly handles local subscription errors. +func TestInputStart_LocalSubscriptionError(t *testing.T) { + persister := testutil.NewMockPersister("") + + input := newTestInput() + input.channel = "test-channel" + input.startAt = "beginning" + input.pollInterval = 1 * time.Second + + err := input.Start(persister) + assert.Error(t, err) + assert.Contains(t, err.Error(), "The specified channel could not be found") +} + +// TestInputStart_RemoteSubscriptionError ensures the input correctly handles remote subscription errors. +func TestInputStart_RemoteSubscriptionError(t *testing.T) { + persister := testutil.NewMockPersister("") + + input := newTestInput() + input.startRemoteSession = func() error { return nil } + input.channel = "test-channel" + input.startAt = "beginning" + input.pollInterval = 1 * time.Second + input.remote = RemoteConfig{ + Server: "remote-server", + } + + err := input.Start(persister) + assert.Error(t, err) + assert.Contains(t, err.Error(), "The specified channel could not be found") +} + +// TestInputStart_RemoteSessionError ensures the input correctly handles remote session errors. +func TestInputStart_RemoteSessionError(t *testing.T) { + persister := testutil.NewMockPersister("") + + input := newTestInput() + input.startRemoteSession = func() error { + return errors.New("remote session error") + } + input.channel = "test-channel" + input.startAt = "beginning" + input.pollInterval = 1 * time.Second + input.remote = RemoteConfig{ + Server: "remote-server", + } + + err := input.Start(persister) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to start remote session for server remote-server: remote session error") +} + +// TestInputStart_RemoteAccessDeniedError ensures the input correctly handles remote access denied errors. +func TestInputStart_RemoteAccessDeniedError(t *testing.T) { + persister := testutil.NewMockPersister("") + + originalEvtSubscribeFunc := evtSubscribeFunc + defer func() { evtSubscribeFunc = originalEvtSubscribeFunc }() + + evtSubscribeFunc = func(session uintptr, signalEvent windows.Handle, channelPath *uint16, query *uint16, bookmark uintptr, context uintptr, callback uintptr, flags uint32) (uintptr, error) { + return 0, windows.ERROR_ACCESS_DENIED + } + + input := newTestInput() + input.startRemoteSession = func() error { return nil } + input.channel = "test-channel" + input.startAt = "beginning" + input.pollInterval = 1 * time.Second + input.remote = RemoteConfig{ + Server: "remote-server", + } + + err := input.Start(persister) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to open subscription for remote server") + assert.Contains(t, err.Error(), "Access is denied") +} + +// TestInputStart_BadChannelName ensures the input correctly handles bad channel names. +func TestInputStart_BadChannelName(t *testing.T) { + persister := testutil.NewMockPersister("") + + originalEvtSubscribeFunc := evtSubscribeFunc + defer func() { evtSubscribeFunc = originalEvtSubscribeFunc }() + + evtSubscribeFunc = func(session uintptr, signalEvent windows.Handle, channelPath *uint16, query *uint16, bookmark uintptr, context uintptr, callback uintptr, flags uint32) (uintptr, error) { + return 0, windows.ERROR_EVT_CHANNEL_NOT_FOUND + } + + input := newTestInput() + input.startRemoteSession = func() error { return nil } + input.channel = "bad-channel" + input.startAt = "beginning" + input.pollInterval = 1 * time.Second + input.remote = RemoteConfig{ + Server: "remote-server", + } + + err := input.Start(persister) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to open subscription for remote server") + assert.Contains(t, err.Error(), "The specified channel could not be found") +} diff --git a/pkg/stanza/operator/input/windows/raw.go b/pkg/stanza/operator/input/windows/raw.go index 7c94934c412b..7a489b4b1be0 100644 --- a/pkg/stanza/operator/input/windows/raw.go +++ b/pkg/stanza/operator/input/windows/raw.go @@ -17,6 +17,7 @@ type EventRaw struct { RenderedLevel string `xml:"RenderingInfo>Level"` Level string `xml:"System>Level"` Body string `xml:"-"` + RemoteServer string `xml:"RemoteServer,omitempty"` } // parseTimestamp will parse the timestamp of the event. diff --git a/pkg/stanza/operator/input/windows/subscription.go b/pkg/stanza/operator/input/windows/subscription.go index 77b6738c27d0..d2aba4555460 100644 --- a/pkg/stanza/operator/input/windows/subscription.go +++ b/pkg/stanza/operator/input/windows/subscription.go @@ -16,10 +16,13 @@ import ( // Subscription is a subscription to a windows eventlog channel. type Subscription struct { handle uintptr + Server string } // Open will open the subscription handle. -func (s *Subscription) Open(channel string, startAt string, bookmark Bookmark) error { +// It returns an error if the subscription handle is already open or if any step in the process fails. +// If the remote server is not reachable, it returns an error indicating the failure. +func (s *Subscription) Open(startAt string, sessionHandle uintptr, channel string, bookmark Bookmark) error { if s.handle != 0 { return fmt.Errorf("subscription handle is already open") } @@ -38,7 +41,7 @@ func (s *Subscription) Open(channel string, startAt string, bookmark Bookmark) e } flags := s.createFlags(startAt, bookmark) - subscriptionHandle, err := evtSubscribe(0, signalEvent, channelPtr, nil, bookmark.handle, 0, 0, flags) + subscriptionHandle, err := evtSubscribeFunc(sessionHandle, signalEvent, channelPtr, nil, bookmark.handle, 0, 0, flags) if err != nil { return fmt.Errorf("failed to subscribe to %s channel: %w", channel, err) } @@ -105,9 +108,18 @@ func (s *Subscription) createFlags(startAt string, bookmark Bookmark) uint32 { return EvtSubscribeToFutureEvents } -// NewSubscription will create a new subscription with an empty handle. -func NewSubscription() Subscription { +// NewRemoteSubscription will create a new remote subscription with an empty handle. +func NewRemoteSubscription(server string) Subscription { return Subscription{ + Server: server, + handle: 0, + } +} + +// NewLocalSubscription will create a new local subscription with an empty handle. +func NewLocalSubscription() Subscription { + return Subscription{ + Server: "", handle: 0, } } diff --git a/pkg/stanza/operator/input/windows/xml.go b/pkg/stanza/operator/input/windows/xml.go index 859d72a0b527..dcfe4199f35c 100644 --- a/pkg/stanza/operator/input/windows/xml.go +++ b/pkg/stanza/operator/input/windows/xml.go @@ -31,6 +31,7 @@ type EventXML struct { Security *Security `xml:"System>Security"` Execution *Execution `xml:"System>Execution"` EventData EventData `xml:"EventData"` + RemoteServer string `xml:"RemoteServer,omitempty"` } // parseTimestamp will parse the timestamp of the event. @@ -121,6 +122,10 @@ func (e *EventXML) parseBody() map[string]any { "event_data": parseEventData(e.EventData), } + if e.RemoteServer != "" { + body["remote_server"] = e.RemoteServer + } + if len(details) > 0 { body["details"] = details } diff --git a/pkg/stanza/operator/input/windows/xml_test.go b/pkg/stanza/operator/input/windows/xml_test.go index 734ccc881b69..622cc99eb053 100644 --- a/pkg/stanza/operator/input/windows/xml_test.go +++ b/pkg/stanza/operator/input/windows/xml_test.go @@ -558,3 +558,126 @@ func TestUnmarshalWithUserData(t *testing.T) { require.Equal(t, xml, event) } + +func TestParseBodyRemoteServer(t *testing.T) { + xml := EventXML{ + EventID: EventID{ + ID: 1, + Qualifiers: 2, + }, + Provider: Provider{ + Name: "provider", + GUID: "guid", + EventSourceName: "event source", + }, + TimeCreated: TimeCreated{ + SystemTime: "2020-07-30T01:01:01.123456789Z", + }, + Computer: "computer", + Channel: "application", + RecordID: 1, + Level: "Information", + Message: "message", + Task: "task", + Opcode: "opcode", + Keywords: []string{"keyword"}, + EventData: EventData{Data: []Data{{Name: "1st_name", Value: "value"}, {Name: "2nd_name", Value: "another_value"}}}, + RenderedLevel: "rendered_level", + RenderedTask: "rendered_task", + RenderedOpcode: "rendered_opcode", + RenderedKeywords: []string{"RenderedKeywords"}, + RemoteServer: "remote_server", + } + + expected := map[string]any{ + "event_id": map[string]any{ + "id": uint32(1), + "qualifiers": uint16(2), + }, + "provider": map[string]any{ + "name": "provider", + "guid": "guid", + "event_source": "event source", + }, + "system_time": "2020-07-30T01:01:01.123456789Z", + "computer": "computer", + "channel": "application", + "record_id": uint64(1), + "level": "rendered_level", + "message": "message", + "task": "rendered_task", + "opcode": "rendered_opcode", + "keywords": []string{"RenderedKeywords"}, + "event_data": map[string]any{ + "data": []any{ + map[string]any{"1st_name": "value"}, + map[string]any{"2nd_name": "another_value"}, + }, + }, + "remote_server": "remote_server", + } + + require.Equal(t, expected, xml.parseBody()) +} + +// Additional test cases to ensure comprehensive coverage + +func TestParseBodyNoRemoteServer(t *testing.T) { + xml := EventXML{ + EventID: EventID{ + ID: 1, + Qualifiers: 2, + }, + Provider: Provider{ + Name: "provider", + GUID: "guid", + EventSourceName: "event source", + }, + TimeCreated: TimeCreated{ + SystemTime: "2020-07-30T01:01:01.123456789Z", + }, + Computer: "computer", + Channel: "application", + RecordID: 1, + Level: "Information", + Message: "message", + Task: "task", + Opcode: "opcode", + Keywords: []string{"keyword"}, + EventData: EventData{Data: []Data{{Name: "1st_name", Value: "value"}, {Name: "2nd_name", Value: "another_value"}}}, + RenderedLevel: "rendered_level", + RenderedTask: "rendered_task", + RenderedOpcode: "rendered_opcode", + RenderedKeywords: []string{"RenderedKeywords"}, + RemoteServer: "", + } + + expected := map[string]any{ + "event_id": map[string]any{ + "id": uint32(1), + "qualifiers": uint16(2), + }, + "provider": map[string]any{ + "name": "provider", + "guid": "guid", + "event_source": "event source", + }, + "system_time": "2020-07-30T01:01:01.123456789Z", + "computer": "computer", + "channel": "application", + "record_id": uint64(1), + "level": "rendered_level", + "message": "message", + "task": "rendered_task", + "opcode": "rendered_opcode", + "keywords": []string{"RenderedKeywords"}, + "event_data": map[string]any{ + "data": []any{ + map[string]any{"1st_name": "value"}, + map[string]any{"2nd_name": "another_value"}, + }, + }, + } + + require.Equal(t, expected, xml.parseBody()) +} diff --git a/receiver/windowseventlogreceiver/README.md b/receiver/windowseventlogreceiver/README.md index 742ffdb290c8..83314ec4a4bf 100644 --- a/receiver/windowseventlogreceiver/README.md +++ b/receiver/windowseventlogreceiver/README.md @@ -33,6 +33,7 @@ Tails and parses logs from windows event log API using the [opentelemetry-log-co | `retry_on_failure.initial_interval` | `1 second` | Time to wait after the first failure before retrying. | | `retry_on_failure.max_interval` | `30 seconds` | Upper bound on retry backoff interval. Once this value is reached the delay between consecutive retries will remain constant at the specified value. | | `retry_on_failure.max_elapsed_time` | `5 minutes` | Maximum amount of time (including retries) spent trying to send a logs batch to a downstream consumer. Once this value is reached, the data is discarded. Retrying never stops if set to `0`. | +| remote | object | Remote configuration for connecting to a remote machine to collect logs. Includes server (the address of the remote server), with username, password, and optional domain. | ### Operators @@ -88,3 +89,24 @@ Output entry sample: } ``` +#### Remote Configuration + +If collection of the local event log is desired, a separate receiver needs to be created. + +**Requirements for Remote Configuration:** + +- The remote computer must enable the "Remote Event Log Management" Windows Firewall exception. Otherwise, when you try to use the session handle, the call will error with `RPC_S_SERVER_UNAVAILABLE`. +- The computer to which you are connecting must be running Windows Vista or later. + + +Single server configuration: +```yaml +receivers: + windowseventlog: + channel: application + remote: + server: "remote-server" + username: "user" + password: "password" + domain: "domain" +``` diff --git a/receiver/windowseventlogreceiver/receiver_windows_test.go b/receiver/windowseventlogreceiver/receiver_windows_test.go index bd1b9ff72896..2aa6bd008e2f 100644 --- a/receiver/windowseventlogreceiver/receiver_windows_test.go +++ b/receiver/windowseventlogreceiver/receiver_windows_test.go @@ -105,7 +105,7 @@ func TestReadWindowsEventLogger(t *testing.T) { } // logs sometimes take a while to be written, so a substantial wait buffer is needed - require.Eventually(t, logsReceived, 10*time.Second, 200*time.Millisecond) + require.Eventually(t, logsReceived, 20*time.Second, 200*time.Millisecond) results := sink.AllLogs() require.Len(t, results, 1) @@ -170,7 +170,7 @@ func TestReadWindowsEventLoggerRaw(t *testing.T) { } // logs sometimes take a while to be written, so a substantial wait buffer is needed - require.Eventually(t, logsReceived, 10*time.Second, 200*time.Millisecond) + require.Eventually(t, logsReceived, 20*time.Second, 200*time.Millisecond) results := sink.AllLogs() require.Len(t, results, 1)