plugins/inputs/windows_event_log/wineventlog/utils.go (167 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT
//go:build windows
// +build windows
package wineventlog
import (
"bytes"
"fmt"
"io"
"log"
"strconv"
"strings"
"syscall"
"time"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
const (
bookmarkTemplate = `<BookmarkList><Bookmark Channel="%s" RecordId="%d" IsCurrent="True"/></BookmarkList>`
eventLogQueryTemplate = `<QueryList><Query Id="0"><Select Path="%s">%s</Select></Query></QueryList>`
eventLogLevelFilter = "Level='%s'"
eventIgnoreOldFilter = "TimeCreated[timediff(@SystemTime) <= %d]"
emptySpaceScanLength = 100
UnknownBytesPerCharacter = 0
CRITICAL = "CRITICAL"
ERROR = "ERROR"
WARNING = "WARNING"
INFORMATION = "INFORMATION"
VERBOSE = "VERBOSE"
UNKNOWN = "UNKNOWN"
)
var NumberOfBytesPerCharacter = UnknownBytesPerCharacter
func RenderEventXML(eventHandle EvtHandle, renderBuf []byte) ([]byte, error) {
var bufferUsed, propertyCount uint32
if err := EvtRender(0, eventHandle, EvtRenderEventXml, uint32(len(renderBuf)), &renderBuf[0], &bufferUsed, &propertyCount); err != nil {
return nil, fmt.Errorf("error when rendering events. Details: %v", err)
}
// Per MSDN as of Mar 14th 2022(https://docs.microsoft.com/en-us/windows/win32/api/winevt/nf-winevt-evtrender)
// EvtRender function is still returning buffer used as BYTES, not characters. So keep using utf16ToUTF8Bytes()
return utf16ToUTF8Bytes(renderBuf, bufferUsed)
}
func CreateBookmark(channel string, recordID uint64) (h EvtHandle, err error) {
xml := fmt.Sprintf(bookmarkTemplate, channel, recordID)
p, err := syscall.UTF16PtrFromString(xml)
if err != nil {
return 0, err
}
h, err = EvtCreateBookmark(p)
if err != nil {
return 0, fmt.Errorf("error when creating a bookmark. Details: %v", err)
}
return h, nil
}
func CreateQuery(path string, levels []string) (*uint16, error) {
var filterLevels string
for _, level := range levels {
if filterLevels == "" {
filterLevels = fmt.Sprintf(eventLogLevelFilter, level)
} else {
filterLevels = filterLevels + " or " + fmt.Sprintf(eventLogLevelFilter, level)
}
}
//Ignore events older than 2 weeks
cutOffPeriod := (time.Hour * 24 * 14).Nanoseconds()
ignoreOlderThanTwoWeeksFilter := fmt.Sprintf(eventIgnoreOldFilter, cutOffPeriod/int64(time.Millisecond))
if filterLevels != "" {
filterLevels = "*[System[(" + filterLevels + ") and " + ignoreOlderThanTwoWeeksFilter + "]]"
} else {
filterLevels = "*[System[" + ignoreOlderThanTwoWeeksFilter + "]]"
}
xml := fmt.Sprintf(eventLogQueryTemplate, path, filterLevels)
return syscall.UTF16PtrFromString(xml)
}
func utf16ToUTF8Bytes(in []byte, length uint32) ([]byte, error) {
i := length
if length%2 != 0 {
i = length - 1
}
for ; i-2 > 0; i -= 2 {
v1 := uint16(in[i-2]) | uint16(in[i-1])<<8
// Stop at non-null char.
if v1 != 0 {
break
}
}
win16be := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
utf16bom := unicode.BOMOverride(win16be.NewDecoder())
unicodeReader := transform.NewReader(bytes.NewReader(in[:i]), utf16bom)
decoded, err := io.ReadAll(unicodeReader)
return decoded, err
}
func UTF16ToUTF8BytesForWindowsEventBuffer(in []byte, length uint32) ([]byte, error) {
// Since Windows server 2022, the returned value of used buffer represents for double bytes char count,
// which is half of the actual buffer used by byte(what older Windows OS returns), checking if the length
//land on the end of used buffer, if no, double it.
if NumberOfBytesPerCharacter == UnknownBytesPerCharacter {
if isTheEndOfContent(in, length) {
log.Printf("I! Buffer used: %d is returning as single byte character count", length)
NumberOfBytesPerCharacter = 1
} else {
log.Printf("I! Buffer used: %d is returning as double byte character count, doubling it to get the whole buffer content.", length)
NumberOfBytesPerCharacter = 2
}
}
i := int(length) * NumberOfBytesPerCharacter
if i > cap(in) {
i = cap(in)
}
return utf16ToUTF8Bytes(in, uint32(i))
}
func isTheEndOfContent(in []byte, length uint32) bool {
// scan next (emptySpaceScanLength) bytes, if any of them is none '0', return false
i := int(length)
if i%2 != 0 {
i -= 1
}
max := len(in)
if i+emptySpaceScanLength < max {
max = i + emptySpaceScanLength
}
for ; i < max-2; i += 2 {
v1 := uint16(in[i+2]) | uint16(in[i+1])<<8
// Stop at non-null char.
if v1 != 0 {
return false
}
}
return true
}
func WindowsEventLogLevelName(levelId int32) string {
switch levelId {
case 1:
return CRITICAL
case 2:
return ERROR
case 3:
return WARNING
case 0, 4:
return INFORMATION
case 5:
return VERBOSE
default:
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()
}