Co-authored-by: Anuar Serdaliyev <serdaliyev.anuar@gmail.com>
This commit is contained in:
parent
382dac70c7
commit
57cd20a26c
|
|
@ -177,6 +177,7 @@ import (
|
|||
_ "github.com/influxdata/telegraf/plugins/inputs/varnish"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/vsphere"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/win_eventlog"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/win_services"
|
||||
_ "github.com/influxdata/telegraf/plugins/inputs/wireguard"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
# Windows Eventlog Input Plugin
|
||||
|
||||
## Collect Windows Event Log messages
|
||||
|
||||
Supports Windows Vista and higher.
|
||||
|
||||
Telegraf should have Administrator permissions to subscribe for some of the Windows Events Channels, like System Log.
|
||||
|
||||
### Configuration
|
||||
|
||||
```toml
|
||||
## Telegraf should have Administrator permissions to subscribe for some Windows Events channels
|
||||
## (System log, for example)
|
||||
|
||||
## LCID (Locale ID) for event rendering
|
||||
## 1033 to force English language
|
||||
## 0 to use default Windows locale
|
||||
# locale = 0
|
||||
|
||||
## Name of eventlog, used only if xpath_query is empty
|
||||
## Example: "Application"
|
||||
# eventlog_name = ""
|
||||
|
||||
## xpath_query can be in defined short form like "Event/System[EventID=999]"
|
||||
## or you can form a XML Query. Refer to the Consuming Events article:
|
||||
## https://docs.microsoft.com/en-us/windows/win32/wes/consuming-events
|
||||
## XML query is the recommended form, because it is most flexible
|
||||
## You can create or debug XML Query by creating Custom View in Windows Event Viewer
|
||||
## and then copying resulting XML here
|
||||
xpath_query = '''
|
||||
<QueryList>
|
||||
<Query Id="0" Path="Security">
|
||||
<Select Path="Security">*</Select>
|
||||
<Suppress Path="Security">*[System[( (EventID >= 5152 and EventID <= 5158) or EventID=5379 or EventID=4672)]]</Suppress>
|
||||
</Query>
|
||||
<Query Id="1" Path="Application">
|
||||
<Select Path="Application">*[System[(Level < 4)]]</Select>
|
||||
</Query>
|
||||
<Query Id="2" Path="Windows PowerShell">
|
||||
<Select Path="Windows PowerShell">*[System[(Level < 4)]]</Select>
|
||||
</Query>
|
||||
<Query Id="3" Path="System">
|
||||
<Select Path="System">*</Select>
|
||||
</Query>
|
||||
<Query Id="4" Path="Setup">
|
||||
<Select Path="Setup">*</Select>
|
||||
</Query>
|
||||
</QueryList>
|
||||
'''
|
||||
|
||||
## System field names:
|
||||
## "Source", "EventID", "Version", "Level", "Task", "Opcode", "Keywords", "TimeCreated",
|
||||
## "EventRecordID", "ActivityID", "RelatedActivityID", "ProcessID", "ThreadID", "ProcessName",
|
||||
## "Channel", "Computer", "UserID", "UserName", "Message", "LevelText", "TaskText", "OpcodeText"
|
||||
|
||||
## In addition to System, Data fields can be unrolled from additional XML nodes in event.
|
||||
## Human-readable representation of those nodes is formatted into event Message field,
|
||||
## but XML is more machine-parsable
|
||||
|
||||
# Process UserData XML to fields, if this node exists in Event XML
|
||||
process_userdata = true
|
||||
|
||||
# Process EventData XML to fields, if this node exists in Event XML
|
||||
process_eventdata = true
|
||||
|
||||
## Separator character to use for unrolled XML Data field names
|
||||
separator = "_"
|
||||
|
||||
## Get only first line of Message field. For most events first line is usually more than enough
|
||||
only_first_line_of_message = true
|
||||
|
||||
## Fields to include as tags. Globbing supported ("Level*" for both "Level" and "LevelText")
|
||||
event_tags = ["Source", "EventID", "Level", "LevelText", "Task", "TaskText", "Opcode", "OpcodeText", "Keywords", "Channel", "Computer"]
|
||||
|
||||
## Default list of fields to send. All fields are sent by default. Globbing supported
|
||||
event_fields = ["*"]
|
||||
|
||||
## Fields to exclude. Also applied to data fields. Globbing supported
|
||||
exclude_fields = ["Binary", "Data_Address*"]
|
||||
|
||||
## Skip those tags or fields if their value is empty or equals to zero. Globbing supported
|
||||
exclude_empty = ["*ActivityID", "UserID"]
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
There are three types of filtering: **Event Log** name, **XPath Query** and **XML Query**.
|
||||
|
||||
**Event Log** name filtering is simple:
|
||||
|
||||
```toml
|
||||
eventlog_name = "Application"
|
||||
xpath_query = '''
|
||||
```
|
||||
|
||||
For **XPath Query** filtering set the `xpath_query` value, and `eventlog_name` will be ignored:
|
||||
|
||||
```toml
|
||||
eventlog_name = ""
|
||||
xpath_query = "Event/System[EventID=999]"
|
||||
```
|
||||
|
||||
**XML Query** is the most flexible: you can Select or Suppress any values, and give ranges for other values. XML query is the recommended form, because it is most flexible. You can create or debug XML Query by creating Custom View in Windows Event Viewer and then copying resulting XML in config file.
|
||||
|
||||
XML Query documentation:
|
||||
|
||||
<https://docs.microsoft.com/en-us/windows/win32/wes/consuming-events>
|
||||
|
||||
### Metrics
|
||||
|
||||
You can send any field, *System*, *Computed* or *XML* as tag field. List of those fields is in the `event_tags` config array. Globbing is supported in this array, i.e. `Level*` for all fields beginning with `Level`, or `L?vel` for all fields where the name is `Level`, `L3vel`, `L@vel` and so on. Tag fields are converted to strings automatically.
|
||||
|
||||
By default, all other fields are sent, but you can limit that either by listing it in `event_fields` config array with globbing, or by adding some field name masks in the `exclude_fields` config array.
|
||||
|
||||
You can limit sending fields with empty values by adding masks of names of such fields in the `exclude_empty` config array. Value considered empty, if the System field of type `int` or `uint32` is equal to zero, or if any field of type `string` is an empty string.
|
||||
|
||||
List of System fields:
|
||||
|
||||
- Source (string)
|
||||
- EventID (int)
|
||||
- Version (int)
|
||||
- Level (int)
|
||||
- LevelText (string)
|
||||
- Opcode (int)
|
||||
- OpcodeText (string)
|
||||
- Task (int)
|
||||
- TaskText (string)
|
||||
- Keywords (string): comma-separated in case of multiple values
|
||||
- TimeCreated (string)
|
||||
- EventRecordID (string)
|
||||
- ActivityID (string)
|
||||
- RelatedActivityID (string)
|
||||
- ProcessID (int)
|
||||
- ThreadID (int)
|
||||
- ProcessName (string): derived from ProcessID
|
||||
- Channel (string)
|
||||
- Computer (string): useful if consumed from Forwarded Events
|
||||
- UserID (string): SID
|
||||
- UserName (string): derived from UserID, presented in form of DOMAIN\Username
|
||||
- Message (string)
|
||||
|
||||
### Computed fields
|
||||
|
||||
Fields `Level`, `Opcode` and `Task` are converted to text and saved as computed `*Text` fields.
|
||||
|
||||
`Keywords` field is converted from hex uint64 value by the `_EvtFormatMessage` WINAPI function. There can be more than one value, in that case they will be comma-separated. If keywords can't be converted (bad device driver or forwarded from another computer with unknown Event Channel), hex uint64 is saved as is.
|
||||
|
||||
`ProcessName` field is found by looking up ProcessID. Can be empty if telegraf doesn't have enough permissions.
|
||||
|
||||
`Username` field is found by looking up SID from UserID.
|
||||
|
||||
`Message` field is rendered from the event data, and can be several kilobytes of text with line breaks. For most events the first line of this text is more then enough, and additional info is more useful to be parsed as XML fields. So, for brevity, plugin takes only the first line. You can set `only_first_line_of_message` parameter to `false` to take full message text.
|
||||
|
||||
### Additional Fields
|
||||
|
||||
The content of **Event Data** and **User Data** XML Nodes can be added as additional fields, and is added by default. You can disable that by setting `process_userdata` or `process_eventdata` parameters to `false`.
|
||||
|
||||
For the fields from additional XML Nodes the `Name` attribute is taken as the name, and inner text is the value. Type of those fields is always string.
|
||||
|
||||
Name of the field is formed from XML Path by adding _ inbetween levels. For example, if UserData XML looks like this:
|
||||
|
||||
```xml
|
||||
<UserData>
|
||||
<CbsPackageChangeState xmlns="http://manifests.microsoft.com/win/2004/08/windows/setup_provider">
|
||||
<PackageIdentifier>KB4566782</PackageIdentifier>
|
||||
<IntendedPackageState>5112</IntendedPackageState>
|
||||
<IntendedPackageStateTextized>Installed</IntendedPackageStateTextized>
|
||||
<ErrorCode>0x0</ErrorCode>
|
||||
<Client>UpdateAgentLCU</Client>
|
||||
</CbsPackageChangeState>
|
||||
</UserData>
|
||||
```
|
||||
|
||||
It will be converted to following fields:
|
||||
|
||||
```text
|
||||
CbsPackageChangeState_PackageIdentifier = "KB4566782"
|
||||
CbsPackageChangeState_IntendedPackageState = "5112"
|
||||
CbsPackageChangeState_IntendedPackageStateTextized = "Installed"
|
||||
CbsPackageChangeState_ErrorCode = "0x0"
|
||||
CbsPackageChangeState_Client = "UpdateAgentLCU"
|
||||
```
|
||||
|
||||
If there are more than one field with the same name, all those fields are given suffix with number: `_1`, `_2` and so on.
|
||||
|
||||
### Localization
|
||||
|
||||
Human readable Event Description is in the Message field. But it is better to be skipped in favour of the Event XML values, because they are more machine-readable.
|
||||
|
||||
Keywords, LevelText, TaskText, OpcodeText and Message are saved with the current Windows locale by default. You can override this, for example, to English locale by setting `locale` config parameter to `1033`. Unfortunately, **Event Data** and **User Data** XML Nodes are in default Windows locale only.
|
||||
|
||||
Locale should be present on the computer. English locale is usually available on all localized versions of modern Windows. List of locales:
|
||||
|
||||
<https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a>
|
||||
|
||||
### Example Output
|
||||
|
||||
Some values are changed for anonymity.
|
||||
|
||||
```text
|
||||
win_eventlog,Channel=System,Computer=PC,EventID=105,Keywords=0x8000000000000000,Level=4,LevelText=Information,Opcode=10,OpcodeText=General,Source=WudfUsbccidDriver,Task=1,TaskText=Driver,host=PC ProcessName="WUDFHost.exe",UserName="NT AUTHORITY\\LOCAL SERVICE",Data_dwMaxCCIDMessageLength="271",Data_bPINSupport="0x0",Data_bMaxCCIDBusySlots="1",EventRecordID=1914688i,UserID="S-1-5-19",Version=0i,Data_bClassGetEnvelope="0x0",Data_wLcdLayout="0x0",Data_bClassGetResponse="0x0",TimeCreated="2020-08-21T08:43:26.7481077Z",Message="The Smartcard reader reported the following class descriptor (part 2)." 1597999410000000000
|
||||
|
||||
win_eventlog,Channel=Security,Computer=PC,EventID=4798,Keywords=Audit\ Success,Level=0,LevelText=Information,Opcode=0,OpcodeText=Info,Source=Microsoft-Windows-Security-Auditing,Task=13824,TaskText=User\ Account\ Management,host=PC Data_TargetDomainName="PC",Data_SubjectUserName="User",Data_CallerProcessId="0x3d5c",Data_SubjectLogonId="0x46d14f8d",Version=0i,EventRecordID=223157i,Message="A user's local group membership was enumerated.",Data_TargetUserName="User",Data_TargetSid="S-1-5-21-.-.-.-1001",Data_SubjectUserSid="S-1-5-21-.-.-.-1001",Data_CallerProcessName="C:\\Windows\\explorer.exe",ActivityID="{0d4cc11d-7099-0002-4dc1-4c0d9970d601}",UserID="",Data_SubjectDomainName="PC",TimeCreated="2020-08-21T08:43:27.3036771Z",ProcessName="lsass.exe" 1597999410000000000
|
||||
|
||||
win_eventlog,Channel=Microsoft-Windows-Dhcp-Client/Admin,Computer=PC,EventID=1002,Keywords=0x4000000000000001,Level=2,LevelText=Error,Opcode=76,OpcodeText=IpLeaseDenied,Source=Microsoft-Windows-Dhcp-Client,Task=3,TaskText=Address\ Configuration\ State\ Event,host=PC Version=0i,Message="The IP address lease 10.20.30.40 for the Network Card with network address 0xaabbccddeeff has been denied by the DHCP server 10.20.30.1 (The DHCP Server sent a DHCPNACK message).",UserID="S-1-5-19",Data_HWLength="6",Data_HWAddress="545595B7EA01",TimeCreated="2020-08-21T08:43:42.8265853Z",EventRecordID=34i,ProcessName="svchost.exe",UserName="NT AUTHORITY\\LOCAL SERVICE" 1597999430000000000
|
||||
|
||||
win_eventlog,Channel=System,Computer=PC,EventID=10016,Keywords=Classic,Level=3,LevelText=Warning,Opcode=0,OpcodeText=Info,Source=Microsoft-Windows-DistributedCOM,Task=0,host=PC Data_param3="Активация",Data_param6="PC",Data_param8="S-1-5-21-2007059868-50816014-3139024325-1001",Version=0i,UserName="PC\\User",Data_param1="по умолчанию для компьютера",Data_param2="Локально",Data_param7="User",Data_param9="LocalHost (с использованием LRPC)",Data_param10="Microsoft.Windows.ShellExperienceHost_10.0.19041.423_neutral_neutral_cw5n1h2txyewy",ActivityID="{839cac9e-73a1-4559-a847-62f3a5e73e44}",ProcessName="svchost.exe",Message="The по умолчанию для компьютера permission settings do not grant Локально Активация permission for the COM Server application with CLSID ",Data_param5="{316CDED5-E4AE-4B15-9113-7055D84DCC97}",Data_param11="S-1-15-2-.-.-.-.-.-.-2861478708",TimeCreated="2020-08-21T08:43:45.5233759Z",EventRecordID=1914689i,UserID="S-1-5-21-.-.-.-1001",Data_param4="{C2F03A33-21F5-47FA-B4BB-156362A2F239}" 1597999430000000000
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
// Event is the event entry representation
|
||||
// Only the most common elements are processed, human-readable data is rendered in Message
|
||||
// More info on schema, if there will be need to add more:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/wes/eventschema-elements
|
||||
type Event struct {
|
||||
Source Provider `xml:"System>Provider"`
|
||||
EventID int `xml:"System>EventID"`
|
||||
Version int `xml:"System>Version"`
|
||||
Level int `xml:"System>Level"`
|
||||
Task int `xml:"System>Task"`
|
||||
Opcode int `xml:"System>Opcode"`
|
||||
Keywords string `xml:"System>Keywords"`
|
||||
TimeCreated TimeCreated `xml:"System>TimeCreated"`
|
||||
EventRecordID int `xml:"System>EventRecordID"`
|
||||
Correlation Correlation `xml:"System>Correlation"`
|
||||
Execution Execution `xml:"System>Execution"`
|
||||
Channel string `xml:"System>Channel"`
|
||||
Computer string `xml:"System>Computer"`
|
||||
Security Security `xml:"System>Security"`
|
||||
UserData UserData `xml:"UserData"`
|
||||
EventData EventData `xml:"EventData"`
|
||||
Message string
|
||||
LevelText string
|
||||
TaskText string
|
||||
OpcodeText string
|
||||
}
|
||||
|
||||
// UserData Application-provided XML data
|
||||
type UserData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// EventData Application-provided XML data
|
||||
type EventData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// Provider is the Event provider information
|
||||
type Provider struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
}
|
||||
|
||||
// Correlation is used for the event grouping
|
||||
type Correlation struct {
|
||||
ActivityID string `xml:"ActivityID,attr"`
|
||||
RelatedActivityID string `xml:"RelatedActivityID,attr"`
|
||||
}
|
||||
|
||||
// Execution Info for Event
|
||||
type Execution struct {
|
||||
ProcessID uint32 `xml:"ProcessID,attr"`
|
||||
ThreadID uint32 `xml:"ThreadID,attr"`
|
||||
ProcessName string
|
||||
}
|
||||
|
||||
// Security Data for Event
|
||||
type Security struct {
|
||||
UserID string `xml:"UserID,attr"`
|
||||
}
|
||||
|
||||
// TimeCreated field for Event
|
||||
type TimeCreated struct {
|
||||
SystemTime string `xml:"SystemTime,attr"`
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import "syscall"
|
||||
|
||||
// Event log error codes.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
ERROR_INSUFFICIENT_BUFFER syscall.Errno = 122
|
||||
ERROR_NO_MORE_ITEMS syscall.Errno = 259
|
||||
ERROR_INVALID_OPERATION syscall.Errno = 4317
|
||||
//revive:enable:var-naming
|
||||
)
|
||||
|
||||
// EvtSubscribeFlag defines the possible values that specify when to start subscribing to events.
|
||||
type EvtSubscribeFlag uint32
|
||||
|
||||
// EVT_SUBSCRIBE_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385588(v=vs.85).aspx
|
||||
const (
|
||||
EvtSubscribeToFutureEvents EvtSubscribeFlag = 1
|
||||
)
|
||||
|
||||
// EvtRenderFlag uint32
|
||||
type EvtRenderFlag uint32
|
||||
|
||||
// EVT_RENDER_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385563(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
// Render the event as an XML string. For details on the contents of the
|
||||
// XML string, see the Event schema.
|
||||
EvtRenderEventXml EvtRenderFlag = 1
|
||||
//revive:enable:var-naming
|
||||
)
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// DecodeUTF16 to UTF8 bytes
|
||||
func DecodeUTF16(b []byte) ([]byte, error) {
|
||||
|
||||
if len(b)%2 != 0 {
|
||||
return nil, fmt.Errorf("must have even length byte slice")
|
||||
}
|
||||
|
||||
u16s := make([]uint16, 1)
|
||||
|
||||
ret := &bytes.Buffer{}
|
||||
|
||||
b8buf := make([]byte, 4)
|
||||
|
||||
lb := len(b)
|
||||
for i := 0; i < lb; i += 2 {
|
||||
u16s[0] = uint16(b[i]) + (uint16(b[i+1]) << 8)
|
||||
r := utf16.Decode(u16s)
|
||||
n := utf8.EncodeRune(b8buf, r[0])
|
||||
ret.Write(b8buf[:n])
|
||||
}
|
||||
|
||||
return ret.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetFromSnapProcess finds information about process by the given pid
|
||||
// Returns process parent pid, threads info handle and process name
|
||||
func GetFromSnapProcess(pid uint32) (uint32, uint32, string, error) {
|
||||
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, uint32(pid))
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
defer windows.CloseHandle(snap)
|
||||
var pe32 windows.ProcessEntry32
|
||||
pe32.Size = uint32(unsafe.Sizeof(pe32))
|
||||
if err = windows.Process32First(snap, &pe32); err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
for {
|
||||
if pe32.ProcessID == uint32(pid) {
|
||||
szexe := windows.UTF16ToString(pe32.ExeFile[:])
|
||||
return uint32(pe32.ParentProcessID), uint32(pe32.Threads), szexe, nil
|
||||
}
|
||||
if err = windows.Process32Next(snap, &pe32); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0, 0, "", fmt.Errorf("couldn't find pid: %d", pid)
|
||||
}
|
||||
|
||||
type xmlnode struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content []byte `xml:",innerxml"`
|
||||
Text string `xml:",chardata"`
|
||||
Nodes []xmlnode `xml:",any"`
|
||||
}
|
||||
|
||||
// EventField for unique rendering
|
||||
type EventField struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// UnmarshalXML redefined for xml elements walk
|
||||
func (n *xmlnode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
n.Attrs = start.Attr
|
||||
type node xmlnode
|
||||
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// UnrollXMLFields extracts fields from xml data
|
||||
func UnrollXMLFields(data []byte, fieldsUsage map[string]int, separator string) ([]EventField, map[string]int) {
|
||||
buf := bytes.NewBuffer(data)
|
||||
dec := xml.NewDecoder(buf)
|
||||
var fields []EventField
|
||||
for {
|
||||
var node xmlnode
|
||||
err := dec.Decode(&node)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// log.Fatal(err)
|
||||
break
|
||||
}
|
||||
var parents []string
|
||||
walkXML([]xmlnode{node}, parents, separator, func(node xmlnode, parents []string, separator string) bool {
|
||||
innerText := strings.TrimSpace(node.Text)
|
||||
if len(innerText) > 0 {
|
||||
valueName := strings.Join(parents, separator)
|
||||
fieldsUsage[valueName]++
|
||||
field := EventField{Name: valueName, Value: innerText}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return fields, fieldsUsage
|
||||
}
|
||||
|
||||
func walkXML(nodes []xmlnode, parents []string, separator string, f func(xmlnode, []string, string) bool) {
|
||||
for _, node := range nodes {
|
||||
parentName := node.XMLName.Local
|
||||
for _, attr := range node.Attrs {
|
||||
attrName := strings.ToLower(attr.Name.Local)
|
||||
if attrName == "name" {
|
||||
// Add Name attribute to parent name
|
||||
parentName = strings.Join([]string{parentName, attr.Value}, separator)
|
||||
}
|
||||
}
|
||||
nodeParents := append(parents, parentName)
|
||||
if f(node, nodeParents, separator) {
|
||||
walkXML(node.Nodes, nodeParents, separator, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UniqueFieldNames forms unique field names
|
||||
// by adding _<num> if there are several of them
|
||||
func UniqueFieldNames(fields []EventField, fieldsUsage map[string]int, separator string) []EventField {
|
||||
var fieldsCounter = map[string]int{}
|
||||
var fieldsUnique []EventField
|
||||
for _, field := range fields {
|
||||
fieldName := field.Name
|
||||
if fieldsUsage[field.Name] > 1 {
|
||||
fieldsCounter[field.Name]++
|
||||
fieldName = fmt.Sprint(field.Name, separator, fieldsCounter[field.Name])
|
||||
}
|
||||
fieldsUnique = append(fieldsUnique, EventField{
|
||||
Name: fieldName,
|
||||
Value: field.Value,
|
||||
})
|
||||
}
|
||||
return fieldsUnique
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func TestDecodeUTF16(t *testing.T) {
|
||||
testString := "Test String"
|
||||
utf16s := utf16.Encode([]rune(testString))
|
||||
var bytesUtf16 bytes.Buffer
|
||||
writer := io.Writer(&bytesUtf16)
|
||||
lb := len(utf16s)
|
||||
for i := 0; i < lb; i++ {
|
||||
word := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(word, utf16s[i])
|
||||
_, err := writer.Write(word)
|
||||
if err != nil {
|
||||
t.Errorf("error preparing UTF-16 test string")
|
||||
return
|
||||
}
|
||||
}
|
||||
type args struct {
|
||||
b []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Wrong UTF-16",
|
||||
args: args{b: append(bytesUtf16.Bytes(), byte('\x00'))},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "UTF-16",
|
||||
args: args{b: bytesUtf16.Bytes()},
|
||||
want: []byte(testString),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := DecodeUTF16(tt.args.b)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DecodeUTF16() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DecodeUTF16() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var xmlbroken = `
|
||||
<BrokenXML>
|
||||
<Data/>qq</Data>
|
||||
</BrokenXML>
|
||||
`
|
||||
|
||||
var xmldata = `
|
||||
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
|
||||
<UserData>
|
||||
<CbsPackageChangeState xmlns="http://manifests.microsoft.com/win/2004/08/windows/setup_provider">
|
||||
<IntendedPackageState>5111</IntendedPackageState>
|
||||
<ErrorCode><Code>0x0</Code></ErrorCode>
|
||||
</CbsPackageChangeState>
|
||||
</UserData>
|
||||
<EventData>
|
||||
<Data>2120-07-26T15:24:25Z</Data>
|
||||
<Data>RulesEngine</Data>
|
||||
<Data Name="Engine">RulesEngine</Data>
|
||||
</EventData>
|
||||
</Event>
|
||||
`
|
||||
|
||||
type testEvent struct {
|
||||
UserData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
} `xml:"UserData"`
|
||||
EventData struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
} `xml:"EventData"`
|
||||
}
|
||||
|
||||
func TestUnrollXMLFields(t *testing.T) {
|
||||
container := testEvent{}
|
||||
err := xml.Unmarshal([]byte(xmldata), &container)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't unmarshal precooked xml string xmldata")
|
||||
return
|
||||
}
|
||||
|
||||
type args struct {
|
||||
data []byte
|
||||
fieldsUsage map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want1 []EventField
|
||||
want2 map[string]int
|
||||
}{
|
||||
{
|
||||
name: "Broken XML",
|
||||
args: args{
|
||||
data: []byte(xmlbroken),
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: nil,
|
||||
want2: map[string]int{},
|
||||
},
|
||||
{
|
||||
name: "EventData with non-unique names and one Name attr",
|
||||
args: args{
|
||||
data: container.EventData.InnerXML,
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: []EventField{
|
||||
{Name: "Data", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data", Value: "RulesEngine"},
|
||||
{Name: "Data_Engine", Value: "RulesEngine"},
|
||||
},
|
||||
want2: map[string]int{"Data": 2, "Data_Engine": 1},
|
||||
},
|
||||
{
|
||||
name: "UserData with non-unique names and three levels of depth",
|
||||
args: args{
|
||||
data: container.UserData.InnerXML,
|
||||
fieldsUsage: map[string]int{},
|
||||
},
|
||||
want1: []EventField{
|
||||
{Name: "CbsPackageChangeState_IntendedPackageState", Value: "5111"},
|
||||
{Name: "CbsPackageChangeState_ErrorCode_Code", Value: "0x0"},
|
||||
},
|
||||
want2: map[string]int{
|
||||
"CbsPackageChangeState_ErrorCode_Code": 1,
|
||||
"CbsPackageChangeState_IntendedPackageState": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := UnrollXMLFields(tt.args.data, tt.args.fieldsUsage, "_")
|
||||
if !reflect.DeepEqual(got, tt.want1) {
|
||||
t.Errorf("ExtractFields() got = %v, want %v", got, tt.want1)
|
||||
}
|
||||
if !reflect.DeepEqual(got1, tt.want2) {
|
||||
t.Errorf("ExtractFields() got1 = %v, want %v", got1, tt.want2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueFieldNames(t *testing.T) {
|
||||
type args struct {
|
||||
fields []EventField
|
||||
fieldsUsage map[string]int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []EventField
|
||||
}{
|
||||
{
|
||||
name: "Unique values",
|
||||
args: args{
|
||||
fields: []EventField{
|
||||
{Name: "Data", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data", Value: "RulesEngine"},
|
||||
{Name: "Engine", Value: "RulesEngine"},
|
||||
},
|
||||
fieldsUsage: map[string]int{"Data": 2, "Engine": 1},
|
||||
},
|
||||
want: []EventField{
|
||||
{Name: "Data_1", Value: "2120-07-26T15:24:25Z"},
|
||||
{Name: "Data_2", Value: "RulesEngine"},
|
||||
{Name: "Engine", Value: "RulesEngine"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := UniqueFieldNames(tt.args.fields, tt.args.fieldsUsage, "_"); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("PrintFields() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var sampleConfig = `
|
||||
## Telegraf should have Administrator permissions to subscribe for some Windows Events channels
|
||||
## (System log, for example)
|
||||
|
||||
## LCID (Locale ID) for event rendering
|
||||
## 1033 to force English language
|
||||
## 0 to use default Windows locale
|
||||
# locale = 0
|
||||
|
||||
## Name of eventlog, used only if xpath_query is empty
|
||||
## Example: "Application"
|
||||
# eventlog_name = ""
|
||||
|
||||
## xpath_query can be in defined short form like "Event/System[EventID=999]"
|
||||
## or you can form a XML Query. Refer to the Consuming Events article:
|
||||
## https://docs.microsoft.com/en-us/windows/win32/wes/consuming-events
|
||||
## XML query is the recommended form, because it is most flexible
|
||||
## You can create or debug XML Query by creating Custom View in Windows Event Viewer
|
||||
## and then copying resulting XML here
|
||||
xpath_query = '''
|
||||
<QueryList>
|
||||
<Query Id="0" Path="Security">
|
||||
<Select Path="Security">*</Select>
|
||||
<Suppress Path="Security">*[System[( (EventID >= 5152 and EventID <= 5158) or EventID=5379 or EventID=4672)]]</Suppress>
|
||||
</Query>
|
||||
<Query Id="1" Path="Application">
|
||||
<Select Path="Application">*[System[(Level < 4)]]</Select>
|
||||
</Query>
|
||||
<Query Id="2" Path="Windows PowerShell">
|
||||
<Select Path="Windows PowerShell">*[System[(Level < 4)]]</Select>
|
||||
</Query>
|
||||
<Query Id="3" Path="System">
|
||||
<Select Path="System">*</Select>
|
||||
</Query>
|
||||
<Query Id="4" Path="Setup">
|
||||
<Select Path="Setup">*</Select>
|
||||
</Query>
|
||||
</QueryList>
|
||||
'''
|
||||
|
||||
## System field names:
|
||||
## "Source", "EventID", "Version", "Level", "Task", "Opcode", "Keywords", "TimeCreated",
|
||||
## "EventRecordID", "ActivityID", "RelatedActivityID", "ProcessID", "ThreadID", "ProcessName",
|
||||
## "Channel", "Computer", "UserID", "UserName", "Message", "LevelText", "TaskText", "OpcodeText"
|
||||
|
||||
## In addition to System, Data fields can be unrolled from additional XML nodes in event.
|
||||
## Human-readable representation of those nodes is formatted into event Message field,
|
||||
## but XML is more machine-parsable
|
||||
|
||||
# Process UserData XML to fields, if this node exists in Event XML
|
||||
process_userdata = true
|
||||
|
||||
# Process EventData XML to fields, if this node exists in Event XML
|
||||
process_eventdata = true
|
||||
|
||||
## Separator character to use for unrolled XML Data field names
|
||||
separator = "_"
|
||||
|
||||
## Get only first line of Message field. For most events first line is usually more than enough
|
||||
only_first_line_of_message = true
|
||||
|
||||
## Fields to include as tags. Globbing supported ("Level*" for both "Level" and "LevelText")
|
||||
event_tags = ["Source", "EventID", "Level", "LevelText", "Task", "TaskText", "Opcode", "OpcodeText", "Keywords", "Channel", "Computer"]
|
||||
|
||||
## Default list of fields to send. All fields are sent by default. Globbing supported
|
||||
event_fields = ["*"]
|
||||
|
||||
## Fields to exclude. Also applied to data fields. Globbing supported
|
||||
exclude_fields = ["Binary", "Data_Address*"]
|
||||
|
||||
## Skip those tags or fields if their value is empty or equals to zero. Globbing supported
|
||||
exclude_empty = ["*ActivityID", "UserID"]
|
||||
`
|
||||
|
||||
// WinEventLog config
|
||||
type WinEventLog struct {
|
||||
Locale uint32 `toml:"locale"`
|
||||
EventlogName string `toml:"eventlog_name"`
|
||||
Query string `toml:"xpath_query"`
|
||||
ProcessUserData bool `toml:"process_userdata"`
|
||||
ProcessEventData bool `toml:"process_eventdata"`
|
||||
Separator string `toml:"separator"`
|
||||
OnlyFirstLineOfMessage bool `toml:"only_first_line_of_message"`
|
||||
EventTags []string `toml:"event_tags"`
|
||||
EventFields []string `toml:"event_fields"`
|
||||
ExcludeFields []string `toml:"exclude_fields"`
|
||||
ExcludeEmpty []string `toml:"exclude_empty"`
|
||||
subscription EvtHandle
|
||||
buf []byte
|
||||
Log telegraf.Logger
|
||||
}
|
||||
|
||||
var bufferSize = 1 << 14
|
||||
|
||||
var description = "Input plugin to collect Windows Event Log messages"
|
||||
|
||||
// Description for win_eventlog
|
||||
func (w *WinEventLog) Description() string {
|
||||
return description
|
||||
}
|
||||
|
||||
// SampleConfig for win_eventlog
|
||||
func (w *WinEventLog) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Gather Windows Event Log entries
|
||||
func (w *WinEventLog) Gather(acc telegraf.Accumulator) error {
|
||||
|
||||
var err error
|
||||
if w.subscription == 0 {
|
||||
w.subscription, err = w.evtSubscribe(w.EventlogName, w.Query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Windows Event Log subscription error: %v", err.Error())
|
||||
}
|
||||
}
|
||||
w.Log.Debug("Subscription handle id:", w.subscription)
|
||||
|
||||
loop:
|
||||
for {
|
||||
events, err := w.fetchEvents(w.subscription)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err == ERROR_NO_MORE_ITEMS:
|
||||
break loop
|
||||
case err != nil:
|
||||
w.Log.Error("Error getting events:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
// Prepare fields names usage counter
|
||||
var fieldsUsage = map[string]int{}
|
||||
|
||||
tags := map[string]string{}
|
||||
fields := map[string]interface{}{}
|
||||
evt := reflect.ValueOf(&event).Elem()
|
||||
// Walk through all fields of Event struct to process System tags or fields
|
||||
for i := 0; i < evt.NumField(); i++ {
|
||||
fieldName := evt.Type().Field(i).Name
|
||||
fieldType := evt.Field(i).Type().String()
|
||||
fieldValue := evt.Field(i).Interface()
|
||||
computedValues := map[string]interface{}{}
|
||||
switch fieldName {
|
||||
case "Source":
|
||||
fieldValue = event.Source.Name
|
||||
fieldType = reflect.TypeOf(fieldValue).String()
|
||||
case "Execution":
|
||||
fieldValue := event.Execution.ProcessID
|
||||
fieldType = reflect.TypeOf(fieldValue).String()
|
||||
fieldName = "ProcessID"
|
||||
// Look up Process Name from pid
|
||||
if should, _ := w.shouldProcessField("ProcessName"); should {
|
||||
_, _, processName, err := GetFromSnapProcess(fieldValue)
|
||||
if err == nil {
|
||||
computedValues["ProcessName"] = processName
|
||||
}
|
||||
}
|
||||
case "TimeCreated":
|
||||
fieldValue = event.TimeCreated.SystemTime
|
||||
fieldType = reflect.TypeOf(fieldValue).String()
|
||||
case "Correlation":
|
||||
if should, _ := w.shouldProcessField("ActivityID"); should {
|
||||
activityID := event.Correlation.ActivityID
|
||||
if len(activityID) > 0 {
|
||||
computedValues["ActivityID"] = activityID
|
||||
}
|
||||
}
|
||||
if should, _ := w.shouldProcessField("RelatedActivityID"); should {
|
||||
relatedActivityID := event.Correlation.RelatedActivityID
|
||||
if len(relatedActivityID) > 0 {
|
||||
computedValues["RelatedActivityID"] = relatedActivityID
|
||||
}
|
||||
}
|
||||
case "Security":
|
||||
computedValues["UserID"] = event.Security.UserID
|
||||
// Look up UserName and Domain from SID
|
||||
if should, _ := w.shouldProcessField("UserName"); should {
|
||||
sid := event.Security.UserID
|
||||
usid, err := syscall.StringToSid(sid)
|
||||
if err == nil {
|
||||
username, domain, _, err := usid.LookupAccount("")
|
||||
if err == nil {
|
||||
computedValues["UserName"] = fmt.Sprint(domain, "\\", username)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
if should, where := w.shouldProcessField(fieldName); should {
|
||||
if where == "tags" {
|
||||
strValue := fmt.Sprintf("%v", fieldValue)
|
||||
if !w.shouldExcludeEmptyField(fieldName, "string", strValue) {
|
||||
tags[fieldName] = strValue
|
||||
fieldsUsage[fieldName]++
|
||||
}
|
||||
} else if where == "fields" {
|
||||
if !w.shouldExcludeEmptyField(fieldName, fieldType, fieldValue) {
|
||||
fields[fieldName] = fieldValue
|
||||
fieldsUsage[fieldName]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert computed fields
|
||||
for computedKey, computedValue := range computedValues {
|
||||
if should, where := w.shouldProcessField(computedKey); should {
|
||||
if where == "tags" {
|
||||
tags[computedKey] = fmt.Sprintf("%v", computedValue)
|
||||
fieldsUsage[computedKey]++
|
||||
} else if where == "fields" {
|
||||
fields[computedKey] = computedValue
|
||||
fieldsUsage[computedKey]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unroll additional XML
|
||||
var xmlFields []EventField
|
||||
if w.ProcessUserData {
|
||||
fieldsUserData, xmlFieldsUsage := UnrollXMLFields(event.UserData.InnerXML, fieldsUsage, w.Separator)
|
||||
xmlFields = append(xmlFields, fieldsUserData...)
|
||||
fieldsUsage = xmlFieldsUsage
|
||||
}
|
||||
if w.ProcessEventData {
|
||||
fieldsEventData, xmlFieldsUsage := UnrollXMLFields(event.EventData.InnerXML, fieldsUsage, w.Separator)
|
||||
xmlFields = append(xmlFields, fieldsEventData...)
|
||||
fieldsUsage = xmlFieldsUsage
|
||||
}
|
||||
uniqueXMLFields := UniqueFieldNames(xmlFields, fieldsUsage, w.Separator)
|
||||
for _, xmlField := range uniqueXMLFields {
|
||||
if !w.shouldExclude(xmlField.Name) {
|
||||
fields[xmlField.Name] = xmlField.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Pass collected metrics
|
||||
acc.AddFields("win_eventlog", fields, tags)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldExclude(field string) (should bool) {
|
||||
for _, excludePattern := range w.ExcludeFields {
|
||||
// Check if field name matches excluded list
|
||||
if matched, _ := filepath.Match(excludePattern, field); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldProcessField(field string) (should bool, list string) {
|
||||
for _, pattern := range w.EventTags {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
// Tags are not excluded
|
||||
return true, "tags"
|
||||
}
|
||||
}
|
||||
|
||||
for _, pattern := range w.EventFields {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
if w.shouldExclude(field) {
|
||||
return false, "excluded"
|
||||
}
|
||||
return true, "fields"
|
||||
}
|
||||
}
|
||||
return false, "excluded"
|
||||
}
|
||||
|
||||
func (w *WinEventLog) shouldExcludeEmptyField(field string, fieldType string, fieldValue interface{}) (should bool) {
|
||||
for _, pattern := range w.ExcludeEmpty {
|
||||
if matched, _ := filepath.Match(pattern, field); matched {
|
||||
switch fieldType {
|
||||
case "string":
|
||||
return len(fieldValue.(string)) < 1
|
||||
case "int":
|
||||
return fieldValue.(int) == 0
|
||||
case "uint32":
|
||||
return fieldValue.(uint32) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *WinEventLog) evtSubscribe(logName, xquery string) (EvtHandle, error) {
|
||||
var logNamePtr, xqueryPtr *uint16
|
||||
|
||||
sigEvent, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer windows.CloseHandle(sigEvent)
|
||||
|
||||
logNamePtr, err = syscall.UTF16PtrFromString(logName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
xqueryPtr, err = syscall.UTF16PtrFromString(xquery)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
subsHandle, err := _EvtSubscribe(0, uintptr(sigEvent), logNamePtr, xqueryPtr,
|
||||
0, 0, 0, EvtSubscribeToFutureEvents)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return subsHandle, nil
|
||||
}
|
||||
|
||||
func (w *WinEventLog) fetchEventHandles(subsHandle EvtHandle) ([]EvtHandle, error) {
|
||||
var eventsNumber uint32
|
||||
var evtReturned uint32
|
||||
|
||||
eventsNumber = 5
|
||||
|
||||
eventHandles := make([]EvtHandle, eventsNumber)
|
||||
|
||||
err := _EvtNext(subsHandle, eventsNumber, &eventHandles[0], 0, 0, &evtReturned)
|
||||
if err != nil {
|
||||
if err == ERROR_INVALID_OPERATION && evtReturned == 0 {
|
||||
return nil, ERROR_NO_MORE_ITEMS
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return eventHandles[:evtReturned], nil
|
||||
}
|
||||
|
||||
func (w *WinEventLog) fetchEvents(subsHandle EvtHandle) ([]Event, error) {
|
||||
var events []Event
|
||||
|
||||
eventHandles, err := w.fetchEventHandles(subsHandle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, eventHandle := range eventHandles {
|
||||
if eventHandle != 0 {
|
||||
event, err := w.renderEvent(eventHandle)
|
||||
if err == nil {
|
||||
// w.Log.Debugf("Got event: %v", event)
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(eventHandles); i++ {
|
||||
err := _EvtClose(eventHandles[i])
|
||||
if err != nil {
|
||||
return events, err
|
||||
}
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (w *WinEventLog) renderEvent(eventHandle EvtHandle) (Event, error) {
|
||||
var bufferUsed, propertyCount uint32
|
||||
|
||||
event := Event{}
|
||||
err := _EvtRender(0, eventHandle, EvtRenderEventXml, uint32(len(w.buf)), &w.buf[0], &bufferUsed, &propertyCount)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
eventXML, err := DecodeUTF16(w.buf[:bufferUsed])
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
err = xml.Unmarshal([]byte(eventXML), &event)
|
||||
if err != nil {
|
||||
// We can return event without most text values,
|
||||
// that way we will not loose information
|
||||
// This can happen when processing Forwarded Events
|
||||
return event, nil
|
||||
}
|
||||
|
||||
publisherHandle, err := openPublisherMetadata(0, event.Source.Name, w.Locale)
|
||||
if err != nil {
|
||||
return event, nil
|
||||
}
|
||||
defer _EvtClose(publisherHandle)
|
||||
|
||||
// Populating text values
|
||||
keywords, err := formatEventString(EvtFormatMessageKeyword, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.Keywords = keywords
|
||||
}
|
||||
message, err := formatEventString(EvtFormatMessageEvent, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
if w.OnlyFirstLineOfMessage {
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
scanner.Scan()
|
||||
message = scanner.Text()
|
||||
}
|
||||
event.Message = message
|
||||
}
|
||||
level, err := formatEventString(EvtFormatMessageLevel, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.LevelText = level
|
||||
}
|
||||
task, err := formatEventString(EvtFormatMessageTask, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.TaskText = task
|
||||
}
|
||||
opcode, err := formatEventString(EvtFormatMessageOpcode, eventHandle, publisherHandle)
|
||||
if err == nil {
|
||||
event.OpcodeText = opcode
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func formatEventString(
|
||||
messageFlag EvtFormatMessageFlag,
|
||||
eventHandle EvtHandle,
|
||||
publisherHandle EvtHandle,
|
||||
) (string, error) {
|
||||
var bufferUsed uint32
|
||||
err := _EvtFormatMessage(publisherHandle, eventHandle, 0, 0, 0, messageFlag,
|
||||
0, nil, &bufferUsed)
|
||||
if err != nil && err != ERROR_INSUFFICIENT_BUFFER {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bufferUsed *= 2
|
||||
buffer := make([]byte, bufferUsed)
|
||||
bufferUsed = 0
|
||||
|
||||
err = _EvtFormatMessage(publisherHandle, eventHandle, 0, 0, 0, messageFlag,
|
||||
uint32(len(buffer)/2), &buffer[0], &bufferUsed)
|
||||
bufferUsed *= 2
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := DecodeUTF16(buffer[:bufferUsed])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var out string
|
||||
if messageFlag == EvtFormatMessageKeyword {
|
||||
// Keywords are returned as array of a zero-terminated strings
|
||||
splitZero := func(c rune) bool { return c == '\x00' }
|
||||
eventKeywords := strings.FieldsFunc(string(result), splitZero)
|
||||
// So convert them to comma-separated string
|
||||
out = strings.Join(eventKeywords, ",")
|
||||
} else {
|
||||
result := bytes.Trim(result, "\x00")
|
||||
out = string(result)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// openPublisherMetadata opens a handle to the publisher's metadata. Close must
|
||||
// be called on returned EvtHandle when finished with the handle.
|
||||
func openPublisherMetadata(
|
||||
session EvtHandle,
|
||||
publisherName string,
|
||||
lang uint32,
|
||||
) (EvtHandle, error) {
|
||||
p, err := syscall.UTF16PtrFromString(publisherName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
h, err := _EvtOpenPublisherMetadata(session, p, nil, lang, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("win_eventlog", func() telegraf.Input {
|
||||
return &WinEventLog{
|
||||
buf: make([]byte, bufferSize),
|
||||
ProcessUserData: true,
|
||||
ProcessEventData: true,
|
||||
Separator: "_",
|
||||
OnlyFirstLineOfMessage: true,
|
||||
EventTags: []string{"Source", "EventID", "Level", "LevelText", "Keywords", "Channel", "Computer"},
|
||||
EventFields: []string{"*"},
|
||||
ExcludeEmpty: []string{"Task", "Opcode", "*ActivityID", "UserID"},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// +build !windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWinEventLog_shouldExcludeEmptyField(t *testing.T) {
|
||||
type args struct {
|
||||
field string
|
||||
fieldType string
|
||||
fieldValue interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
w *WinEventLog
|
||||
args args
|
||||
wantShould bool
|
||||
}{
|
||||
{
|
||||
name: "Not in list",
|
||||
args: args{field: "qq", fieldType: "string", fieldValue: ""},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
args: args{field: "test", fieldType: "string", fieldValue: ""},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-empty string",
|
||||
args: args{field: "test", fieldType: "string", fieldValue: "qq"},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Zero int",
|
||||
args: args{field: "test", fieldType: "int", fieldValue: int(0)},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-zero int",
|
||||
args: args{field: "test", fieldType: "int", fieldValue: int(-1)},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Zero uint32",
|
||||
args: args{field: "test", fieldType: "uint32", fieldValue: uint32(0)},
|
||||
wantShould: true,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
{
|
||||
name: "Non-zero uint32",
|
||||
args: args{field: "test", fieldType: "uint32", fieldValue: uint32(0xc0fefeed)},
|
||||
wantShould: false,
|
||||
w: &WinEventLog{ExcludeEmpty: []string{"te*"}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotShould := tt.w.shouldExcludeEmptyField(tt.args.field, tt.args.fieldType, tt.args.fieldValue); gotShould != tt.wantShould {
|
||||
t.Errorf("WinEventLog.shouldExcludeEmptyField() = %v, want %v", gotShould, tt.wantShould)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWinEventLog_shouldProcessField(t *testing.T) {
|
||||
tags := []string{"Source", "Level*"}
|
||||
fields := []string{"EventID", "Message*"}
|
||||
excluded := []string{"Message*"}
|
||||
type args struct {
|
||||
field string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
w *WinEventLog
|
||||
args args
|
||||
wantShould bool
|
||||
wantList string
|
||||
}{
|
||||
{
|
||||
name: "Not in tags",
|
||||
args: args{field: "test"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Tags",
|
||||
args: args{field: "LevelText"},
|
||||
wantShould: true,
|
||||
wantList: "tags",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "Not in Fields",
|
||||
args: args{field: "EventId"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Fields",
|
||||
args: args{field: "EventID"},
|
||||
wantShould: true,
|
||||
wantList: "fields",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
{
|
||||
name: "In Fields and Excluded",
|
||||
args: args{field: "Messages"},
|
||||
wantShould: false,
|
||||
wantList: "excluded",
|
||||
w: &WinEventLog{EventTags: tags, EventFields: fields, ExcludeFields: excluded},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotShould, gotList := tt.w.shouldProcessField(tt.args.field)
|
||||
if gotShould != tt.wantShould {
|
||||
t.Errorf("WinEventLog.shouldProcessField() gotShould = %v, want %v", gotShould, tt.wantShould)
|
||||
}
|
||||
if gotList != tt.wantList {
|
||||
t.Errorf("WinEventLog.shouldProcessField() gotList = %v, want %v", gotList, tt.wantList)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
//+build windows
|
||||
|
||||
//revive:disable-next-line:var-naming
|
||||
// Package win_eventlog Input plugin to collect Windows Event Log messages
|
||||
package win_eventlog
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// EvtHandle uintptr
|
||||
type EvtHandle uintptr
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
//revive:disable-next-line:var-naming
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
//revive:disable-next-line:var-naming
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
)
|
||||
|
||||
// EvtFormatMessageFlag defines the values that specify the message string from
|
||||
// the event to format.
|
||||
type EvtFormatMessageFlag uint32
|
||||
|
||||
// EVT_FORMAT_MESSAGE_FLAGS enumeration
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385525(v=vs.85).aspx
|
||||
const (
|
||||
//revive:disable:var-naming
|
||||
// Format the event's message string.
|
||||
EvtFormatMessageEvent EvtFormatMessageFlag = iota + 1
|
||||
// Format the message string of the level specified in the event.
|
||||
EvtFormatMessageLevel
|
||||
// Format the message string of the task specified in the event.
|
||||
EvtFormatMessageTask
|
||||
// Format the message string of the task specified in the event.
|
||||
EvtFormatMessageOpcode
|
||||
// Format the message string of the keywords specified in the event. If the
|
||||
// event specifies multiple keywords, the formatted string is a list of
|
||||
// null-terminated strings. Increment through the strings until your pointer
|
||||
// points past the end of the used buffer.
|
||||
EvtFormatMessageKeyword
|
||||
//revive:enable:var-naming
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return nil
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modwevtapi = windows.NewLazySystemDLL("wevtapi.dll")
|
||||
|
||||
procEvtSubscribe = modwevtapi.NewProc("EvtSubscribe")
|
||||
procEvtRender = modwevtapi.NewProc("EvtRender")
|
||||
procEvtClose = modwevtapi.NewProc("EvtClose")
|
||||
procEvtNext = modwevtapi.NewProc("EvtNext")
|
||||
procEvtFormatMessage = modwevtapi.NewProc("EvtFormatMessage")
|
||||
procEvtOpenPublisherMetadata = modwevtapi.NewProc("EvtOpenPublisherMetadata")
|
||||
)
|
||||
|
||||
func _EvtSubscribe(session EvtHandle, signalEvent uintptr, channelPath *uint16, query *uint16, bookmark EvtHandle, context uintptr, callback syscall.Handle, flags EvtSubscribeFlag) (handle EvtHandle, err error) {
|
||||
r0, _, e1 := syscall.Syscall9(procEvtSubscribe.Addr(), 8, uintptr(session), uintptr(signalEvent), uintptr(unsafe.Pointer(channelPath)), uintptr(unsafe.Pointer(query)), uintptr(bookmark), uintptr(context), uintptr(callback), uintptr(flags), 0)
|
||||
handle = EvtHandle(r0)
|
||||
if handle == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtRender(context EvtHandle, fragment EvtHandle, flags EvtRenderFlag, bufferSize uint32, buffer *byte, bufferUsed *uint32, propertyCount *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procEvtRender.Addr(), 7, uintptr(context), uintptr(fragment), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferUsed)), uintptr(unsafe.Pointer(propertyCount)), 0, 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtClose(object EvtHandle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procEvtClose.Addr(), 1, uintptr(object), 0, 0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtNext(resultSet EvtHandle, eventArraySize uint32, eventArray *EvtHandle, timeout uint32, flags uint32, numReturned *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procEvtNext.Addr(), 6, uintptr(resultSet), uintptr(eventArraySize), uintptr(unsafe.Pointer(eventArray)), uintptr(timeout), uintptr(flags), uintptr(unsafe.Pointer(numReturned)))
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtFormatMessage(publisherMetadata EvtHandle, event EvtHandle, messageID uint32, valueCount uint32, values uintptr, flags EvtFormatMessageFlag, bufferSize uint32, buffer *byte, bufferUsed *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procEvtFormatMessage.Addr(), 9, uintptr(publisherMetadata), uintptr(event), uintptr(messageID), uintptr(valueCount), uintptr(values), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferUsed)))
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func _EvtOpenPublisherMetadata(session EvtHandle, publisherIdentity *uint16, logFilePath *uint16, locale uint32, flags uint32) (handle EvtHandle, err error) {
|
||||
r0, _, e1 := syscall.Syscall6(procEvtOpenPublisherMetadata.Addr(), 5, uintptr(session), uintptr(unsafe.Pointer(publisherIdentity)), uintptr(unsafe.Pointer(logFilePath)), uintptr(locale), uintptr(flags), 0)
|
||||
handle = EvtHandle(r0)
|
||||
if handle == 0 {
|
||||
if e1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Loading…
Reference in New Issue