diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 053b6a144c3..c10616bd3d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -112,6 +112,7 @@ CHANGELOG* /x-pack/filebeat/input/cel/ @elastic/security-service-integrations /x-pack/filebeat/input/cometd/ @elastic/obs-infraobs-integrations /x-pack/filebeat/input/entityanalytics/ @elastic/security-service-integrations +/x-pack/filebeat/input/etw/ @elastic/sec-windows-platform /x-pack/filebeat/input/gcppubsub/ @elastic/security-service-integrations /x-pack/filebeat/input/gcs/ @elastic/security-service-integrations /x-pack/filebeat/input/http_endpoint/ @elastic/security-service-integrations diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index f3740c4eb5b..73c3e12d5e2 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -179,6 +179,7 @@ Setting environmental variable ELASTIC_NETINFO:false in Elastic Agent pod will d - Prevent complete loss of long request trace data. {issue}37826[37826] {pull}37836[37836] - Added experimental version of the Websocket Input. {pull}37774[37774] - Add support for PEM-based Okta auth in CEL. {pull}37813[37813] +- Add ETW input. {pull}36915[36915] - Update CEL mito extensions to v1.9.0 to add keys/values helper. {pull}37971[37971] *Auditbeat* diff --git a/filebeat/docs/fields.asciidoc b/filebeat/docs/fields.asciidoc index 1c76bb70919..ddc887d246f 100644 --- a/filebeat/docs/fields.asciidoc +++ b/filebeat/docs/fields.asciidoc @@ -92,6 +92,7 @@ grouped in the following categories: * <> * <> * <> +* <> * <> * <> * <> @@ -158960,6 +158961,200 @@ alias to: source.geo.region_iso_code -- +[[exported-fields-winlog]] +== Windows ETW fields + +Fields from the ETW input (Event Tracing for Windows). + + + +[float] +=== winlog + +All fields specific to the Windows Event Tracing are defined here. + + + +*`winlog.activity_id`*:: ++ +-- +A globally unique identifier that identifies the current activity. The events that are published with this identifier are part of the same activity. + + +type: keyword + +required: False + +-- + +*`winlog.channel`*:: ++ +-- +Used to enable special event processing. Channel values below 16 are reserved for use by Microsoft to enable special treatment by the ETW runtime. Channel values 16 and above will be ignored by the ETW runtime (treated the same as channel 0) and can be given user-defined semantics. + + +type: keyword + +required: False + +-- + +*`winlog.event_data`*:: ++ +-- +The event-specific data. The content of this object is specific to any provider and event. + + +type: object + +required: False + +-- + +*`winlog.flags`*:: ++ +-- +Flags that provide information about the event such as the type of session it was logged to and if the event contains extended data. + + +type: keyword + +required: False + +-- + +*`winlog.keywords`*:: ++ +-- +The keywords are used to indicate an event's membership in a set of event categories. + + +type: keyword + +required: False + +-- + +*`winlog.level`*:: ++ +-- +Level of severity. Level values 0 through 5 are defined by Microsoft. Level values 6 through 15 are reserved. Level values 16 through 255 can be defined by the event provider. + + +type: keyword + +required: False + +-- + +*`winlog.opcode`*:: ++ +-- +The opcode defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. + + +type: keyword + +required: False + +-- + +*`winlog.process_id`*:: ++ +-- +Identifies the process that generated the event. + + +type: keyword + +required: False + +-- + +*`winlog.provider_guid`*:: ++ +-- +A globally unique identifier that identifies the provider that logged the event. + + +type: keyword + +required: False + +-- + +*`winlog.provider_name`*:: ++ +-- +The source of the event log record (the application or service that logged the record). + + +type: keyword + +required: False + +-- + +*`winlog.session`*:: ++ +-- +Configured session to forward ETW events from providers to consumers. + + +type: keyword + +required: False + +-- + +*`winlog.severity`*:: ++ +-- +Human-readable level of severity. + + +type: keyword + +required: False + +-- + +*`winlog.task`*:: ++ +-- +The task defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. + + +type: keyword + +required: False + +-- + +*`winlog.thread_id`*:: ++ +-- +Identifies the thread that generated the event. + + +type: keyword + +required: False + +-- + +*`winlog.version`*:: ++ +-- +Specify the version of a manifest-based event. + + +type: long + +required: False + +-- + [[exported-fields-zeek]] == Zeek fields diff --git a/filebeat/docs/filebeat-options.asciidoc b/filebeat/docs/filebeat-options.asciidoc index 0787e3660bf..1e9f9cac6e0 100644 --- a/filebeat/docs/filebeat-options.asciidoc +++ b/filebeat/docs/filebeat-options.asciidoc @@ -75,8 +75,10 @@ You can configure {beatname_uc} to use the following inputs: * <<{beatname_lc}-input-cometd>> * <<{beatname_lc}-input-container>> * <<{beatname_lc}-input-entity-analytics>> +* <<{beatname_lc}-input-etw>> * <<{beatname_lc}-input-filestream>> * <<{beatname_lc}-input-gcp-pubsub>> +* <<{beatname_lc}-input-gcs>> * <<{beatname_lc}-input-http_endpoint>> * <<{beatname_lc}-input-httpjson>> * <<{beatname_lc}-input-journald>> @@ -90,7 +92,6 @@ You can configure {beatname_uc} to use the following inputs: * <<{beatname_lc}-input-syslog>> * <<{beatname_lc}-input-tcp>> * <<{beatname_lc}-input-udp>> -* <<{beatname_lc}-input-gcs>> * <<{beatname_lc}-input-websocket>> include::multiline.asciidoc[] @@ -113,10 +114,14 @@ include::inputs/input-container.asciidoc[] include::../../x-pack/filebeat/docs/inputs/input-entity-analytics.asciidoc[] +include::../../x-pack/filebeat/docs/inputs/input-etw.asciidoc[] + include::inputs/input-filestream.asciidoc[] include::../../x-pack/filebeat/docs/inputs/input-gcp-pubsub.asciidoc[] +include::../../x-pack/filebeat/docs/inputs/input-gcs.asciidoc[] + include::../../x-pack/filebeat/docs/inputs/input-http-endpoint.asciidoc[] include::../../x-pack/filebeat/docs/inputs/input-httpjson.asciidoc[] @@ -145,6 +150,4 @@ include::inputs/input-udp.asciidoc[] include::inputs/input-unix.asciidoc[] -include::../../x-pack/filebeat/docs/inputs/input-gcs.asciidoc[] - include::../../x-pack/filebeat/docs/inputs/input-websocket.asciidoc[] diff --git a/x-pack/filebeat/_meta/config/filebeat.inputs.reference.xpack.yml.tmpl b/x-pack/filebeat/_meta/config/filebeat.inputs.reference.xpack.yml.tmpl index a35c0af5dc4..c5861174636 100644 --- a/x-pack/filebeat/_meta/config/filebeat.inputs.reference.xpack.yml.tmpl +++ b/x-pack/filebeat/_meta/config/filebeat.inputs.reference.xpack.yml.tmpl @@ -180,3 +180,44 @@ # This is used to shift collection start time and end time back in order to # collect logs when there is a delay in CloudWatch. #latency: 1m + +#------------------------------ ETW input -------------------------------- +# Beta: Config options for ETW (Event Trace for Windows) input (Only available for Windows) +#- type: etw + #enabled: false + #id: etw-dnsserver + + # Path to an .etl file to read from. + #file: "C:\Windows\System32\Winevt\Logs\Logfile.etl" + + # GUID of an ETW provider. + # Run 'logman query providers' to list the available providers. + #provider.guid: {EB79061A-A566-4698-9119-3ED2807060E7} + + # Name of an ETW provider. + # Run 'logman query providers' to list the available providers. + #provider.name: Microsoft-Windows-DNSServer + + # Tag to identify created sessions. + # If missing, its default value is the provider ID prefixed by 'Elastic-'. + #session_name: DNSServer-Analytical-Trace + + # Filter collected events with a level value that is less than or equal to this level. + # Allowed values are critical, error, warning, informational, and verbose. + #trace_level: verbose + + # 8-byte bitmask that enables the filtering of events from specific provider subcomponents. + # The provider will write a particular event if the event's keyword bits match any of the bits + # in this bitmask. + # Run 'logman query providers ""' to list available keywords. + #match_any_keyword: 0x8000000000000000 + + # 8-byte bitmask that enables the filtering of events from + # specific provider subcomponents. The provider will write a particular + # event if the event's keyword bits match all of the bits in this bitmask. + # Run 'logman query providers ""' to list available keywords. + #match_all_keyword: 0 + + # An existing session to read from. + # Run 'logman query -ets' to list existing sessions. + #session: UAL_Usermode_Provider diff --git a/x-pack/filebeat/docs/inputs/input-etw.asciidoc b/x-pack/filebeat/docs/inputs/input-etw.asciidoc new file mode 100644 index 00000000000..9ace3fdcc1b --- /dev/null +++ b/x-pack/filebeat/docs/inputs/input-etw.asciidoc @@ -0,0 +1,144 @@ +[role="xpack"] + +:type: etw + +[id="{beatname_lc}-input-{type}"] +=== ETW input + +++++ +ETW +++++ + +beta[] + +https://learn.microsoft.com/en-us/windows/win32/etw/event-tracing-portal[Event Tracing for Windows] is a powerful logging and tracing mechanism built into the Windows operating system. It provides a detailed view of application and system behavior, performance issues, and runtime diagnostics. Trace events contain an event header and provider-defined data that describes the current state of an application or operation. You can use the events to debug an application and perform capacity and performance analysis. + +The ETW input can interact with ETW in three distinct ways: it can create a new session to capture events from user-mode providers, attach to an already existing session to collect ongoing event data, or read events from a pre-recorded .etl file. This functionality enables the module to adapt to different scenarios, such as real-time event monitoring or analyzing historical data. + +This input currently supports manifest-based, MOF (classic) and TraceLogging providers while WPP providers are not supported. https://learn.microsoft.com/en-us/windows/win32/etw/about-event-tracing#types-of-providers[Here] you can find more information about the available types of providers. + +It has been tested in every Windows versions supported by Filebeat, starting from Windows 8.1 and Windows Server 2016. In addition, administrative privileges are required in order to control event tracing sessions. + +Example configurations: + +Read from a provider by name: +["source","yaml",subs="attributes"] +---- +{beatname_lc}.inputs: +- type: etw + id: etw-dnsserver + enabled: true + provider.name: Microsoft-Windows-DNSServer + session_name: DNSServer-Analytical + trace_level: verbose + match_any_keyword: 0x8000000000000000 + match_all_keyword: 0 +---- + +Same provider can be defined by its GUID: +["source","yaml",subs="attributes"] +---- +{beatname_lc}.inputs: +- type: etw + id: etw-dnsserver + enabled: true + provider.guid: {EB79061A-A566-4698-9119-3ED2807060E7} + session_name: DNSServer-Analytical + trace_level: verbose + match_any_keyword: 0x8000000000000000 + match_all_keyword: 0 +---- + +Read from a current session: +["source","yaml",subs="attributes"] +---- +{beatname_lc}.inputs: +- type: etw + enabled: true + id: etw-dnsserver-session + session: UAL_Usermode_Provider +---- + +Read from a .etl file: +["source","yaml",subs="attributes"] +---- +{beatname_lc}.inputs: +- type: etw + enabled: true + id: etw-dnsserver-session + file: "C\Windows\System32\Winevt\Logs\Logfile.etl" +---- + +NOTE: Examples shown above are mutually exclusive, since the options `provider.name`, `provider.guid`, `session` and `file` cannot be present at the same time. Nevertheless, it is a requirement that one of them appears. + +Multiple providers example: +["source","yaml",subs="attributes"] +---- +{beatname_lc}.inputs: +- type: etw + id: etw-dnsserver + enabled: true + provider.name: Microsoft-Windows-DNSServer + session_name: DNSServer-Analytical + trace_level: verbose + match_any_keyword: 0xfffffffffffffffff + match_all_keyword: 0 +- type: etw + id: etw-security + enabled: true + provider.name: Microsoft-Windows-Security-Auditing + session_name: Security-Auditing + trace_level: warning + match_any_keyword: 0xffffffffffffffff + match_all_keyword: 0 +---- + +==== Configuration options + +The `ETW` input supports the following configuration options. + +[float] +==== `file` + +Specifies the path to an .etl file for reading ETW events. This file format is commonly used for storing ETW event logs. + +[float] +==== `provider.guid` + +Identifies the GUID of an ETW provider. To see available providers, use the command `logman query providers`. + +[float] +==== `provider.name` + +Specifies the name of the ETW provider. Available providers can be listed using `logman query providers`. + +[float] +==== `session_name` + +When specified a provider, a new session is created. It sets the name for a new ETW session associated with the provider. If not provided, the default is the provider ID prefixed with 'Elastic-'. + +[float] +==== `trace_level` + +Defines the filtering level for events based on severity. Valid options include critical, error, warning, informational, and verbose. + +[float] +==== `match_any_keyword` + +An 8-byte bitmask used for filtering events from specific provider subcomponents based on keyword matching. Any matching keyword will enable the event to be written. Default value is `0xfffffffffffffffff` so it matches every available keyword. + +Run `logman query providers ""` to list the available keywords for a specific provider. + +[float] +==== `match_all_keyword` + +Similar to MatchAnyKeyword, this 8-byte bitmask filters events that match all specified keyword bits. Default value is `0` to let every event pass. + +Run `logman query providers ""` to list the available keywords for a specific provider. + +[float] +==== `session` + +Names an existing ETW session to read from. Existing sessions can be listed using `logman query -ets`. + +:type!: diff --git a/x-pack/filebeat/filebeat.reference.yml b/x-pack/filebeat/filebeat.reference.yml index 14308c2cce1..419278ce85d 100644 --- a/x-pack/filebeat/filebeat.reference.yml +++ b/x-pack/filebeat/filebeat.reference.yml @@ -3579,6 +3579,47 @@ filebeat.inputs: # collect logs when there is a delay in CloudWatch. #latency: 1m +#------------------------------ ETW input -------------------------------- +# Beta: Config options for ETW (Event Trace for Windows) input (Only available for Windows) +#- type: etw + #enabled: false + #id: etw-dnsserver + + # Path to an .etl file to read from. + #file: "C:\Windows\System32\Winevt\Logs\Logfile.etl" + + # GUID of an ETW provider. + # Run 'logman query providers' to list the available providers. + #provider.guid: {EB79061A-A566-4698-9119-3ED2807060E7} + + # Name of an ETW provider. + # Run 'logman query providers' to list the available providers. + #provider.name: Microsoft-Windows-DNSServer + + # Tag to identify created sessions. + # If missing, its default value is the provider ID prefixed by 'Elastic-'. + #session_name: DNSServer-Analytical-Trace + + # Filter collected events with a level value that is less than or equal to this level. + # Allowed values are critical, error, warning, informational, and verbose. + #trace_level: verbose + + # 8-byte bitmask that enables the filtering of events from specific provider subcomponents. + # The provider will write a particular event if the event's keyword bits match any of the bits + # in this bitmask. + # Run 'logman query providers ""' to list available keywords. + #match_any_keyword: 0x8000000000000000 + + # 8-byte bitmask that enables the filtering of events from + # specific provider subcomponents. The provider will write a particular + # event if the event's keyword bits match all of the bits in this bitmask. + # Run 'logman query providers ""' to list available keywords. + #match_all_keyword: 0 + + # An existing session to read from. + # Run 'logman query -ets' to list existing sessions. + #session: UAL_Usermode_Provider + # =========================== Filebeat autodiscover ============================ # Autodiscover allows you to detect changes in the system and spawn new modules diff --git a/x-pack/filebeat/include/list.go b/x-pack/filebeat/include/list.go index f7c35308ed3..43b6758766e 100644 --- a/x-pack/filebeat/include/list.go +++ b/x-pack/filebeat/include/list.go @@ -12,6 +12,7 @@ import ( _ "github.com/elastic/beats/v7/x-pack/filebeat/input/awss3" _ "github.com/elastic/beats/v7/x-pack/filebeat/input/azureeventhub" _ "github.com/elastic/beats/v7/x-pack/filebeat/input/cometd" + _ "github.com/elastic/beats/v7/x-pack/filebeat/input/etw" _ "github.com/elastic/beats/v7/x-pack/filebeat/input/gcppubsub" _ "github.com/elastic/beats/v7/x-pack/filebeat/input/lumberjack" _ "github.com/elastic/beats/v7/x-pack/filebeat/input/netflow" diff --git a/x-pack/filebeat/input/default-inputs/inputs_other.go b/x-pack/filebeat/input/default-inputs/inputs_other.go index d0725a23dd9..91d5917f261 100644 --- a/x-pack/filebeat/input/default-inputs/inputs_other.go +++ b/x-pack/filebeat/input/default-inputs/inputs_other.go @@ -2,7 +2,7 @@ // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. -//go:build !aix +//go:build !aix && !windows package inputs diff --git a/x-pack/filebeat/input/default-inputs/inputs_windows.go b/x-pack/filebeat/input/default-inputs/inputs_windows.go new file mode 100644 index 00000000000..361883f39ad --- /dev/null +++ b/x-pack/filebeat/input/default-inputs/inputs_windows.go @@ -0,0 +1,45 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build windows + +package inputs + +import ( + "github.com/elastic/beats/v7/filebeat/beater" + v2 "github.com/elastic/beats/v7/filebeat/input/v2" + "github.com/elastic/beats/v7/libbeat/beat" + "github.com/elastic/beats/v7/x-pack/filebeat/input/awscloudwatch" + "github.com/elastic/beats/v7/x-pack/filebeat/input/awss3" + "github.com/elastic/beats/v7/x-pack/filebeat/input/azureblobstorage" + "github.com/elastic/beats/v7/x-pack/filebeat/input/cel" + "github.com/elastic/beats/v7/x-pack/filebeat/input/cloudfoundry" + "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics" + "github.com/elastic/beats/v7/x-pack/filebeat/input/etw" + "github.com/elastic/beats/v7/x-pack/filebeat/input/gcs" + "github.com/elastic/beats/v7/x-pack/filebeat/input/http_endpoint" + "github.com/elastic/beats/v7/x-pack/filebeat/input/httpjson" + "github.com/elastic/beats/v7/x-pack/filebeat/input/lumberjack" + "github.com/elastic/beats/v7/x-pack/filebeat/input/o365audit" + "github.com/elastic/beats/v7/x-pack/filebeat/input/shipper" + "github.com/elastic/elastic-agent-libs/logp" +) + +func xpackInputs(info beat.Info, log *logp.Logger, store beater.StateStore) []v2.Plugin { + return []v2.Plugin{ + azureblobstorage.Plugin(log, store), + cel.Plugin(log, store), + cloudfoundry.Plugin(), + entityanalytics.Plugin(log), + gcs.Plugin(log, store), + http_endpoint.Plugin(), + httpjson.Plugin(log, store), + o365audit.Plugin(log, store), + awss3.Plugin(store), + awscloudwatch.Plugin(), + lumberjack.Plugin(), + shipper.Plugin(log, store), + etw.Plugin(), + } +} diff --git a/x-pack/filebeat/input/etw/_meta/fields.yml b/x-pack/filebeat/input/etw/_meta/fields.yml new file mode 100644 index 00000000000..9a732f73e3b --- /dev/null +++ b/x-pack/filebeat/input/etw/_meta/fields.yml @@ -0,0 +1,111 @@ +- key: winlog + title: "Windows ETW" + description: > + Fields from the ETW input (Event Tracing for Windows). + fields: + + - name: winlog + type: group + description: > + All fields specific to the Windows Event Tracing are defined here. + fields: + + - name: activity_id + type: keyword + required: false + description: > + A globally unique identifier that identifies the current activity. The + events that are published with this identifier are part of the same + activity. + + - name: channel + type: keyword + required: false + description: > + Used to enable special event processing. Channel values below 16 are reserved for use by Microsoft to enable special treatment by the ETW runtime. Channel values 16 and above will be ignored by the ETW runtime (treated the same as channel 0) and can be given user-defined semantics. + + - name: event_data + type: object + object_type: keyword + required: false + description: > + The event-specific data. The content of this object is specific to + any provider and event. + + - name: flags + type: keyword + required: false + description: > + Flags that provide information about the event such as the type of session it was logged to and if the event contains extended data. + + - name: keywords + type: keyword + required: false + description: > + The keywords are used to indicate an event's membership in a set of event categories. + + - name: level + type: keyword + required: false + description: > + Level of severity. Level values 0 through 5 are defined by Microsoft. Level values 6 through 15 are reserved. Level values 16 through 255 can be defined by the event provider. + + - name: opcode + type: keyword + required: false + description: > + The opcode defined in the event. Task and opcode are typically used to + identify the location in the application from where the event was + logged. + + - name: process_id + type: keyword + required: false + description: > + Identifies the process that generated the event. + + - name: provider_guid + type: keyword + required: false + description: > + A globally unique identifier that identifies the provider that logged + the event. + + - name: provider_name + type: keyword + required: false + description: > + The source of the event log record (the application or service that + logged the record). + + - name: session + type: keyword + required: false + description: > + Configured session to forward ETW events from providers to consumers. + + - name: severity + type: keyword + required: false + description: > + Human-readable level of severity. + + - name: task + type: keyword + required: false + description: > + The task defined in the event. Task and opcode are typically used to + identify the location in the application from where the event was + logged. + + - name: thread_id + type: keyword + required: false + description: > + Identifies the thread that generated the event. + + - name: version + type: long + required: false + description: > + Specify the version of a manifest-based event. diff --git a/x-pack/filebeat/input/etw/config.go b/x-pack/filebeat/input/etw/config.go new file mode 100644 index 00000000000..2f3925884f3 --- /dev/null +++ b/x-pack/filebeat/input/etw/config.go @@ -0,0 +1,114 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build windows + +package etw + +import ( + "fmt" + + "github.com/elastic/beats/v7/x-pack/libbeat/reader/etw" +) + +var validTraceLevel = map[string]bool{ + "critical": true, + "error": true, + "warning": true, + "information": true, + "verbose": true, +} + +type config struct { + // Logfile is the path to an .etl file to read from. + Logfile string `config:"file"` + // ProviderGUID is the GUID of an ETW provider. + // Run 'logman query providers' to list the available providers. + ProviderGUID string `config:"provider.guid"` + // ProviderName is the name of an ETW provider. + // Run 'logman query providers' to list the available providers. + ProviderName string `config:"provider.name"` + // SessionName is the name used to create a new session for the + // defined provider. If missing, its default value is the provider ID + // prefixed by 'Elastic-' + SessionName string `config:"session_name"` + // TraceLevel filters all provider events with a level value + // that is less than or equal to this level. + // Allowed values are critical, error, warning, informational, and verbose. + TraceLevel string `config:"trace_level"` + // MatchAnyKeyword is an 8-byte bitmask that enables the filtering of + // events from specific provider subcomponents. The provider will write + // a particular event if the event's keyword bits match any of the bits + // in this bitmask. + // See https://learn.microsoft.com/en-us/message-analyzer/system-etw-provider-event-keyword-level-settings for more details. + // Use logman query providers "" to list the available keywords. + MatchAnyKeyword uint64 `config:"match_any_keyword"` + // An 8-byte bitmask that enables the filtering of events from + // specific provider subcomponents. The provider will write a particular + // event if the event's keyword bits match all of the bits in this bitmask. + // See https://learn.microsoft.com/en-us/message-analyzer/system-etw-provider-event-keyword-level-settings for more details. + MatchAllKeyword uint64 `config:"match_all_keyword"` + // Session is the name of an existing session to read from. + // Run 'logman query -ets' to list existing sessions. + Session string `config:"session"` +} + +func convertConfig(cfg config) etw.Config { + return etw.Config{ + Logfile: cfg.Logfile, + ProviderGUID: cfg.ProviderGUID, + ProviderName: cfg.ProviderName, + SessionName: cfg.SessionName, + TraceLevel: cfg.TraceLevel, + MatchAnyKeyword: cfg.MatchAnyKeyword, + MatchAllKeyword: cfg.MatchAllKeyword, + Session: cfg.Session, + } +} + +func defaultConfig() config { + return config{ + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + } +} + +func (c *config) Validate() error { + if c.ProviderName == "" && c.ProviderGUID == "" && c.Logfile == "" && c.Session == "" { + return fmt.Errorf("provider, existing logfile or running session must be set") + } + + if !validTraceLevel[c.TraceLevel] { + return fmt.Errorf("invalid Trace Level value '%s'", c.TraceLevel) + } + + if c.ProviderGUID != "" { + if c.ProviderName != "" { + return fmt.Errorf("configuration constraint error: provider GUID and provider name cannot be defined together") + } + if c.Logfile != "" { + return fmt.Errorf("configuration constraint error: provider GUID and file cannot be defined together") + } + if c.Session != "" { + return fmt.Errorf("configuration constraint error: provider GUID and existing session cannot be defined together") + } + } + + if c.ProviderName != "" { + if c.Logfile != "" { + return fmt.Errorf("configuration constraint error: provider name and file cannot be defined together") + } + if c.Session != "" { + return fmt.Errorf("configuration constraint error: provider name and existing session cannot be defined together") + } + } + + if c.Logfile != "" { + if c.Session != "" { + return fmt.Errorf("configuration constraint error: file and existing session cannot be defined together") + } + } + + return nil +} diff --git a/x-pack/filebeat/input/etw/config_test.go b/x-pack/filebeat/input/etw/config_test.go new file mode 100644 index 00000000000..d18af17bd64 --- /dev/null +++ b/x-pack/filebeat/input/etw/config_test.go @@ -0,0 +1,140 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build windows + +package etw + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + confpkg "github.com/elastic/elastic-agent-libs/config" +) + +func Test_validateConfig(t *testing.T) { + testCases := []struct { + name string // Sub-test name. + config config // Load config parameters. + wantError string // Expected error + }{ + { + name: "valid config", + config: config{ + ProviderName: "Microsoft-Windows-DNSServer", + SessionName: "MySession-DNSServer", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + }, + { + name: "minimal config", + config: config{ + ProviderName: "Microsoft-Windows-DNSServer", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + }, + { + name: "missing source config", + config: config{ + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "provider, existing logfile or running session must be set", + }, + { + name: "invalid trace level", + config: config{ + ProviderName: "Microsoft-Windows-DNSServer", + TraceLevel: "failed", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "invalid Trace Level value 'failed'", + }, + { + name: "conflict provider GUID and name", + config: config{ + ProviderGUID: "{eb79061a-a566-4698-1234-3ed2807033a0}", + ProviderName: "Microsoft-Windows-DNSServer", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: provider GUID and provider name cannot be defined together", + }, + { + name: "conflict provider GUID and logfile", + config: config{ + ProviderGUID: "{eb79061a-a566-4698-1234-3ed2807033a0}", + Logfile: "C:\\Windows\\System32\\winevt\\File.etl", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: provider GUID and file cannot be defined together", + }, + { + name: "conflict provider GUID and session", + config: config{ + ProviderGUID: "{eb79061a-a566-4698-1234-3ed2807033a0}", + Session: "EventLog-Application", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: provider GUID and existing session cannot be defined together", + }, + { + name: "conflict provider name and logfile", + config: config{ + ProviderName: "Microsoft-Windows-DNSServer", + Logfile: "C:\\Windows\\System32\\winevt\\File.etl", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: provider name and file cannot be defined together", + }, + { + name: "conflict provider name and session", + config: config{ + ProviderName: "Microsoft-Windows-DNSServer", + Session: "EventLog-Application", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: provider name and existing session cannot be defined together", + }, + { + name: "conflict logfile and session", + config: config{ + Logfile: "C:\\Windows\\System32\\winevt\\File.etl", + Session: "EventLog-Application", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + }, + wantError: "configuration constraint error: file and existing session cannot be defined together", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := confpkg.MustNewConfigFrom(tc.config) + config := defaultConfig() + err := c.Unpack(&config) + + // Validate responses + if tc.wantError != "" { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tc.wantError) + } else { + t.Fatalf("Configuration validation failed. No returned error while expecting '%s'", tc.wantError) + } + } else { + if err != nil { + t.Fatalf("Configuration validation failed. No error expected but got '%v'", err) + } + } + }) + } +} diff --git a/x-pack/filebeat/input/etw/fields.go b/x-pack/filebeat/input/etw/fields.go new file mode 100644 index 00000000000..4ae281b363b --- /dev/null +++ b/x-pack/filebeat/input/etw/fields.go @@ -0,0 +1,23 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// Code generated by beats/dev-tools/cmd/asset/asset.go - DO NOT EDIT. + +package etw + +import ( + "github.com/elastic/beats/v7/libbeat/asset" +) + +func init() { + if err := asset.SetFields("filebeat", "etw", asset.ModuleFieldsPri, AssetEtw); err != nil { + panic(err) + } +} + +// AssetEtw returns asset data. +// This is the base64 encoded zlib format compressed contents of input/etw. +func AssetEtw() string { + return "eJzUVk2P2zYQvftXDHLp7sFGtsDm4EOBIkjQAu2pLnJcjMiRNF1qqJCUVP/7gh/ySmsFaNG4aHwzRb55b+bNkHt4pvMRJhZjmx1A4GDoCG8+sWg7efhw+vRmB6DJK8d9YCtH+GEHAPCRyWgPtbMdhJbiTmDphwB3H0aSACeHiqWB2joocPeHHUCdDh53CWUPgh0tCMRfOPd0hMbZoS8rG/Hj70djChz4nhTXrCDYROciYMUFHYGmmoU0tOToUKBWnJa8UAUeOZyfWF++zQSf6TxZt1x39HlgR/oINRpPiy9fEJBEQGNshcacYRD+PBCwJglcMzkILYaX/z5JU4NzUdTM7QCnllaQFEX7fDhK7ofKsG9Jw8ShhdCyXwZJW9AFsHUK4LFb410iXSdItShC5lbJ+d2TjiUlwcpQrjKaLBB6ZxV5z9Ic4H3mASOagTxUZOwED++SNkee3Eg6WXHwBNUZfmXlrLd12EAPjjB0MUJ1vnjbDRK4o6tIMYZowMqOBBMbAxUBN2Id6Y3zcJfQo6qSaUA/ZxHe3icwhRJRGh5JImG3n03rqUMJrPxGKVJSnjQGvKqGrf4gFRbLeeHpq9bq1FLmsL80YyST7AnKSogZTRZjXwgArzp3bTo5xwqPrKNFRWfsDd21wcbfyoAfI3jupEIGWGrrOoy7Y9mHkEqZLekH1caCxpXII+r10aNWgANM6MHYpsmmjqK4XpyOSUIWD/RnINGkcwKvJRdxN1MdKzbHSC00lD5k0awwEKBkzt956KiryPmWe2ABBE+pzEUSBmqsY9pyrKHxdqPjlwie8z+SS3MyL5XGfQuhdXZoWnhc3QvL4fDqyLvLkYfH1WR5te/hZeP3j49zOy8CvJR8dvhGdmyvrKZbljhHuBBjeSF2gBP652TRsivqDeeeVb6qsiFWkOVKyfKMVblFCir2veGylB4NU7yAF5mY0K/Qcp9s5KWM/RteyT+vL9wSME+BhoTcZYB/aSbNZX1qhv/Ry+EyTtPHnOAV4t+SJOvnwVd3pbeDUzQ/RrI5jG3AkbJOw91rN1kHsQtZUdK1YaIElI/fbygrA/pWmt5bqbkZXLq+81UQbHyLTOh0ehuU91pqiznNPm5SVvzQkdsanvNYuxXtn4YOZe8IdXocmatxek0poH++pTMi/rc2rUIbU/jfDasc75/MqpHcpv2NlebfkvstPe5yjkuc6CGEDoVr8mFfYaxN4fZXAAAA///9F1n8" +} diff --git a/x-pack/filebeat/input/etw/input.go b/x-pack/filebeat/input/etw/input.go new file mode 100644 index 00000000000..050fcf6ddf9 --- /dev/null +++ b/x-pack/filebeat/input/etw/input.go @@ -0,0 +1,281 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build windows + +package etw + +import ( + "fmt" + "math" + "strconv" + "sync" + "time" + + input "github.com/elastic/beats/v7/filebeat/input/v2" + stateless "github.com/elastic/beats/v7/filebeat/input/v2/input-stateless" + "github.com/elastic/beats/v7/libbeat/beat" + "github.com/elastic/beats/v7/libbeat/feature" + "github.com/elastic/beats/v7/x-pack/libbeat/reader/etw" + conf "github.com/elastic/elastic-agent-libs/config" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" + + "golang.org/x/sys/windows" +) + +const ( + inputName = "etw" +) + +// It abstracts the underlying operations needed to work with ETW, allowing for easier +// testing and decoupling from the Windows-specific ETW API. +type sessionOperator interface { + newSession(config config) (*etw.Session, error) + attachToExistingSession(session *etw.Session) error + createRealtimeSession(session *etw.Session) error + startConsumer(session *etw.Session) error + stopSession(session *etw.Session) error +} + +type realSessionOperator struct{} + +func (op *realSessionOperator) newSession(config config) (*etw.Session, error) { + return etw.NewSession(convertConfig(config)) +} + +func (op *realSessionOperator) attachToExistingSession(session *etw.Session) error { + return session.AttachToExistingSession() +} + +func (op *realSessionOperator) createRealtimeSession(session *etw.Session) error { + return session.CreateRealtimeSession() +} + +func (op *realSessionOperator) startConsumer(session *etw.Session) error { + return session.StartConsumer() +} + +func (op *realSessionOperator) stopSession(session *etw.Session) error { + return session.StopSession() +} + +// etwInput struct holds the configuration and state for the ETW input +type etwInput struct { + log *logp.Logger + config config + etwSession *etw.Session + operator sessionOperator +} + +func Plugin() input.Plugin { + return input.Plugin{ + Name: inputName, + Stability: feature.Beta, + Info: "Collect ETW logs.", + Manager: stateless.NewInputManager(configure), + } +} + +func configure(cfg *conf.C) (stateless.Input, error) { + conf := defaultConfig() + if err := cfg.Unpack(&conf); err != nil { + return nil, err + } + + return &etwInput{ + config: conf, + operator: &realSessionOperator{}, + }, nil +} + +func (e *etwInput) Name() string { return inputName } + +func (e *etwInput) Test(_ input.TestContext) error { + return nil +} + +// Run starts the ETW session and processes incoming events. +func (e *etwInput) Run(ctx input.Context, publisher stateless.Publisher) error { + var err error + + // Initialize a new ETW session with the provided configuration + e.etwSession, err = e.operator.newSession(e.config) + if err != nil { + return fmt.Errorf("error initializing ETW session: %w", err) + } + + // Set up logger with session information + e.log = ctx.Logger.With("session", e.etwSession.Name) + e.log.Info("Starting " + inputName + " input") + + // Handle realtime session creation or attachment + if e.etwSession.Realtime { + if !e.etwSession.NewSession { + // Attach to an existing session + err = e.operator.attachToExistingSession(e.etwSession) + if err != nil { + return fmt.Errorf("unable to retrieve handler: %w", err) + } + e.log.Debug("attached to existing session") + } else { + // Create a new realtime session + err = e.operator.createRealtimeSession(e.etwSession) + if err != nil { + return fmt.Errorf("realtime session could not be created: %w", err) + } + e.log.Debug("created session") + } + } + // Defer the cleanup and closing of resources + var wg sync.WaitGroup + var once sync.Once + + // Create an error channel to communicate errors from the goroutine + errChan := make(chan error, 1) + + defer func() { + once.Do(e.Close) + e.log.Info(inputName + " input stopped") + }() + + // eventReceivedCallback processes each ETW event + eventReceivedCallback := func(record *etw.EventRecord) uintptr { + if record == nil { + e.log.Error("received null event record") + return 1 + } + + e.log.Debugf("received event %d with length %d", record.EventHeader.EventDescriptor.Id, record.UserDataLength) + + data, err := etw.GetEventProperties(record) + if err != nil { + e.log.Errorw("failed to read event properties", "error", err) + return 1 + } + + evt := buildEvent(data, record.EventHeader, e.etwSession, e.config) + publisher.Publish(evt) + + return 0 + } + + // Set the callback function for the ETW session + e.etwSession.Callback = eventReceivedCallback + + // Start a goroutine to consume ETW events + wg.Add(1) + go func() { + defer wg.Done() + e.log.Debug("starting to listen ETW events") + if err = e.operator.startConsumer(e.etwSession); err != nil { + errChan <- fmt.Errorf("failed to start consumer: %w", err) // Send error to channel + return + } + e.log.Debug("stopped to read ETW events from session") + errChan <- nil + }() + + // We ensure resources are closed when receiving a cancellation signal + go func() { + <-ctx.Cancelation.Done() + once.Do(e.Close) + }() + + wg.Wait() // Ensure all goroutines have finished before closing + close(errChan) + if err, ok := <-errChan; ok && err != nil { + return err + } + + return nil +} + +var ( + // levelToSeverity maps ETW trace levels to names for use in ECS log.level. + levelToSeverity = map[uint8]string{ + 1: "critical", // Abnormal exit or termination events + 2: "error", // Severe error events + 3: "warning", // Warning events such as allocation failures + 4: "information", // Non-error events such as entry or exit events + 5: "verbose", // Detailed trace events + } + + // zeroGUID is the zero-value for a windows.GUID. + zeroGUID = windows.GUID{} +) + +// buildEvent builds the final beat.Event emitted by this input. +func buildEvent(data map[string]any, h etw.EventHeader, session *etw.Session, cfg config) beat.Event { + winlog := map[string]any{ + "activity_guid": h.ActivityId.String(), + "channel": strconv.FormatUint(uint64(h.EventDescriptor.Channel), 10), + "event_data": data, + "flags": strconv.FormatUint(uint64(h.Flags), 10), + "keywords": strconv.FormatUint(h.EventDescriptor.Keyword, 10), + "opcode": strconv.FormatUint(uint64(h.EventDescriptor.Opcode), 10), + "process_id": strconv.FormatUint(uint64(h.ProcessId), 10), + "provider_guid": h.ProviderId.String(), + "session": session.Name, + "task": strconv.FormatUint(uint64(h.EventDescriptor.Task), 10), + "thread_id": strconv.FormatUint(uint64(h.ThreadId), 10), + "version": h.EventDescriptor.Version, + } + // Fallback to the session GUID if there is no provider GUID. + if h.ProviderId == zeroGUID { + winlog["provider_guid"] = session.GUID.String() + } + + event := mapstr.M{ + "code": strconv.FormatUint(uint64(h.EventDescriptor.Id), 10), + "created": time.Now().UTC(), + "kind": "event", + "severity": h.EventDescriptor.Level, + } + if cfg.ProviderName != "" { + event["provider"] = cfg.ProviderName + } + + fields := mapstr.M{ + "event": event, + "winlog": winlog, + } + if level, found := levelToSeverity[h.EventDescriptor.Level]; found { + fields.Put("log.level", level) + } + if cfg.Logfile != "" { + fields.Put("log.file.path", cfg.Logfile) + } + + return beat.Event{ + Timestamp: convertFileTimeToGoTime(uint64(h.TimeStamp)), + Fields: fields, + } +} + +// convertFileTimeToGoTime converts a Windows FileTime to a Go time.Time structure. +func convertFileTimeToGoTime(fileTime64 uint64) time.Time { + // Define the offset between Windows epoch (1601) and Unix epoch (1970) + const epochDifference = 116444736000000000 + if fileTime64 < epochDifference { + // Time is before the Unix epoch, adjust accordingly + return time.Time{} + } + + fileTime := windows.Filetime{ + HighDateTime: uint32(fileTime64 >> 32), + LowDateTime: uint32(fileTime64 & math.MaxUint32), + } + + return time.Unix(0, fileTime.Nanoseconds()).UTC() +} + +// Close stops the ETW session and logs the outcome. +func (e *etwInput) Close() { + if err := e.operator.stopSession(e.etwSession); err != nil { + e.log.Error("failed to shutdown ETW session") + return + } + e.log.Info("successfully shutdown") +} diff --git a/x-pack/filebeat/input/etw/input_test.go b/x-pack/filebeat/input/etw/input_test.go new file mode 100644 index 00000000000..af1fa36d4bd --- /dev/null +++ b/x-pack/filebeat/input/etw/input_test.go @@ -0,0 +1,510 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build windows + +package etw + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + input "github.com/elastic/beats/v7/filebeat/input/v2" + "github.com/elastic/beats/v7/x-pack/libbeat/reader/etw" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" + + "golang.org/x/sys/windows" +) + +type mockSessionOperator struct { + // Fields to store function implementations that tests can customize + newSessionFunc func(config config) (*etw.Session, error) + attachToExistingSessionFunc func(session *etw.Session) error + createRealtimeSessionFunc func(session *etw.Session) error + startConsumerFunc func(session *etw.Session) error + stopSessionFunc func(session *etw.Session) error +} + +func (m *mockSessionOperator) newSession(config config) (*etw.Session, error) { + if m.newSessionFunc != nil { + return m.newSessionFunc(config) + } + return nil, nil +} + +func (m *mockSessionOperator) attachToExistingSession(session *etw.Session) error { + if m.attachToExistingSessionFunc != nil { + return m.attachToExistingSessionFunc(session) + } + return nil +} + +func (m *mockSessionOperator) createRealtimeSession(session *etw.Session) error { + if m.createRealtimeSessionFunc != nil { + return m.createRealtimeSessionFunc(session) + } + return nil +} + +func (m *mockSessionOperator) startConsumer(session *etw.Session) error { + if m.startConsumerFunc != nil { + return m.startConsumerFunc(session) + } + return nil +} + +func (m *mockSessionOperator) stopSession(session *etw.Session) error { + if m.stopSessionFunc != nil { + return m.stopSessionFunc(session) + } + return nil +} + +func Test_RunEtwInput_NewSessionError(t *testing.T) { + // Mocks + mockOperator := &mockSessionOperator{} + + // Setup the mock behavior for NewSession + mockOperator.newSessionFunc = func(config config) (*etw.Session, error) { + return nil, fmt.Errorf("failed creating session '%s'", config.SessionName) + } + + // Setup input + inputCtx := input.Context{ + Cancelation: nil, + Logger: logp.NewLogger("test"), + } + + etwInput := &etwInput{ + config: config{ + ProviderName: "Microsoft-Windows-Provider", + SessionName: "MySession", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + operator: mockOperator, + } + + // Run test + err := etwInput.Run(inputCtx, nil) + assert.EqualError(t, err, "error initializing ETW session: failed creating session 'MySession'") +} + +func Test_RunEtwInput_AttachToExistingSessionError(t *testing.T) { + // Mocks + mockOperator := &mockSessionOperator{} + + // Setup the mock behavior for NewSession + mockOperator.newSessionFunc = func(config config) (*etw.Session, error) { + mockSession := &etw.Session{ + Name: "MySession", + Realtime: true, + NewSession: false} + return mockSession, nil + } + // Setup the mock behavior for AttachToExistingSession + mockOperator.attachToExistingSessionFunc = func(session *etw.Session) error { + return fmt.Errorf("mock error") + } + + // Setup input + inputCtx := input.Context{ + Cancelation: nil, + Logger: logp.NewLogger("test"), + } + + etwInput := &etwInput{ + config: config{ + ProviderName: "Microsoft-Windows-Provider", + SessionName: "MySession", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + operator: mockOperator, + } + + // Run test + err := etwInput.Run(inputCtx, nil) + assert.EqualError(t, err, "unable to retrieve handler: mock error") +} + +func Test_RunEtwInput_CreateRealtimeSessionError(t *testing.T) { + // Mocks + mockOperator := &mockSessionOperator{} + + // Setup the mock behavior for NewSession + mockOperator.newSessionFunc = func(config config) (*etw.Session, error) { + mockSession := &etw.Session{ + Name: "MySession", + Realtime: true, + NewSession: true} + return mockSession, nil + } + // Setup the mock behavior for AttachToExistingSession + mockOperator.attachToExistingSessionFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for CreateRealtimeSession + mockOperator.createRealtimeSessionFunc = func(session *etw.Session) error { + return fmt.Errorf("mock error") + } + + // Setup input + inputCtx := input.Context{ + Cancelation: nil, + Logger: logp.NewLogger("test"), + } + + etwInput := &etwInput{ + config: config{ + ProviderName: "Microsoft-Windows-Provider", + SessionName: "MySession", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + operator: mockOperator, + } + + // Run test + err := etwInput.Run(inputCtx, nil) + assert.EqualError(t, err, "realtime session could not be created: mock error") +} + +func Test_RunEtwInput_StartConsumerError(t *testing.T) { + // Mocks + mockOperator := &mockSessionOperator{} + + // Setup the mock behavior for NewSession + mockOperator.newSessionFunc = func(config config) (*etw.Session, error) { + mockSession := &etw.Session{ + Name: "MySession", + Realtime: true, + NewSession: true} + return mockSession, nil + } + // Setup the mock behavior for AttachToExistingSession + mockOperator.attachToExistingSessionFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for CreateRealtimeSession + mockOperator.createRealtimeSessionFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for StartConsumer + mockOperator.startConsumerFunc = func(session *etw.Session) error { + return fmt.Errorf("mock error") + } + // Setup the mock behavior for StopSession + mockOperator.stopSessionFunc = func(session *etw.Session) error { + return nil + } + + // Setup cancellation + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + // Setup input + inputCtx := input.Context{ + Cancelation: ctx, + Logger: logp.NewLogger("test"), + } + + etwInput := &etwInput{ + config: config{ + ProviderName: "Microsoft-Windows-Provider", + SessionName: "MySession", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + operator: mockOperator, + } + + // Run test + err := etwInput.Run(inputCtx, nil) + assert.EqualError(t, err, "failed to start consumer: mock error") +} + +func Test_RunEtwInput_Success(t *testing.T) { + // Mocks + mockOperator := &mockSessionOperator{} + + // Setup the mock behavior for NewSession + mockOperator.newSessionFunc = func(config config) (*etw.Session, error) { + mockSession := &etw.Session{ + Name: "MySession", + Realtime: true, + NewSession: true} + return mockSession, nil + } + // Setup the mock behavior for AttachToExistingSession + mockOperator.attachToExistingSessionFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for CreateRealtimeSession + mockOperator.createRealtimeSessionFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for StartConsumer + mockOperator.startConsumerFunc = func(session *etw.Session) error { + return nil + } + // Setup the mock behavior for StopSession + mockOperator.stopSessionFunc = func(session *etw.Session) error { + return nil + } + + // Setup cancellation + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + // Setup input + inputCtx := input.Context{ + Cancelation: ctx, + Logger: logp.NewLogger("test"), + } + + etwInput := &etwInput{ + config: config{ + ProviderName: "Microsoft-Windows-Provider", + SessionName: "MySession", + TraceLevel: "verbose", + MatchAnyKeyword: 0xffffffffffffffff, + MatchAllKeyword: 0, + }, + operator: mockOperator, + } + + // Run test + go func() { + err := etwInput.Run(inputCtx, nil) + if err != nil { + t.Errorf("Run() error = %v, wantErr %v", err, false) + } + }() + + // Simulate waiting for a condition + time.Sleep(time.Millisecond * 100) + cancelFunc() // Trigger cancellation to test cleanup and goroutine exit +} + +func Test_buildEvent(t *testing.T) { + tests := []struct { + name string + data map[string]any + header etw.EventHeader + session *etw.Session + cfg config + expected mapstr.M + }{ + { + name: "TestStandardData", + data: map[string]any{ + "key": "value", + }, + header: etw.EventHeader{ + Size: 0, + HeaderType: 0, + Flags: 30, + EventProperty: 30, + ThreadId: 80, + ProcessId: 60, + TimeStamp: 133516441890350000, + ProviderId: windows.GUID{ + Data1: 0x12345678, + Data2: 0x1234, + Data3: 0x1234, + Data4: [8]byte{0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + EventDescriptor: etw.EventDescriptor{ + Id: 20, + Version: 90, + Channel: 10, + Level: 1, // Critical + Opcode: 50, + Task: 70, + Keyword: 40, + }, + Time: 0, + ActivityId: windows.GUID{ + Data1: 0x12345678, + Data2: 0x1234, + Data3: 0x1234, + Data4: [8]byte{0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + }, + session: &etw.Session{ + GUID: windows.GUID{ + Data1: 0x12345678, + Data2: 0x1234, + Data3: 0x1234, + Data4: [8]byte{0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + Name: "Elastic-TestProvider", + }, + cfg: config{ + ProviderName: "TestProvider", + }, + + expected: mapstr.M{ + "winlog": map[string]any{ + "activity_guid": "{12345678-1234-1234-1234-123456789ABC}", + "channel": "10", + "event_data": map[string]any{ + "key": "value", + }, + "flags": "30", + "keywords": "40", + "opcode": "50", + "process_id": "60", + "provider_guid": "{12345678-1234-1234-1234-123456789ABC}", + "session": "Elastic-TestProvider", + "task": "70", + "thread_id": "80", + "version": "90", + }, + "event.code": "20", + "event.provider": "TestProvider", + "event.severity": uint8(1), + "log.level": "critical", + }, + }, + { + // This case tests an unmapped severity, empty provider GUID and including logfile + name: "TestAlternativeMetadata", + data: map[string]any{ + "key": "value", + }, + header: etw.EventHeader{ + Size: 0, + HeaderType: 0, + Flags: 30, + EventProperty: 30, + ThreadId: 80, + ProcessId: 60, + TimeStamp: 133516441890350000, + EventDescriptor: etw.EventDescriptor{ + Id: 20, + Version: 90, + Channel: 10, + Level: 17, // Unknown + Opcode: 50, + Task: 70, + Keyword: 40, + }, + Time: 0, + ActivityId: windows.GUID{ + Data1: 0x12345678, + Data2: 0x1234, + Data3: 0x1234, + Data4: [8]byte{0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + }, + session: &etw.Session{ + GUID: windows.GUID{ + Data1: 0x12345678, + Data2: 0x1234, + Data3: 0x1234, + Data4: [8]byte{0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, + }, + Name: "Elastic-TestProvider", + }, + cfg: config{ + ProviderName: "TestProvider", + Logfile: "C:\\TestFile", + }, + + expected: mapstr.M{ + "winlog": map[string]any{ + "activity_guid": "{12345678-1234-1234-1234-123456789ABC}", + "channel": "10", + "event_data": map[string]any{ + "key": "value", + }, + "flags": "30", + "keywords": "40", + "opcode": "50", + "process_id": "60", + "provider_guid": "{12345678-1234-1234-1234-123456789ABC}", + "session": "Elastic-TestProvider", + "task": "70", + "thread_id": "80", + "version": "90", + }, + "event.code": "20", + "event.provider": "TestProvider", + "event.severity": uint8(17), + "log.file.path": "C:\\TestFile", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + evt := buildEvent(tt.data, tt.header, tt.session, tt.cfg) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["activity_guid"], evt.Fields["winlog"].(map[string]any)["activity_guid"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["channel"], evt.Fields["winlog"].(map[string]any)["channel"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["event_data"], evt.Fields["winlog"].(map[string]any)["event_data"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["flags"], evt.Fields["winlog"].(map[string]any)["flags"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["keywords"], evt.Fields["winlog"].(map[string]any)["keywords"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["opcode"], evt.Fields["winlog"].(map[string]any)["opcode"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["process_id"], evt.Fields["winlog"].(map[string]any)["process_id"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["provider_guid"], evt.Fields["winlog"].(map[string]any)["provider_guid"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["session"], evt.Fields["winlog"].(map[string]any)["session"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["task"], evt.Fields["winlog"].(map[string]any)["task"]) + assert.Equal(t, tt.expected["winlog"].(map[string]any)["thread_id"], evt.Fields["winlog"].(map[string]any)["thread_id"]) + mapEv := evt.Fields.Flatten() + + assert.Equal(t, tt.expected["winlog"].(map[string]any)["version"], strconv.Itoa(int(mapEv["winlog.version"].(uint8)))) + assert.Equal(t, tt.expected["event.code"], mapEv["event.code"]) + assert.Equal(t, tt.expected["event.provider"], mapEv["event.provider"]) + assert.Equal(t, tt.expected["event.severity"], mapEv["event.severity"]) + assert.Equal(t, tt.expected["log.file.path"], mapEv["log.file.path"]) + assert.Equal(t, tt.expected["log.level"], mapEv["log.level"]) + + }) + } +} + +func Test_convertFileTimeToGoTime(t *testing.T) { + tests := []struct { + name string + fileTime uint64 + want time.Time + }{ + { + name: "TestZeroValue", + fileTime: 0, + want: time.Time{}, + }, + { + name: "TestUnixEpoch", + fileTime: 116444736000000000, // January 1, 1970 (Unix epoch) + want: time.Unix(0, 0), + }, + { + name: "TestActualDate", + fileTime: 133515900000000000, // February 05, 2024, 7:00:00 AM + want: time.Date(2024, 02, 05, 7, 0, 0, 0, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertFileTimeToGoTime(tt.fileTime) + if !got.Equal(tt.want) { + t.Errorf("convertFileTimeToGoTime() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/x-pack/libbeat/reader/etw/provider.go b/x-pack/libbeat/reader/etw/provider.go index e0a20c3facd..63042d0f772 100644 --- a/x-pack/libbeat/reader/etw/provider.go +++ b/x-pack/libbeat/reader/etw/provider.go @@ -73,9 +73,3 @@ func guidFromProviderName(providerName string) (windows.GUID, error) { // No matching provider is found. return windows.GUID{}, fmt.Errorf("unable to find GUID from provider name") } - -// IsGUIDValid checks if GUID contains valid data -// (any of the fields in the GUID are non-zero) -func IsGUIDValid(guid windows.GUID) bool { - return guid.Data1 != 0 || guid.Data2 != 0 || guid.Data3 != 0 || guid.Data4 != [8]byte{} -} diff --git a/x-pack/libbeat/reader/etw/provider_test.go b/x-pack/libbeat/reader/etw/provider_test.go index d8c561ef3e4..0a10e0b495b 100644 --- a/x-pack/libbeat/reader/etw/provider_test.go +++ b/x-pack/libbeat/reader/etw/provider_test.go @@ -176,24 +176,3 @@ func TestGUIDFromProviderName_Success(t *testing.T) { assert.NoError(t, err) assert.Equal(t, mockGUID, guid, "GUID should match the mock GUID") } - -func TestIsGUIDValid_True(t *testing.T) { - // Valid GUID - validGUID := windows.GUID{ - Data1: 0xeb79061a, - Data2: 0xa566, - Data3: 0x4698, - Data4: [8]byte{0x12, 0x34, 0x3e, 0xd2, 0x80, 0x70, 0x33, 0xa0}, - } - - valid := IsGUIDValid(validGUID) - assert.True(t, valid, "IsGUIDValid should return true for a valid GUID") -} - -func TestIsGUIDValid_False(t *testing.T) { - // Invalid GUID (all zeros) - invalidGUID := windows.GUID{} - - valid := IsGUIDValid(invalidGUID) - assert.False(t, valid, "IsGUIDValid should return false for an invalid GUID") -} diff --git a/x-pack/libbeat/reader/etw/session.go b/x-pack/libbeat/reader/etw/session.go index 3a8e7be51d7..3216ff3af05 100644 --- a/x-pack/libbeat/reader/etw/session.go +++ b/x-pack/libbeat/reader/etw/session.go @@ -156,9 +156,8 @@ func newSessionProperties(sessionName string) *EventTraceProperties { } // NewSession initializes and returns a new ETW Session based on the provided configuration. -func NewSession(conf Config) (Session, error) { - var session Session - var err error +func NewSession(conf Config) (*Session, error) { + session := &Session{} // Assign ETW Windows API functions session.startTrace = _StartTrace @@ -183,9 +182,10 @@ func NewSession(conf Config) (Session, error) { session.NewSession = true // Indicate this is a new session + var err error session.GUID, err = setSessionGUIDFunc(conf) if err != nil { - return Session{}, err + return nil, fmt.Errorf("error when initializing session '%s': %w", session.Name, err) } // Initialize additional session properties. diff --git a/x-pack/libbeat/reader/etw/session_test.go b/x-pack/libbeat/reader/etw/session_test.go index 005b9839d5c..f79c9473f3e 100644 --- a/x-pack/libbeat/reader/etw/session_test.go +++ b/x-pack/libbeat/reader/etw/session_test.go @@ -209,9 +209,8 @@ func TestNewSession_GUIDError(t *testing.T) { } session, err := NewSession(conf) - assert.EqualError(t, err, "mock error") - expectedSession := Session{} - assert.Equal(t, expectedSession, session, "Session should be its zero value when an error occurs") + assert.EqualError(t, err, "error when initializing session 'Session1': mock error") + assert.Nil(t, session) }