diff --git a/plugins/inputs/windows_event_log/wineventlog/utils.go b/plugins/inputs/windows_event_log/wineventlog/utils.go
index ca57856007..fd3ee10f81 100644
--- a/plugins/inputs/windows_event_log/wineventlog/utils.go
+++ b/plugins/inputs/windows_event_log/wineventlog/utils.go
@@ -11,6 +11,8 @@ import (
"fmt"
"io/ioutil"
"log"
+ "strconv"
+ "strings"
"syscall"
"time"
@@ -168,3 +170,52 @@ func WindowsEventLogLevelName(levelId int32) string {
return UNKNOWN
}
}
+
+// insertPlaceholderValues formats the message with the correct values if we see those data
+// in evtDataValues.
+//
+// In some cases wevtapi does not insert values when formatting the message. The message
+// will contain insertion string placeholders, of the form %n, where %1 indicates the first
+// insertion string, and so on. Noted that wevtapi start the index with 1.
+// https://learn.microsoft.com/en-us/windows/win32/eventlog/event-identifiers#insertion-strings
+func insertPlaceholderValues(rawMessage string, evtDataValues []Datum) string {
+ if len(evtDataValues) == 0 || len(rawMessage) == 0 {
+ return rawMessage
+ }
+ var sb strings.Builder
+ prevIndex := 0
+ searchingIndex := false
+ for i, c := range rawMessage {
+ // found `%` previously. Determine the index number from the following character(s)
+ if searchingIndex && (c > '9' || c < '0') {
+ // Convert the Slice since the last `%` and see if it's a valid number.
+ ind, err := strconv.Atoi(rawMessage[prevIndex+1 : i])
+ // If the index is in [1 - len(evtDataValues)], get it from evtDataValues.
+ if err == nil && ind <= len(evtDataValues) && ind > 0 {
+ sb.WriteString(evtDataValues[ind-1].Value)
+ } else {
+ sb.WriteString(rawMessage[prevIndex:i])
+ }
+ prevIndex = i
+ // In case of consecutive `%`, continue searching for the next index
+ if c != '%' {
+ searchingIndex = false
+ }
+ } else {
+ if c == '%' {
+ sb.WriteString(rawMessage[prevIndex:i])
+ searchingIndex = true
+ prevIndex = i
+ }
+
+ }
+ }
+ // handle the slice since the last `%` to the end of rawMessage
+ ind, err := strconv.Atoi(rawMessage[prevIndex+1:])
+ if searchingIndex && err == nil && ind <= len(evtDataValues) && ind > 0 {
+ sb.WriteString(evtDataValues[ind-1].Value)
+ } else {
+ sb.WriteString(rawMessage[prevIndex:])
+ }
+ return sb.String()
+}
diff --git a/plugins/inputs/windows_event_log/wineventlog/utils_test.go b/plugins/inputs/windows_event_log/wineventlog/utils_test.go
index 7b05d93350..40586504d3 100644
--- a/plugins/inputs/windows_event_log/wineventlog/utils_test.go
+++ b/plugins/inputs/windows_event_log/wineventlog/utils_test.go
@@ -77,6 +77,58 @@ func TestFullBufferUsedWithHalfUsedSizeReturned(t *testing.T) {
assert.Equal(t, bufferUsed, len(str))
}
+func TestInsertPlaceholderValues(t *testing.T) {
+ evtDataValues := []Datum{
+ {"value_1"}, {"value_2"}, {"value_3"}, {"value_4"},
+ }
+ tests := []struct {
+ name string
+ message string
+ expected string
+ }{
+ {
+ "Placeholders %{number} should be replaced by insertion strings",
+ "Service %1 in region %3 stop at %2",
+ "Service value_1 in region value_3 stop at value_2",
+ },
+ {
+ "String without a placeholder should remain the same after insertion",
+ "This is a sentence without placeholders",
+ "This is a sentence without placeholders",
+ },
+ {
+ "Empty string should remain the same",
+ "",
+ "",
+ },
+ {
+ "Index should start from 1 and less than or equal to the amount of values in event data",
+ "%0 %3 %5",
+ "%0 value_3 %5",
+ },
+ {
+ "Handle consecutive % characters",
+ "%1 %%3% %2",
+ "value_1 %value_3% value_2",
+ },
+ {
+ "Handle % character at the end of message",
+ "%3 %2%",
+ "value_3 value_2%",
+ },
+ {
+ "Characters after a % other than numbers should be ignored",
+ "%foo, %foo1, %#$%^&1",
+ "%foo, %foo1, %#$%^&1",
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, insertPlaceholderValues(tc.message, evtDataValues))
+ })
+ }
+}
+
func resetState() {
NumberOfBytesPerCharacter = 0
}
diff --git a/plugins/inputs/windows_event_log/wineventlog/wineventlog.go b/plugins/inputs/windows_event_log/wineventlog/wineventlog.go
index 656839bc52..0dd349397b 100644
--- a/plugins/inputs/windows_event_log/wineventlog/wineventlog.go
+++ b/plugins/inputs/windows_event_log/wineventlog/wineventlog.go
@@ -328,10 +328,20 @@ func (w *windowsEventLog) getRecord(evtHandle EvtHandle) (*windowsEventLogRecord
return nil, fmt.Errorf("utf16ToUTF8Bytes() err %v", err)
}
+ // The insertion strings could be in either EventData or UserData
+ // Notes on the insertion strings:
+ // - The EvtFormatMessage has the valueCount and values parameters, yet it does not work when we tried passing
+ // EventData/UserData into those parameters. We can later do more research on making EvtFormatMessage with
+ // valueCount and values parameters works and compare if there is any benefit.
+ dataValues := newRecord.EventData.Data
+ // The UserData section is used if EventData is empty
+ if len(dataValues) == 0 {
+ dataValues = newRecord.UserData.Data
+ }
switch w.renderFormat {
case FormatXml, FormatDefault:
//XML format
- newRecord.XmlFormatContent = string(descriptionBytes)
+ newRecord.XmlFormatContent = insertPlaceholderValues(string(descriptionBytes), dataValues)
case FormatPlainText:
//old SSM agent Windows format
var recordMessage eventMessage
@@ -339,7 +349,7 @@ func (w *windowsEventLog) getRecord(evtHandle EvtHandle) (*windowsEventLogRecord
if err != nil {
return nil, fmt.Errorf("Unmarshal() err %v", err)
}
- newRecord.System.Description = recordMessage.Message
+ newRecord.System.Description = insertPlaceholderValues(recordMessage.Message, dataValues)
default:
return nil, fmt.Errorf("renderFormat is not recognized, %s", w.renderFormat)
}
diff --git a/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord.go b/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord.go
index 33f88164fc..516f5b6b01 100644
--- a/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord.go
+++ b/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord.go
@@ -7,6 +7,7 @@
package wineventlog
import (
+ "encoding/xml"
"fmt"
"strconv"
"time"
@@ -45,6 +46,9 @@ type windowsEventLogRecord struct {
Name string `xml:"Name,attr"`
} `xml:"Provider"`
} `xml:"System"`
+
+ EventData EventData `xml:"EventData"`
+ UserData UserData `xml:"UserData"`
}
func newEventLogRecord(l *windowsEventLog) *windowsEventLogRecord {
@@ -78,3 +82,46 @@ func (record *windowsEventLogRecord) Value() (valueString string, err error) {
func (record *windowsEventLogRecord) Timestamp() string {
return fmt.Sprint(record.System.TimeCreated.SystemTime.UnixNano())
}
+
+type Datum struct {
+ Value string `xml:",chardata"`
+}
+
+type EventData struct {
+ Data []Datum `xml:",any"`
+}
+
+type UserData struct {
+ Data []Datum `xml:",any"`
+}
+
+// UnmarshalXML unmarshals the UserData section in the windows event xml to UserData struct
+//
+// UserData has slightly different schema than EventData so that we need to override this
+// to get similar structure
+// https://learn.microsoft.com/en-us/windows/win32/wes/eventschema-userdatatype-complextype
+// https://learn.microsoft.com/en-us/windows/win32/wes/eventschema-eventdatatype-complextype
+func (u *UserData) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ in := EventData{}
+
+ // Read tokens until we find the first StartElement then unmarshal it.
+ for {
+ t, err := d.Token()
+ if err != nil {
+ return err
+ }
+
+ if se, ok := t.(xml.StartElement); ok {
+ err = d.DecodeElement(&in, &se)
+ if err != nil {
+ return err
+ }
+
+ u.Data = in.Data
+ d.Skip()
+ break
+ }
+ }
+
+ return nil
+}
diff --git a/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord_test.go b/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord_test.go
new file mode 100644
index 0000000000..e07c600e2f
--- /dev/null
+++ b/plugins/inputs/windows_event_log/wineventlog/wineventlogrecord_test.go
@@ -0,0 +1,67 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+//go:build windows
+// +build windows
+
+package wineventlog
+
+import (
+ "encoding/xml"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestUnmarshalWinEvtRecord(t *testing.T) {
+ tests := []struct {
+ xml string
+ wEvtRecord windowsEventLogRecord
+ }{
+ {
+ xml: `
+
+
+ 2022-10-28T22:33:25Z
+ RulesEngine
+ 2
+
+
+ `,
+ wEvtRecord: windowsEventLogRecord{
+ EventData: EventData{
+ Data: []Datum{
+ {"2022-10-28T22:33:25Z"},
+ {"RulesEngine"},
+ {"2"},
+ },
+ },
+ },
+ },
+ {
+ xml: `
+
+
+
+ 0
+ 2022-10-26T20:24:13.4253261Z
+
+
+
+ `,
+ wEvtRecord: windowsEventLogRecord{
+ UserData: UserData{
+ Data: []Datum{
+ {"0"},
+ {"2022-10-26T20:24:13.4253261Z"},
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ var record windowsEventLogRecord
+ xml.Unmarshal([]byte(test.xml), &record)
+ assert.Equal(t, test.wEvtRecord, record)
+ }
+}