diff --git a/Vagrantfile b/Vagrantfile
index b7cc6f03523..eba786adda2 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -164,6 +164,11 @@ Vagrant.configure(2) do |config|
config.vm.define "win2019", primary: true do |c|
c.vm.box = "StefanScherer/windows_2019"
c.vm.provision "shell", inline: $winPsProvision, privileged: false
+
+ c.vm.provider :virtualbox do |vbox|
+ vbox.memory = 4096
+ vbox.cpus = 4
+ end
end
# Solaris 11.2
diff --git a/winlogbeat/eventlog/bench_test.go b/winlogbeat/eventlog/bench_test.go
index e665f01bbbd..ec680c5fb5d 100644
--- a/winlogbeat/eventlog/bench_test.go
+++ b/winlogbeat/eventlog/bench_test.go
@@ -22,95 +22,112 @@ package eventlog
import (
"bytes"
"flag"
+ "fmt"
"math/rand"
- "os/exec"
"strconv"
+ "strings"
"testing"
- "time"
- elog "github.com/andrewkroh/sys/windows/svc/eventlog"
- "github.com/dustin/go-humanize"
+ "golang.org/x/sys/windows/svc/eventlog"
+
+ "github.com/elastic/beats/v7/libbeat/common"
)
-// Benchmark tests with customized output. (`go test -v -benchtime 10s -benchtest .`)
+const gigabyte = 1 << 30
var (
- benchTest = flag.Bool("benchtest", false, "Run benchmarks for the eventlog package")
- injectAmount = flag.Int("inject", 50000, "Number of events to inject before running benchmarks")
+ benchTest = flag.Bool("benchtest", false, "Run benchmarks for the eventlog package.")
+ injectAmount = flag.Int("inject", 1E6, "Number of events to inject before running benchmarks.")
)
-// TestBenchmarkBatchReadSize tests the performance of different
-// batch_read_size values.
-func TestBenchmarkBatchReadSize(t *testing.T) {
+// TestBenchmarkRead benchmarks each event log reader implementation with
+// different batch sizes.
+//
+// Recommended usage:
+// go test -run TestBenchmarkRead -benchmem -benchtime 10s -benchtest -v .
+func TestBenchmarkRead(t *testing.T) {
if !*benchTest {
t.Skip("-benchtest not enabled")
}
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t)
+ defer teardown()
- // Increase the log size so that it can hold these large events.
- output, err := exec.Command("wevtutil.exe", "sl", "/ms:1073741824", providerName).CombinedOutput()
- if err != nil {
- t.Fatal(err, string(output))
- }
+ setLogSize(t, providerName, gigabyte)
// Publish test messages:
for i := 0; i < *injectAmount; i++ {
- err = log.Report(elog.Info, uint32(rand.Int63()%1000), []string{strconv.Itoa(i) + " " + randomSentence(256)})
+ err := writer.Report(eventlog.Info, uint32(rand.Int63()%1000), []string{strconv.Itoa(i) + " " + randomSentence(256)})
if err != nil {
- t.Fatal("ReportEvent error", err)
+ t.Fatal(err)
}
}
- benchTest := func(batchSize int) {
- var err error
- result := testing.Benchmark(func(b *testing.B) {
- eventlog, tearDown := setupWinEventLog(t, 0, map[string]interface{}{
- "name": providerName,
- "batch_read_size": batchSize,
- })
- defer tearDown()
- b.ResetTimer()
-
- // Each iteration reads one batch.
- for i := 0; i < b.N; i++ {
- _, err = eventlog.Read()
- if err != nil {
- return
- }
+ for _, api := range []string{winEventLogAPIName, winEventLogExpAPIName} {
+ t.Run("api="+api, func(t *testing.T) {
+ for _, batchSize := range []int{10, 100, 500, 1000} {
+ t.Run(fmt.Sprintf("batch_size=%d", batchSize), func(t *testing.T) {
+ result := testing.Benchmark(benchmarkEventLog(api, batchSize))
+ outputBenchmarkResults(t, result)
+ })
}
})
+ }
- if err != nil {
- t.Fatal(err)
- return
+ t.Run("api="+eventLoggingAPIName, func(t *testing.T) {
+ result := testing.Benchmark(benchmarkEventLog(eventLoggingAPIName, -1))
+ outputBenchmarkResults(t, result)
+ })
+}
+
+func benchmarkEventLog(api string, batchSize int) func(b *testing.B) {
+ return func(b *testing.B) {
+ conf := common.MapStr{
+ "name": providerName,
+ }
+ if strings.HasPrefix(api, "wineventlog") {
+ conf.Put("batch_read_size", batchSize)
+ conf.Put("no_more_events", "stop")
}
- t.Logf("batch_size=%v, total_events=%v, batch_time=%v, events_per_sec=%v, bytes_alloced_per_event=%v, total_allocs=%v",
- batchSize,
- result.N*batchSize,
- time.Duration(result.NsPerOp()),
- float64(batchSize)/time.Duration(result.NsPerOp()).Seconds(),
- humanize.Bytes(result.MemBytes/(uint64(result.N)*uint64(batchSize))),
- result.MemAllocs)
- }
+ log := openLog(b, api, nil, conf)
+ defer log.Close()
+
+ events := 0
+ b.ResetTimer()
+
+ // Each iteration reads one batch.
+ for i := 0; i < b.N; i++ {
+ records, err := log.Read()
+ if err != nil {
+ b.Fatal(err)
+ return
+ }
+ events += len(records)
+ }
+
+ b.StopTimer()
- benchTest(10)
- benchTest(100)
- benchTest(500)
- benchTest(1000)
+ b.ReportMetric(float64(events), "events")
+ b.ReportMetric(float64(batchSize), "batch_size")
+ }
}
-// Utility Functions
+func outputBenchmarkResults(t testing.TB, result testing.BenchmarkResult) {
+ totalBatches := result.N
+ totalEvents := int(result.Extra["events"])
+ totalBytes := result.MemBytes
+ totalAllocs := result.MemAllocs
+
+ eventsPerSec := float64(totalEvents) / result.T.Seconds()
+ bytesPerEvent := float64(totalBytes) / float64(totalEvents)
+ bytesPerBatch := float64(totalBytes) / float64(totalBatches)
+ allocsPerEvent := float64(totalAllocs) / float64(totalEvents)
+ allocsPerBatch := float64(totalAllocs) / float64(totalBatches)
+
+ t.Logf("%.2f events/sec\t %d B/event\t %d B/batch\t %d allocs/event\t %d allocs/batch",
+ eventsPerSec, int(bytesPerEvent), int(bytesPerBatch), int(allocsPerEvent), int(allocsPerBatch))
+}
var randomWords = []string{
"recover",
diff --git a/winlogbeat/eventlog/eventlogging.go b/winlogbeat/eventlog/eventlogging.go
index 3e9494c91b6..963797264b2 100644
--- a/winlogbeat/eventlog/eventlogging.go
+++ b/winlogbeat/eventlog/eventlogging.go
@@ -27,6 +27,7 @@ import (
"github.com/joeshaw/multierror"
"github.com/elastic/beats/v7/libbeat/common"
+ "github.com/elastic/beats/v7/libbeat/common/cfgwarn"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/winlogbeat/checkpoint"
"github.com/elastic/beats/v7/winlogbeat/sys"
@@ -277,6 +278,8 @@ func (l *eventLogging) ignoreOlder(r *Record) bool {
// newEventLogging creates and returns a new EventLog for reading event logs
// using the Event Logging API.
func newEventLogging(options *common.Config) (EventLog, error) {
+ cfgwarn.Deprecate("8.0", "The eventlogging API reader is deprecated.")
+
c := eventLoggingConfig{
ReadBufferSize: win.MaxEventBufferSize,
FormatBufferSize: win.MaxFormatMessageBufferSize,
diff --git a/winlogbeat/eventlog/eventlogging_test.go b/winlogbeat/eventlog/eventlogging_test.go
index 7ed4ea78772..ba8524cad09 100644
--- a/winlogbeat/eventlog/eventlogging_test.go
+++ b/winlogbeat/eventlog/eventlogging_test.go
@@ -21,14 +21,11 @@ package eventlog
import (
"fmt"
- "os/exec"
- "strconv"
"strings"
"sync"
"testing"
- elog "github.com/andrewkroh/sys/windows/svc/eventlog"
- "github.com/joeshaw/multierror"
+ "github.com/andrewkroh/sys/windows/svc/eventlog"
"github.com/stretchr/testify/assert"
"github.com/elastic/beats/v7/libbeat/logp"
@@ -54,37 +51,33 @@ const (
netEventMsgFile = "%SystemRoot%\\System32\\netevent.dll"
)
-const allLevels = elog.Success | elog.AuditFailure | elog.AuditSuccess | elog.Error | elog.Info | elog.Warning
-
-const gigabyte = 1 << 30
-
// Test messages.
var messages = map[uint32]struct {
eventType uint16
message string
}{
1: {
- eventType: elog.Info,
+ eventType: eventlog.Info,
message: "Hmmmm.",
},
2: {
- eventType: elog.Success,
+ eventType: eventlog.Success,
message: "I am so blue I'm greener than purple.",
},
3: {
- eventType: elog.Warning,
+ eventType: eventlog.Warning,
message: "I stepped on a Corn Flake, now I'm a Cereal Killer.",
},
4: {
- eventType: elog.Error,
+ eventType: eventlog.Error,
message: "The quick brown fox jumps over the lazy dog.",
},
5: {
- eventType: elog.AuditSuccess,
+ eventType: eventlog.AuditSuccess,
message: "Where do random thoughts come from?",
},
6: {
- eventType: elog.AuditFailure,
+ eventType: eventlog.AuditFailure,
message: "Login failure for user xyz!",
},
}
@@ -100,107 +93,27 @@ func configureLogp() {
} else {
logp.DevelopmentSetup(logp.WithLevel(logp.WarnLevel))
}
-
- // Clear the event log before starting.
- log, _ := elog.Open(sourceName)
- eventlogging.ClearEventLog(eventlogging.Handle(log.Handle), "")
- log.Close()
})
}
-// initLog initializes an event logger. It registers the source name with
-// the registry if it does not already exist.
-func initLog(provider, source, msgFile string) (*elog.Log, error) {
- // Install entry to registry:
- _, err := elog.Install(providerName, sourceName, msgFile, true, allLevels)
- if err != nil {
- return nil, err
- }
-
- // Open a new logger for writing events:
- log, err := elog.Open(sourceName)
- if err != nil {
- var errs multierror.Errors
- errs = append(errs, err)
- err := elog.RemoveSource(providerName, sourceName)
- if err != nil {
- errs = append(errs, err)
- }
- err = elog.RemoveProvider(providerName)
- if err != nil {
- errs = append(errs, err)
- }
- return nil, errs.Err()
- }
-
- return log, nil
-}
-
-// uninstallLog unregisters the event logger from the registry and closes the
-// log's handle if it is open.
-func uninstallLog(provider, source string, log *elog.Log) error {
- var errs multierror.Errors
-
- if log != nil {
- err := eventlogging.ClearEventLog(eventlogging.Handle(log.Handle), "")
- if err != nil {
- errs = append(errs, err)
- }
-
- err = log.Close()
- if err != nil {
- errs = append(errs, err)
- }
- }
-
- err := elog.RemoveSource(providerName, sourceName)
- if err != nil {
- errs = append(errs, err)
- }
-
- err = elog.RemoveProvider(providerName)
- if err != nil {
- errs = append(errs, err)
- }
-
- return errs.Err()
-}
-
-// setLogSize set the maximum number of bytes that an event log can hold.
-func setLogSize(t testing.TB, provider string, sizeBytes int) {
- output, err := exec.Command("wevtutil.exe", "sl", "/ms:"+strconv.Itoa(sizeBytes), providerName).CombinedOutput()
- if err != nil {
- t.Fatal("failed to set log size", err, string(output))
- }
-}
-
// Verify that all messages are read from the event log.
func TestRead(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t)
+ defer teardown()
// Publish test messages:
for k, m := range messages {
- err = log.Report(m.eventType, k, []string{m.message})
- if err != nil {
+ if err := writer.Report(m.eventType, k, []string{m.message}); err != nil {
t.Fatal(err)
}
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -219,7 +132,7 @@ func TestRead(t *testing.T) {
}
// Validate getNumberOfEventLogRecords returns the correct number of messages.
- numMessages, err := eventlogging.GetNumberOfEventLogRecords(eventlogging.Handle(log.Handle))
+ numMessages, err := eventlogging.GetNumberOfEventLogRecords(eventlogging.Handle(writer.Handle))
assert.NoError(t, err)
assert.Equal(t, len(messages), int(numMessages))
}
@@ -229,36 +142,27 @@ func TestRead(t *testing.T) {
// possible buffer so this error should not occur.
func TestFormatMessageWithLargeMessage(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t)
+ defer teardown()
- message := "Hello"
- err = log.Report(elog.Info, 1, []string{message})
- if err != nil {
+ const message = "Hello"
+ if err := writer.Report(eventlog.Info, 1, []string{message}); err != nil {
t.Fatal(err)
}
// Messages are received as UTF-16 so we must have enough space in the read
// buffer for the message, a windows newline, and a null-terminator.
- requiredBufferSize := len(message+"\r\n")*2 + 2
+ const requiredBufferSize = len(message+"\r\n")*2 + 2
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{
+ log := openEventLogging(t, 0, map[string]interface{}{
"name": providerName,
// Use a buffer smaller than what is required.
"format_buffer_size": requiredBufferSize / 2,
})
- defer teardown()
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -275,29 +179,20 @@ func TestFormatMessageWithLargeMessage(t *testing.T) {
// insert strings (the message parameters) is returned.
func TestReadUnknownEventId(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, servicesMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t, servicesMsgFile)
+ defer teardown()
- var eventID uint32 = 1000
- msg := "Test Message"
- err = log.Success(eventID, msg)
- if err != nil {
+ const eventID uint32 = 1000
+ const msg = "Test Message"
+ if err := writer.Success(eventID, msg); err != nil {
t.Fatal(err)
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -319,30 +214,20 @@ func TestReadUnknownEventId(t *testing.T) {
// of the files then the next file should be checked.
func TestReadTriesMultipleEventMsgFiles(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName,
- servicesMsgFile+";"+eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t, servicesMsgFile, eventCreateMsgFile)
+ defer teardown()
- var eventID uint32 = 1000
- msg := "Test Message"
- err = log.Success(eventID, msg)
- if err != nil {
+ const eventID uint32 = 1000
+ const msg = "Test Message"
+ if err := writer.Success(eventID, msg); err != nil {
t.Fatal(err)
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -359,34 +244,25 @@ func TestReadTriesMultipleEventMsgFiles(t *testing.T) {
// Test event messages that require more than one message parameter.
func TestReadMultiParameterMsg(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, servicesMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t, servicesMsgFile)
+ defer teardown()
// EventID observed by exporting system event log to XML and doing calculation.
// 7036
// 1073748860 = 16384 << 16 + 7036
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa385206(v=vs.85).aspx
- var eventID uint32 = 1073748860
- template := "The %s service entered the %s state."
+ const eventID uint32 = 1073748860
+ const template = "The %s service entered the %s state."
msgs := []string{"Windows Update", "running"}
- err = log.Report(elog.Info, eventID, msgs)
- if err != nil {
+ if err := writer.Report(eventlog.Info, eventID, msgs); err != nil {
t.Fatal(err)
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -406,40 +282,31 @@ func TestReadMultiParameterMsg(t *testing.T) {
func TestOpenInvalidProvider(t *testing.T) {
configureLogp()
- el := newTestEventLogging(t, map[string]interface{}{"name": "nonExistentProvider"})
- assert.NoError(t, el.Open(checkpoint.EventLogState{}), "Calling Open() on an unknown provider "+
- "should automatically open Application.")
- _, err := el.Read()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": "nonExistentProvider"})
+ defer log.Close()
+
+ _, err := log.Read()
assert.NoError(t, err)
}
// Test event messages that require no parameters.
func TestReadNoParameterMsg(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, netEventMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t, netEventMsgFile)
+ defer teardown()
- var eventID uint32 = 2147489654 // 1<<31 + 6006
- template := "The Event log service was stopped."
+ const eventID uint32 = 2147489654 // 1<<31 + 6006
+ const template = "The Event log service was stopped."
msgs := []string{}
- err = log.Report(elog.Info, eventID, msgs)
- if err != nil {
+ if err := writer.Report(eventlog.Info, eventID, msgs); err != nil {
t.Fatal(err)
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -458,33 +325,25 @@ func TestReadNoParameterMsg(t *testing.T) {
// being cleared or reset while reading.
func TestReadWhileCleared(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
-
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ writer, teardown := createLog(t)
defer teardown()
- log.Info(1, "Message 1")
- log.Info(2, "Message 2")
- lr, err := eventlog.Read()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
+
+ writer.Info(1, "Message 1")
+ writer.Info(2, "Message 2")
+ lr, err := log.Read()
assert.NoError(t, err, "Expected 2 messages but received error")
assert.Len(t, lr, 2, "Expected 2 messages")
- assert.NoError(t, eventlogging.ClearEventLog(eventlogging.Handle(log.Handle), ""))
- lr, err = eventlog.Read()
+ assert.NoError(t, eventlogging.ClearEventLog(eventlogging.Handle(writer.Handle), ""))
+ lr, err = log.Read()
assert.NoError(t, err, "Expected 0 messages but received error")
assert.Len(t, lr, 0, "Expected 0 message")
- log.Info(3, "Message 3")
- lr, err = eventlog.Read()
+ writer.Info(3, "Message 3")
+ lr, err = log.Read()
assert.NoError(t, err, "Expected 1 message but received error")
assert.Len(t, lr, 1, "Expected 1 message")
if len(lr) > 0 {
@@ -493,34 +352,25 @@ func TestReadWhileCleared(t *testing.T) {
}
// Test event messages that include less parameters than required for message
-// formating (caused a crash in previous versions)
+// formatting (caused a crash in previous versions)
func TestReadMissingParameters(t *testing.T) {
configureLogp()
- log, err := initLog(providerName, sourceName, servicesMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+ writer, teardown := createLog(t, servicesMsgFile)
+ defer teardown()
- var eventID uint32 = 1073748860
+ const eventID uint32 = 1073748860
// Missing parameters will be substituted by "(null)"
- template := "The %s service entered the (null) state."
+ const template = "The %s service entered the (null) state."
msgs := []string{"Windows Update"}
- err = log.Report(elog.Info, eventID, msgs)
- if err != nil {
+ if err := writer.Report(eventlog.Info, eventID, msgs); err != nil {
t.Fatal(err)
}
// Read messages:
- eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
- defer teardown()
+ log := openEventLogging(t, 0, map[string]interface{}{"name": providerName})
+ defer log.Close()
- records, err := eventlog.Read()
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
@@ -535,22 +385,7 @@ func TestReadMissingParameters(t *testing.T) {
strings.TrimRight(records[0].Message, "\r\n"))
}
-func newTestEventLogging(t *testing.T, options map[string]interface{}) EventLog {
- return newTestEventLog(t, newEventLogging, options)
+func openEventLogging(t *testing.T, recordID uint64, options map[string]interface{}) EventLog {
+ t.Helper()
+ return openLog(t, eventLoggingAPIName, &checkpoint.EventLogState{RecordNumber: recordID}, options)
}
-
-func setupEventLogging(t *testing.T, recordID uint64, options map[string]interface{}) (EventLog, func()) {
- return setupEventLog(t, newEventLogging, recordID, options)
-}
-
-// TODO: Add more test cases:
-// - Record number rollover (there may be an issue with this if ++ is used anywhere)
-// - Reading from a source name instead of provider name (can't be done according to docs).
-// - Persistent read mode shall support specifying a record number (or not specifying a record number).
-// -- Invalid record number based on range (should start at first record).
-// -- Invalid record number based on range timestamp match check (should start at first record).
-// -- Valid record number
-// --- Do not replay first record (it was already reported)
-// -- First read (no saved state) should return the first record (send first reported record).
-// - NewOnly read mode shall seek to end and ignore first.
-// - ReadThenExit read mode shall seek to end, read backwards, honor the EOF, then exit.
diff --git a/winlogbeat/eventlog/wineventlog_expirimental.go b/winlogbeat/eventlog/wineventlog_expirimental.go
new file mode 100644
index 00000000000..3c142707428
--- /dev/null
+++ b/winlogbeat/eventlog/wineventlog_expirimental.go
@@ -0,0 +1,299 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package eventlog
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+ "go.uber.org/multierr"
+ "golang.org/x/sys/windows"
+
+ "github.com/elastic/beats/v7/libbeat/common"
+ "github.com/elastic/beats/v7/libbeat/common/cfgwarn"
+ "github.com/elastic/beats/v7/libbeat/logp"
+ "github.com/elastic/beats/v7/winlogbeat/checkpoint"
+ win "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog"
+)
+
+const (
+ // winEventLogExpApiName is the name used to identify the Windows Event Log API
+ // as both an event type and an API.
+ winEventLogExpAPIName = "wineventlog-experimental"
+)
+
+// winEventLogExp implements the EventLog interface for reading from the Windows
+// Event Log API.
+type winEventLogExp struct {
+ config winEventLogConfig
+ query string
+ channelName string // Name of the channel from which to read.
+ file bool // Reading from file rather than channel.
+ maxRead int // Maximum number returned in one Read.
+ lastRead checkpoint.EventLogState // Record number of the last read event.
+ log *logp.Logger
+
+ iterator *win.EventIterator
+ renderer *win.Renderer
+}
+
+// Name returns the name of the event log (i.e. Application, Security, etc.).
+func (l *winEventLogExp) Name() string {
+ return l.channelName
+}
+
+func (l *winEventLogExp) Open(state checkpoint.EventLogState) error {
+ l.lastRead = state
+
+ var err error
+ l.iterator, err = win.NewEventIterator(
+ win.WithSubscriptionFactory(func() (handle win.EvtHandle, err error) {
+ return l.open(l.lastRead)
+ }),
+ win.WithBatchSize(l.maxRead))
+
+ return err
+}
+
+func (l *winEventLogExp) open(state checkpoint.EventLogState) (win.EvtHandle, error) {
+ var bookmark win.Bookmark
+ if len(state.Bookmark) > 0 {
+ var err error
+ bookmark, err = win.NewBookmarkFromXML(state.Bookmark)
+ if err != nil {
+ return win.NilHandle, err
+ }
+ defer bookmark.Close()
+ }
+
+ if l.file {
+ return l.openFile(state, bookmark)
+ }
+ return l.openChannel(bookmark)
+}
+
+func (l *winEventLogExp) openChannel(bookmark win.Bookmark) (win.EvtHandle, error) {
+ // Using a pull subscription to receive events. See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa385771(v=vs.85).aspx#pull
+ signalEvent, err := windows.CreateEvent(nil, 0, 0, nil)
+ if err != nil {
+ return win.NilHandle, err
+ }
+ defer windows.CloseHandle(signalEvent)
+
+ var flags win.EvtSubscribeFlag
+ if bookmark > 0 {
+ flags = win.EvtSubscribeStartAfterBookmark
+ } else {
+ flags = win.EvtSubscribeStartAtOldestRecord
+ }
+
+ l.log.Debugw("Using subscription query.", "winlog.query", l.query)
+ return win.Subscribe(
+ 0, // Session - nil for localhost
+ signalEvent,
+ "", // Channel - empty b/c channel is in the query
+ l.query, // Query - nil means all events
+ win.EvtHandle(bookmark), // Bookmark - for resuming from a specific event
+ flags)
+}
+
+func (l *winEventLogExp) openFile(state checkpoint.EventLogState, bookmark win.Bookmark) (win.EvtHandle, error) {
+ path := l.channelName
+
+ h, err := win.EvtQuery(0, path, "", win.EvtQueryFilePath|win.EvtQueryForwardDirection)
+ if err != nil {
+ return win.NilHandle, errors.Wrapf(err, "failed to get handle to event log file %v", path)
+ }
+
+ if bookmark > 0 {
+ l.log.Debugf("Seeking to bookmark. timestamp=%v bookmark=%v",
+ state.Timestamp, state.Bookmark)
+
+ // This seeks to the last read event and strictly validates that the
+ // bookmarked record number exists.
+ if err = win.EvtSeek(h, 0, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark|win.EvtSeekStrict); err == nil {
+ // Then we advance past the last read event to avoid sending that
+ // event again. This won't fail if we're at the end of the file.
+ err = errors.Wrap(
+ win.EvtSeek(h, 1, win.EvtHandle(bookmark), win.EvtSeekRelativeToBookmark),
+ "failed to seek past bookmarked position")
+ } else {
+ l.log.Warnf("s Failed to seek to bookmarked location in %v (error: %v). "+
+ "Recovering by reading the log from the beginning. (Did the file "+
+ "change since it was last read?)", path, err)
+ err = errors.Wrap(
+ win.EvtSeek(h, 0, 0, win.EvtSeekRelativeToFirst),
+ "failed to seek to beginning of log")
+ }
+
+ if err != nil {
+ return win.NilHandle, err
+ }
+ }
+
+ return h, err
+}
+
+func (l *winEventLogExp) Read() ([]Record, error) {
+ var records []Record
+
+ for h, ok := l.iterator.Next(); ok; h, ok = l.iterator.Next() {
+ record, err := l.processHandle(h)
+ if err != nil {
+ l.log.Warnw("Dropping event due to rendering error.", "error", err)
+ incrementMetric(dropReasons, err)
+ continue
+ }
+ records = append(records, *record)
+
+ // It has read the maximum requested number of events.
+ if len(records) >= l.maxRead {
+ return records, nil
+ }
+ }
+
+ // An error occurred while retrieving more events.
+ if err := l.iterator.Err(); err != nil {
+ return records, err
+ }
+
+ // Reader is configured to stop when there are no more events.
+ if Stop == l.config.NoMoreEvents {
+ return records, io.EOF
+ }
+
+ return records, nil
+}
+
+func (l *winEventLogExp) processHandle(h win.EvtHandle) (*Record, error) {
+ defer h.Close()
+
+ // NOTE: Render can return an error and a partial event.
+ evt, err := l.renderer.Render(h)
+ if evt == nil {
+ return nil, err
+ }
+ if err != nil {
+ evt.RenderErr = append(evt.RenderErr, err.Error())
+ }
+
+ // TODO: Need to add XML when configured.
+
+ r := &Record{
+ API: winEventLogExpAPIName,
+ Event: *evt,
+ }
+
+ if l.file {
+ r.File = l.channelName
+ }
+
+ r.Offset = checkpoint.EventLogState{
+ Name: l.channelName,
+ RecordNumber: r.RecordID,
+ Timestamp: r.TimeCreated.SystemTime,
+ }
+ if r.Offset.Bookmark, err = l.createBookmarkFromEvent(h); err != nil {
+ l.log.Warnw("Failed creating bookmark.", "error", err)
+ }
+ l.lastRead = r.Offset
+ return r, nil
+}
+
+func (l *winEventLogExp) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) {
+ bookmark, err := win.NewBookmarkFromEvent(evtHandle)
+ if err != nil {
+ return "", errors.Wrap(err, "failed to create new bookmark from event handle")
+ }
+ defer bookmark.Close()
+
+ return bookmark.XML()
+}
+
+func (l *winEventLogExp) Close() error {
+ l.log.Debug("Closing event log reader handles.")
+ return multierr.Combine(
+ l.iterator.Close(),
+ l.renderer.Close(),
+ )
+}
+
+// newWinEventLogExp creates and returns a new EventLog for reading event logs
+// using the Windows Event Log.
+func newWinEventLogExp(options *common.Config) (EventLog, error) {
+ cfgwarn.Experimental("The %s event log reader is experimental.", winEventLogExpAPIName)
+
+ c := winEventLogConfig{BatchReadSize: 512}
+ if err := readConfig(options, &c, winEventLogConfigKeys); err != nil {
+ return nil, err
+ }
+
+ queryLog := c.Name
+ isFile := false
+ if info, err := os.Stat(c.Name); err == nil && info.Mode().IsRegular() {
+ path, err := filepath.Abs(c.Name)
+ if err != nil {
+ return nil, err
+ }
+ isFile = true
+ queryLog = "file://" + path
+ }
+
+ query, err := win.Query{
+ Log: queryLog,
+ IgnoreOlder: c.SimpleQuery.IgnoreOlder,
+ Level: c.SimpleQuery.Level,
+ EventID: c.SimpleQuery.EventID,
+ Provider: c.SimpleQuery.Provider,
+ }.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ log := logp.NewLogger("wineventlog").With("channel", c.Name)
+
+ renderer, err := win.NewRenderer(win.NilHandle, log)
+ if err != nil {
+ return nil, err
+ }
+
+ l := &winEventLogExp{
+ config: c,
+ query: query,
+ channelName: c.Name,
+ file: isFile,
+ maxRead: c.BatchReadSize,
+ renderer: renderer,
+ log: log,
+ }
+
+ return l, nil
+}
+
+func init() {
+ // Register wineventlog API if it is available.
+ available, _ := win.IsAvailable()
+ if available {
+ Register(winEventLogExpAPIName, 10, newWinEventLogExp, win.Channels)
+ }
+}
diff --git a/winlogbeat/eventlog/wineventlog_test.go b/winlogbeat/eventlog/wineventlog_test.go
index c37615551a7..a9205113f78 100644
--- a/winlogbeat/eventlog/wineventlog_test.go
+++ b/winlogbeat/eventlog/wineventlog_test.go
@@ -20,122 +20,191 @@
package eventlog
import (
- "expvar"
+ "io"
+ "os/exec"
"path/filepath"
"strconv"
+ "strings"
"testing"
- elog "github.com/andrewkroh/sys/windows/svc/eventlog"
+ "github.com/andrewkroh/sys/windows/svc/eventlog"
"github.com/stretchr/testify/assert"
+
+ "github.com/elastic/beats/v7/libbeat/common"
+ "github.com/elastic/beats/v7/winlogbeat/checkpoint"
+ "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog"
)
-func TestWinEventLogBatchReadSize(t *testing.T) {
- configureLogp()
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
- if err != nil {
- t.Fatal(err)
- }
- }()
+func TestWindowsEventLogAPI(t *testing.T) {
+ testWindowsEventLog(t, winEventLogAPIName)
+}
- // Publish test messages:
- for k, m := range messages {
- err = log.Report(m.eventType, k, []string{m.message})
+func TestWindowsEventLogAPIExperimental(t *testing.T) {
+ testWindowsEventLog(t, winEventLogExpAPIName)
+}
+
+func testWindowsEventLog(t *testing.T, api string) {
+ writer, teardown := createLog(t)
+ defer teardown()
+
+ setLogSize(t, providerName, gigabyte)
+
+ // Publish large test messages.
+ const totalEvents = 1000
+ for i := 0; i < totalEvents; i++ {
+ err := writer.Report(eventlog.Info, uint32(i%1000), []string{strconv.Itoa(i) + " " + randomSentence(31800)})
if err != nil {
t.Fatal(err)
}
}
- batchReadSize := 2
- eventlog, teardown := setupWinEventLog(t, 0, map[string]interface{}{"name": providerName, "batch_read_size": batchReadSize})
- defer teardown()
-
- records, err := eventlog.Read()
- if err != nil {
- t.Fatal(err)
+ openLog := func(t testing.TB, config map[string]interface{}) EventLog {
+ return openLog(t, api, nil, config)
}
- assert.Len(t, records, batchReadSize)
-}
+ t.Run("batch_read_size_config", func(t *testing.T) {
+ const batchReadSize = 2
-// TestReadLargeBatchSize tests reading from an event log using a large
-// read_batch_size parameter. When combined with large messages this causes
-// EvtNext (wineventlog.EventRecords) to fail with RPC_S_INVALID_BOUND error.
-func TestReadLargeBatchSize(t *testing.T) {
- configureLogp()
- log, err := initLog(providerName, sourceName, eventCreateMsgFile)
- if err != nil {
- t.Fatal(err)
- }
- defer func() {
- err := uninstallLog(providerName, sourceName, log)
+ log := openLog(t, map[string]interface{}{"name": providerName, "batch_read_size": batchReadSize})
+ defer log.Close()
+
+ records, err := log.Read()
if err != nil {
t.Fatal(err)
}
- }()
- setLogSize(t, providerName, gigabyte)
+ assert.Len(t, records, batchReadSize)
+ })
- // Publish large test messages.
- totalEvents := 1000
- for i := 0; i < totalEvents; i++ {
- err = log.Report(elog.Info, uint32(i%1000), []string{strconv.Itoa(i) + " " + randomSentence(31800)})
- if err != nil {
- t.Fatal("ReportEvent error", err)
+ // Test reading from an event log using a large batch_read_size parameter.
+ // When combined with large messages this causes EvtNext to fail with
+ // RPC_S_INVALID_BOUND error. The reader should recover from the error.
+ t.Run("large_batch_read", func(t *testing.T) {
+ log := openLog(t, map[string]interface{}{"name": providerName, "batch_read_size": 1024})
+ defer log.Close()
+
+ var eventCount int
+
+ for eventCount < totalEvents {
+ records, err := log.Read()
+ if err != nil {
+ t.Fatal("read error", err)
+ }
+ if len(records) == 0 {
+ t.Fatal("read returned 0 records")
+ }
+
+ t.Logf("Read() returned %d events.", len(records))
+ eventCount += len(records)
}
- }
- eventlog, teardown := setupWinEventLog(t, 0, map[string]interface{}{"name": providerName, "batch_read_size": 1024})
- defer teardown()
+ assert.Equal(t, totalEvents, eventCount)
+ })
- var eventCount int
- for eventCount < totalEvents {
- records, err := eventlog.Read()
+ t.Run("evtx_file", func(t *testing.T) {
+ path, err := filepath.Abs("../sys/wineventlog/testdata/sysmon-9.01.evtx")
if err != nil {
- t.Fatal("read error", err)
- }
- if len(records) == 0 {
- t.Fatal("read returned 0 records")
+ t.Fatal(err)
}
- eventCount += len(records)
- }
- t.Logf("number of records returned: %v", eventCount)
+ log := openLog(t, map[string]interface{}{
+ "name": path,
+ "no_more_events": "stop",
+ })
+ defer log.Close()
- wineventlog := eventlog.(*winEventLog)
- assert.Equal(t, 1024, wineventlog.maxRead)
+ records, err := log.Read()
+
+ // This implementation returns the EOF on the next call.
+ if err == nil && api == winEventLogAPIName {
+ _, err = log.Read()
+ }
- expvar.Do(func(kv expvar.KeyValue) {
- if kv.Key == "read_errors" {
- t.Log(kv)
+ if assert.Error(t, err, "no_more_events=stop requires io.EOF to be returned") {
+ assert.Equal(t, io.EOF, err)
}
+
+ assert.Len(t, records, 32)
})
}
-func TestReadEvtxFile(t *testing.T) {
- path, err := filepath.Abs("../sys/wineventlog/testdata/sysmon-9.01.evtx")
+// ---- Utility Functions -----
+
+// createLog creates a new event log and returns a handle for writing events
+// to the log.
+func createLog(t testing.TB, messageFiles ...string) (log *eventlog.Log, tearDown func()) {
+ const name = providerName
+ const source = sourceName
+
+ messageFile := eventCreateMsgFile
+ if len(messageFiles) > 0 {
+ messageFile = strings.Join(messageFiles, ";")
+ }
+
+ existed, err := eventlog.Install(name, source, messageFile, true, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
t.Fatal(err)
}
- configureLogp()
- eventlog, teardown := setupWinEventLog(t, 0, map[string]interface{}{
- "name": path,
- })
- defer teardown()
+ if existed {
+ wineventlog.EvtClearLog(wineventlog.NilHandle, name, "")
+ }
- records, err := eventlog.Read()
+ log, err = eventlog.Open(source)
if err != nil {
+ eventlog.RemoveSource(name, source)
+ eventlog.RemoveProvider(name)
t.Fatal(err)
}
- assert.Len(t, records, 32)
+ tearDown = func() {
+ log.Close()
+ wineventlog.EvtClearLog(wineventlog.NilHandle, name, "")
+ eventlog.RemoveSource(name, source)
+ eventlog.RemoveProvider(name)
+ }
+
+ return log, tearDown
}
-func setupWinEventLog(t *testing.T, recordID uint64, options map[string]interface{}) (EventLog, func()) {
- return setupEventLog(t, newWinEventLog, recordID, options)
+// setLogSize set the maximum number of bytes that an event log can hold.
+func setLogSize(t testing.TB, provider string, sizeBytes int) {
+ output, err := exec.Command("wevtutil.exe", "sl", "/ms:"+strconv.Itoa(sizeBytes), provider).CombinedOutput()
+ if err != nil {
+ t.Fatal("Failed to set log size", err, string(output))
+ }
+}
+
+func openLog(t testing.TB, api string, state *checkpoint.EventLogState, config map[string]interface{}) EventLog {
+ cfg, err := common.NewConfigFrom(config)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var log EventLog
+ switch api {
+ case winEventLogAPIName:
+ log, err = newWinEventLog(cfg)
+ case winEventLogExpAPIName:
+ log, err = newWinEventLogExp(cfg)
+ case eventLoggingAPIName:
+ log, err = newEventLogging(cfg)
+ default:
+ t.Fatalf("Unknown API name: '%s'", api)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var eventLogState checkpoint.EventLogState
+ if state != nil {
+ eventLogState = *state
+ }
+
+ if err = log.Open(eventLogState); err != nil {
+ log.Close()
+ t.Fatal(err)
+ }
+
+ return log
}
diff --git a/winlogbeat/sys/event.go b/winlogbeat/sys/event.go
index 50df9ec18d3..d88617d8925 100644
--- a/winlogbeat/sys/event.go
+++ b/winlogbeat/sys/event.go
@@ -41,6 +41,7 @@ type Event struct {
LevelRaw uint8 `xml:"System>Level"`
TaskRaw uint16 `xml:"System>Task"`
OpcodeRaw uint8 `xml:"System>Opcode"`
+ KeywordsRaw HexInt64 `xml:"System>Keywords"`
TimeCreated TimeCreated `xml:"System>TimeCreated"`
RecordID uint64 `xml:"System>EventRecordID"`
Correlation Correlation `xml:"System>Correlation"`
@@ -96,7 +97,7 @@ type Execution struct {
ProcessorTime uint32 `xml:"ProcessorTime,attr"`
}
-// EventIdentifier is the identifer that the provider uses to identify a
+// EventIdentifier is the identifier that the provider uses to identify a
// specific event type.
type EventIdentifier struct {
Qualifiers uint16 `xml:"Qualifiers,attr"`
@@ -225,3 +226,21 @@ func (v *Version) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*v = Version(version)
return nil
}
+
+type HexInt64 uint64
+
+func (v *HexInt64) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var s string
+ if err := d.DecodeElement(&s, &start); err != nil {
+ return err
+ }
+
+ num, err := strconv.ParseInt(s, 0, 64)
+ if err != nil {
+ // Ignore invalid version values.
+ return nil
+ }
+
+ *v = HexInt64(num)
+ return nil
+}
diff --git a/winlogbeat/sys/event_test.go b/winlogbeat/sys/event_test.go
index 0684e99b473..8d0f6ee04f8 100644
--- a/winlogbeat/sys/event_test.go
+++ b/winlogbeat/sys/event_test.go
@@ -94,6 +94,7 @@ func TestXML(t *testing.T) {
EventIdentifier: EventIdentifier{ID: 91},
LevelRaw: 4,
TaskRaw: 9,
+ KeywordsRaw: 0x4000000000000004,
TimeCreated: TimeCreated{allXMLTimeCreated},
RecordID: 100,
Correlation: Correlation{"{A066CCF1-8AB3-459B-B62F-F79F957A5036}", "{85FC0930-9C49-42DA-804B-A7368104BD1B}"},
diff --git a/winlogbeat/sys/wineventlog/bookmark.go b/winlogbeat/sys/wineventlog/bookmark.go
new file mode 100644
index 00000000000..50b773d316e
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/bookmark.go
@@ -0,0 +1,81 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "syscall"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/windows"
+)
+
+// Bookmark is a handle to an event log bookmark.
+type Bookmark EvtHandle
+
+// Close closes the bookmark handle.
+func (b Bookmark) Close() error {
+ return EvtHandle(b).Close()
+}
+
+// XML returns the bookmark's value as XML.
+func (b Bookmark) XML() (string, error) {
+ var bufferUsed uint32
+
+ err := _EvtRender(NilHandle, EvtHandle(b), EvtRenderBookmark, 0, nil, &bufferUsed, nil)
+ if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return "", errors.Wrap(err, "failed to determine necessary buffer size for EvtRender")
+ }
+
+ bb := newByteBuffer()
+ bb.SetLength(int(bufferUsed * 2))
+ defer bb.free()
+
+ err = _EvtRender(NilHandle, EvtHandle(b), EvtRenderBookmark, uint32(len(bb.buf)), &bb.buf[0], &bufferUsed, nil)
+ if err != nil {
+ return "", errors.Wrap(err, "failed to render bookmark XML")
+ }
+
+ return UTF16BytesToString(bb.buf)
+}
+
+// NewBookmarkFromEvent returns a Bookmark pointing to the given event record.
+// The returned handle must be closed.
+func NewBookmarkFromEvent(eventHandle EvtHandle) (Bookmark, error) {
+ h, err := _EvtCreateBookmark(nil)
+ if err != nil {
+ return 0, err
+ }
+ if err = _EvtUpdateBookmark(h, eventHandle); err != nil {
+ h.Close()
+ return 0, err
+ }
+ return Bookmark(h), nil
+}
+
+// NewBookmarkFromXML returns a Bookmark created from an XML bookmark.
+// The returned handle must be closed.
+func NewBookmarkFromXML(xml string) (Bookmark, error) {
+ utf16, err := syscall.UTF16PtrFromString(xml)
+ if err != nil {
+ return 0, err
+ }
+ h, err := _EvtCreateBookmark(utf16)
+ return Bookmark(h), err
+}
diff --git a/winlogbeat/sys/wineventlog/bookmark_test.go b/winlogbeat/sys/wineventlog/bookmark_test.go
new file mode 100644
index 00000000000..34a443a4184
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/bookmark_test.go
@@ -0,0 +1,88 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBookmark(t *testing.T) {
+ log := openLog(t, security4752File)
+ defer log.Close()
+
+ evtHandle := mustNextHandle(t, log)
+ defer evtHandle.Close()
+
+ t.Run("NewBookmarkFromEvent", func(t *testing.T) {
+ bookmark, err := NewBookmarkFromEvent(evtHandle)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ assert.NoError(t, bookmark.Close())
+ }()
+
+ xml, err := bookmark.XML()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Contains(t, xml, "")
+ })
+
+ t.Run("NewBookmarkFromXML", func(t *testing.T) {
+ const savedBookmarkXML = `
+
+
+`
+
+ bookmark, err := NewBookmarkFromXML(savedBookmarkXML)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ assert.NoError(t, bookmark.Close())
+ }()
+
+ xml, err := bookmark.XML()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Ignore whitespace differences.
+ normalizer := strings.NewReplacer(" ", "", "\r\n", "", "\n", "")
+ assert.Equal(t, normalizer.Replace(savedBookmarkXML), normalizer.Replace(xml))
+ })
+
+ t.Run("NewBookmarkFromEvent_invalid", func(t *testing.T) {
+ bookmark, err := NewBookmarkFromEvent(NilHandle)
+ assert.Error(t, err)
+ assert.Zero(t, bookmark)
+ })
+
+ t.Run("NewBookmarkFromXML_invalid", func(t *testing.T) {
+ bookmark, err := NewBookmarkFromXML("{Not XML}")
+ assert.Error(t, err)
+ assert.Zero(t, bookmark)
+ })
+}
diff --git a/winlogbeat/sys/wineventlog/bufferpool.go b/winlogbeat/sys/wineventlog/bufferpool.go
new file mode 100644
index 00000000000..7ed6038f494
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/bufferpool.go
@@ -0,0 +1,113 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package wineventlog
+
+import (
+ "sync"
+
+ "github.com/elastic/beats/v7/winlogbeat/sys"
+)
+
+// bufferPool contains a pool of byteBuffer objects.
+var bufferPool = sync.Pool{
+ New: func() interface{} { return &byteBuffer{buf: make([]byte, 1024)} },
+}
+
+// byteBuffer is an expandable buffer backed by a byte slice.
+type byteBuffer struct {
+ buf []byte
+ offset int
+}
+
+// newByteBuffer return a byteBuffer from the pool. The returned value must
+// be released with free().
+func newByteBuffer() *byteBuffer {
+ b := bufferPool.Get().(*byteBuffer)
+ b.Reset()
+ return b
+}
+
+// free returns the byteBuffer to the pool.
+func (b *byteBuffer) free() {
+ if b == nil {
+ return
+ }
+ bufferPool.Put(b)
+}
+
+// Write appends the contents of p to the buffer, growing the buffer as needed.
+// The return value is the length of p; err is always nil. This implements
+// io.Writer.
+func (b *byteBuffer) Write(p []byte) (int, error) {
+ if len(b.buf) < b.offset+len(p) {
+ // Create a buffer larger than needed so we don't spend lots of time
+ // allocating and copying.
+ spaceNeeded := len(b.buf) - b.offset + len(p)
+ largerBuf := make([]byte, 2*len(b.buf)+spaceNeeded)
+ copy(largerBuf, b.buf[:b.offset])
+ b.buf = largerBuf
+ }
+ n := copy(b.buf[b.offset:], p)
+ b.offset += n
+ return n, nil
+}
+
+// Reset resets the buffer to be empty. It retains the same underlying storage
+// capacity.
+func (b *byteBuffer) Reset() {
+ b.offset = 0
+ b.buf = b.buf[:cap(b.buf)]
+}
+
+// Bytes returns a slice of length b.Len() holding the bytes that have been
+// written to the buffer.
+func (b *byteBuffer) Bytes() []byte {
+ return b.buf[:b.offset]
+}
+
+// Len returns the number of bytes that have been written to the buffer.
+func (b *byteBuffer) Len() int {
+ return b.offset
+}
+
+// SetLength resets the buffer to a specific length. It may allocate a new
+// underlying buffer discarding any existing contents.
+func (b *byteBuffer) SetLength(n int) {
+ b.offset = n
+
+ if n > cap(b.buf) {
+ // Allocate new larger buffer with len=n.
+ b.buf = make([]byte, n)
+ } else {
+ b.buf = b.buf[:n]
+ }
+}
+
+// UTF16BytesToString converts the given UTF-16 bytes to a string.
+func UTF16BytesToString(b []byte) (string, error) {
+ // Use space from the byteBuffer pool as working memory for the conversion.
+ bb := newByteBuffer()
+ defer bb.free()
+
+ if err := sys.UTF16ToUTF8Bytes(b, bb); err != nil {
+ return "", err
+ }
+
+ // This copies the UTF-8 bytes to create a string.
+ return string(bb.Bytes()), nil
+}
diff --git a/winlogbeat/sys/wineventlog/doc.go b/winlogbeat/sys/wineventlog/doc.go
index 7c3936a0fb7..09c8685c331 100644
--- a/winlogbeat/sys/wineventlog/doc.go
+++ b/winlogbeat/sys/wineventlog/doc.go
@@ -15,10 +15,12 @@
// specific language governing permissions and limitations
// under the License.
-/*
-Package wineventlog provides access to the Windows Event Log API used in
-all versions of Windows since Vista (i.e. Windows 7+ and Windows Server 2008+).
-This is distinct from the Event Logging API that was used in Windows XP,
-Windows Server 2003, and Windows 2000.
-*/
+// Package wineventlog provides access to the Windows Event Log API used in
+// all versions of Windows since Vista (i.e. Windows 7+ and Windows Server 2008+).
+// This is distinct from the Event Logging API that was used in Windows XP,
+// Windows Server 2003, and Windows 2000.
package wineventlog
+
+// Add -trace to enable debug prints around syscalls.
+//go:generate go get golang.org/x/sys/windows/mkwinsyscall
+//go:generate $GOPATH/bin/mkwinsyscall.exe -systemdll -output zsyscall_windows.go syscall_windows.go
diff --git a/winlogbeat/sys/wineventlog/format_message.go b/winlogbeat/sys/wineventlog/format_message.go
new file mode 100644
index 00000000000..a8e376127ea
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/format_message.go
@@ -0,0 +1,102 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/windows"
+)
+
+// getMessageStringFromHandle returns the message for the given eventHandle.
+func getMessageStringFromHandle(metadata *PublisherMetadata, eventHandle EvtHandle, values []EvtVariant) (string, error) {
+ return getMessageString(metadata, eventHandle, 0, values)
+}
+
+// getMessageStringFromMessageID returns the message associated with the given
+// message ID.
+func getMessageStringFromMessageID(metadata *PublisherMetadata, messageID uint32, values []EvtVariant) (string, error) {
+ return getMessageString(metadata, NilHandle, messageID, values)
+}
+
+// getMessageString returns an event's message. Don't use this directly. Instead
+// use either getMessageStringFromHandle or getMessageStringFromMessageID.
+func getMessageString(metadata *PublisherMetadata, eventHandle EvtHandle, messageID uint32, values []EvtVariant) (string, error) {
+ var flags EvtFormatMessageFlag
+ if eventHandle > 0 {
+ flags = EvtFormatMessageEvent
+ } else if messageID > 0 {
+ flags = EvtFormatMessageId
+ }
+
+ metadataHandle := NilHandle
+ if metadata != nil {
+ metadataHandle = metadata.Handle
+ }
+
+ return evtFormatMessage(metadataHandle, eventHandle, messageID, values, flags)
+}
+
+// getEventXML returns all data in the event as XML.
+func getEventXML(metadata *PublisherMetadata, eventHandle EvtHandle) (string, error) {
+ metadataHandle := NilHandle
+ if metadata != nil {
+ metadataHandle = metadata.Handle
+ }
+ return evtFormatMessage(metadataHandle, eventHandle, 0, nil, EvtFormatMessageXml)
+}
+
+// evtFormatMessage uses EvtFormatMessage to generate a string.
+func evtFormatMessage(metadataHandle EvtHandle, eventHandle EvtHandle, messageID uint32, values []EvtVariant, messageFlag EvtFormatMessageFlag) (string, error) {
+ var (
+ valuesCount = uint32(len(values))
+ valuesPtr uintptr
+ )
+ if len(values) > 0 {
+ valuesPtr = uintptr(unsafe.Pointer(&values[0]))
+ }
+
+ // Determine the buffer size needed (given in WCHARs).
+ var bufferUsed uint32
+ err := _EvtFormatMessage(metadataHandle, eventHandle, messageID, valuesCount, valuesPtr, messageFlag, 0, nil, &bufferUsed)
+ if err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return "", errors.Wrap(err, "failed in EvtFormatMessage")
+ }
+
+ // Get a buffer from the pool and adjust its length.
+ bb := newByteBuffer()
+ defer bb.free()
+ bb.SetLength(int(bufferUsed * 2))
+
+ err = _EvtFormatMessage(metadataHandle, eventHandle, messageID, valuesCount, valuesPtr, messageFlag, uint32(len(bb.buf)/2), &bb.buf[0], &bufferUsed)
+ if err != nil {
+ switch err {
+ // Ignore some errors so it can tolerate missing or mismatched parameter values.
+ case windows.ERROR_EVT_UNRESOLVED_VALUE_INSERT:
+ case windows.ERROR_EVT_UNRESOLVED_PARAMETER_INSERT:
+ case windows.ERROR_EVT_MAX_INSERTS_REACHED:
+ default:
+ return "", errors.Wrap(err, "failed in EvtFormatMessage")
+ }
+ }
+
+ return UTF16BytesToString(bb.buf)
+}
diff --git a/winlogbeat/sys/wineventlog/format_message_test.go b/winlogbeat/sys/wineventlog/format_message_test.go
new file mode 100644
index 00000000000..492061f7a28
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/format_message_test.go
@@ -0,0 +1,153 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFormatMessage(t *testing.T) {
+ log := openLog(t, security4752File)
+ defer log.Close()
+
+ evtHandle := mustNextHandle(t, log)
+ defer evtHandle.Close()
+
+ publisherMetadata, err := NewPublisherMetadata(NilHandle, "Microsoft-Windows-Security-Auditing")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer publisherMetadata.Close()
+
+ t.Run("getMessageStringFromHandle", func(t *testing.T) {
+ t.Run("no_metadata", func(t *testing.T) {
+ // Metadata is required unless the events were forwarded with "RenderedText".
+ _, err := getMessageStringFromHandle(nil, evtHandle, nil)
+ assert.Error(t, err)
+ })
+
+ t.Run("with_metadata", func(t *testing.T) {
+ // When no values are passed in then event data from the event is
+ // substituted into the message.
+ msg, err := getMessageStringFromHandle(publisherMetadata, evtHandle, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Contains(t, msg, "CN=Administrator,CN=Users,DC=TEST,DC=SAAS")
+ })
+
+ t.Run("custom_values", func(t *testing.T) {
+ // Substitute custom values into the message.
+ msg, err := getMessageStringFromHandle(publisherMetadata, evtHandle, templateInserts.Slice())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Contains(t, msg, `{{eventParam $ 2}}`)
+
+ // NOTE: In this test case I noticed the messages contains
+ // "Logon ID: 0x0"
+ // but it should contain
+ // "Logon ID: {{eventParam $ 9}}"
+ //
+ // This may mean that certain windows.GUID values cannot be
+ // substituted with string values. So we shouldn't rely on this
+ // method to create text/templates. Instead we can use the
+ // getMessageStringFromMessageID (see test below) that works as
+ // expected.
+ assert.NotContains(t, msg, `{{eventParam $ 9}}`)
+ })
+ })
+
+ t.Run("getMessageStringFromMessageID", func(t *testing.T) {
+ // Get the message ID for event 4752.
+ itr, err := publisherMetadata.EventMetadataIterator()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer itr.Close()
+
+ var messageID uint32
+ for itr.Next() {
+ id, err := itr.EventID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if id == 4752 {
+ messageID, err = itr.MessageID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+
+ if messageID == 0 {
+ t.Fatal("message ID for event 4752 not found")
+ }
+
+ t.Run("no_metadata", func(t *testing.T) {
+ // Metadata is required to find the message file.
+ _, err := getMessageStringFromMessageID(nil, messageID, nil)
+ assert.Error(t, err)
+ })
+
+ t.Run("with_metadata", func(t *testing.T) {
+ // When no values are passed in then the raw message is returned
+ // with place-holders like %1 and %2.
+ msg, err := getMessageStringFromMessageID(publisherMetadata, messageID, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Contains(t, msg, "%9")
+ })
+
+ t.Run("custom_values", func(t *testing.T) {
+ msg, err := getMessageStringFromMessageID(publisherMetadata, messageID, templateInserts.Slice())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Contains(t, msg, `{{eventParam $ 2}}`)
+ assert.Contains(t, msg, `{{eventParam $ 9}}`)
+ })
+ })
+
+ t.Run("getEventXML", func(t *testing.T) {
+ t.Run("no_metadata", func(t *testing.T) {
+ // It needs the metadata handle to add the message to the XML.
+ _, err := getEventXML(nil, evtHandle)
+ assert.Error(t, err)
+ })
+
+ t.Run("with_metadata", func(t *testing.T) {
+ xml, err := getEventXML(publisherMetadata, evtHandle)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.True(t, strings.HasPrefix(xml, ""))
+ })
+ })
+}
diff --git a/winlogbeat/sys/wineventlog/iterator.go b/winlogbeat/sys/wineventlog/iterator.go
new file mode 100644
index 00000000000..26492fe96b0
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/iterator.go
@@ -0,0 +1,207 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "sync"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ evtNextMaxHandles = 1024
+ evtNextDefaultHandles = 512
+)
+
+// EventIterator provides an iterator to read events from a log. It takes the
+// place of calling EvtNext directly.
+type EventIterator struct {
+ subscriptionFactory SubscriptionFactory // Factory for producing a new subscription handle.
+ subscription EvtHandle // Handle from EvtQuery or EvtSubscribe.
+ batchSize uint32 // Number of handles to request by default.
+ handles [evtNextMaxHandles]EvtHandle // Handles returned by EvtNext.
+ lastErr error // Last error returned by EvtNext.
+ active []EvtHandle // Slice of the handles array containing the valid unread handles.
+ mutex sync.Mutex // Mutex to enable parallel iteration.
+
+ // For testing purposes to be able to mock EvtNext.
+ evtNext func(resultSet EvtHandle, eventArraySize uint32, eventArray *EvtHandle, timeout uint32, flags uint32, numReturned *uint32) (err error)
+}
+
+// SubscriptionFactory produces a handle from EvtQuery or EvtSubscribe that
+// points to the next unread event. Provide a factory to enable automatic
+// recover of certain errors.
+type SubscriptionFactory func() (EvtHandle, error)
+
+// EventIteratorOption represents a configuration of for the construction of
+// the EventIterator.
+type EventIteratorOption func(*EventIterator)
+
+// WithSubscriptionFactory configures a SubscriptionFactory for the iterator to
+// use to create a subscription handle.
+func WithSubscriptionFactory(factory SubscriptionFactory) EventIteratorOption {
+ return func(itr *EventIterator) {
+ itr.subscriptionFactory = factory
+ }
+}
+
+// WithSubscription configures the iterator with an existing subscription handle.
+func WithSubscription(subscription EvtHandle) EventIteratorOption {
+ return func(itr *EventIterator) {
+ itr.subscription = subscription
+ }
+}
+
+// WithBatchSize configures the number of handles the iterator will request
+// when calling EvtNext. Valid batch sizes range on [1, 1024].
+func WithBatchSize(size int) EventIteratorOption {
+ return func(itr *EventIterator) {
+ if size > 0 {
+ itr.batchSize = uint32(size)
+ }
+ if size > evtNextMaxHandles {
+ itr.batchSize = evtNextMaxHandles
+ }
+ }
+}
+
+// NewEventIterator creates an iterator to read event handles from a subscription.
+// The iterator is thread-safe.
+func NewEventIterator(opts ...EventIteratorOption) (*EventIterator, error) {
+ itr := &EventIterator{
+ batchSize: evtNextDefaultHandles,
+ evtNext: _EvtNext,
+ }
+
+ for _, opt := range opts {
+ opt(itr)
+ }
+
+ if itr.subscriptionFactory == nil && itr.subscription == NilHandle {
+ return nil, errors.New("either a subscription or subscription factory is required")
+ }
+
+ if itr.subscription == NilHandle {
+ handle, err := itr.subscriptionFactory()
+ if err != nil {
+ return nil, err
+ }
+ itr.subscription = handle
+ }
+
+ return itr, nil
+}
+
+// Next advances the iterator to the next handle. After Next returns false, the
+// Err() method will return any error that occurred during iteration, except
+// that if it was windows.ERROR_NO_MORE_ITEMS, Err() will return nil and you
+// may call Next() again later to check if new events are available.
+func (itr *EventIterator) Next() (EvtHandle, bool) {
+ itr.mutex.Lock()
+ defer itr.mutex.Unlock()
+
+ if itr.lastErr != nil {
+ return NilHandle, false
+ }
+
+ if !itr.empty() {
+ itr.active = itr.active[1:]
+ }
+
+ if itr.empty() && !itr.moreHandles() {
+ return NilHandle, false
+ }
+
+ return itr.active[0], true
+}
+
+// empty returns true when there are no more handles left to read from memory.
+func (itr *EventIterator) empty() bool {
+ return len(itr.active) == 0
+}
+
+// moreHandles fetches more handles using EvtNext. It returns true if it
+// successfully fetched more handles.
+func (itr *EventIterator) moreHandles() bool {
+ batchSize := itr.batchSize
+
+ for batchSize > 0 {
+ var numReturned uint32
+
+ err := itr.evtNext(itr.subscription, batchSize, &itr.handles[0], 0, 0, &numReturned)
+ switch err {
+ case nil:
+ itr.lastErr = nil
+ itr.active = itr.handles[:numReturned]
+ case windows.ERROR_NO_MORE_ITEMS, windows.ERROR_INVALID_OPERATION:
+ case windows.RPC_S_INVALID_BOUND:
+ // Attempt automated recovery if we have a factory.
+ if itr.subscriptionFactory != nil {
+ itr.subscription.Close()
+ itr.subscription, err = itr.subscriptionFactory()
+ if err != nil {
+ itr.lastErr = errors.Wrap(err, "failed in EvtNext while trying to "+
+ "recover from RPC_S_INVALID_BOUND error")
+ return false
+ }
+
+ // Reduce batch size and try again.
+ batchSize = batchSize / 2
+ continue
+ } else {
+ itr.lastErr = errors.Wrap(err, "failed in EvtNext (try "+
+ "reducing the batch size or providing a subscription "+
+ "factory for automatic recovery)")
+ }
+ default:
+ itr.lastErr = err
+ }
+
+ break
+ }
+
+ return !itr.empty()
+}
+
+// Err returns the first non-ERROR_NO_MORE_ITEMS error encountered by the
+// EventIterator.
+//
+// Some Windows versions will fail with windows.RPC_S_INVALID_BOUND when the
+// batch size is too large. If this occurs you can recover by closing the
+// iterator, creating a new subscription, seeking to the next unread event, and
+// creating a new EventIterator with a smaller batch size.
+func (itr *EventIterator) Err() error {
+ itr.mutex.Lock()
+ defer itr.mutex.Unlock()
+
+ return itr.lastErr
+}
+
+// Close closes the subscription handle and any unread event handles.
+func (itr *EventIterator) Close() error {
+ itr.mutex.Lock()
+ defer itr.mutex.Unlock()
+
+ for _, h := range itr.active {
+ h.Close()
+ }
+ return itr.subscription.Close()
+}
diff --git a/winlogbeat/sys/wineventlog/iterator_test.go b/winlogbeat/sys/wineventlog/iterator_test.go
new file mode 100644
index 00000000000..e83a5ef5926
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/iterator_test.go
@@ -0,0 +1,267 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/sys/windows"
+)
+
+func TestEventIterator(t *testing.T) {
+ writer, tearDown := createLog(t)
+ defer tearDown()
+
+ const eventCount = 1500
+ for i := 0; i < eventCount; i++ {
+ if err := writer.Info(1, "Test message "+strconv.Itoa(i+1)); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // Validate the assumption that 1024 is the max number of handles supported
+ // by EvtNext.
+ t.Run("max_handles_assumption", func(t *testing.T) {
+ log := openLog(t, winlogbeatTestLogName)
+ defer log.Close()
+
+ var (
+ numReturned uint32
+ handles = [evtNextMaxHandles + 1]EvtHandle{}
+ )
+
+ // Too many handles.
+ err := _EvtNext(log, uint32(len(handles)), &handles[0], 0, 0, &numReturned)
+ assert.Equal(t, windows.ERROR_INVALID_PARAMETER, err)
+
+ // The max number of handles.
+ err = _EvtNext(log, evtNextMaxHandles, &handles[0], 0, 0, &numReturned)
+ if assert.NoError(t, err) {
+ for _, h := range handles[:numReturned] {
+ h.Close()
+ }
+ }
+ })
+
+ t.Run("no_subscription", func(t *testing.T) {
+ _, err := NewEventIterator()
+ assert.Error(t, err)
+ })
+
+ t.Run("with_subscription", func(t *testing.T) {
+ log := openLog(t, winlogbeatTestLogName)
+ defer log.Close()
+
+ itr, err := NewEventIterator(WithSubscription(log))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { assert.NoError(t, itr.Close()) }()
+
+ assert.Nil(t, itr.subscriptionFactory)
+ assert.NotEqual(t, NilHandle, itr.subscription)
+ })
+
+ t.Run("with_subscription_factory", func(t *testing.T) {
+ factory := func() (handle EvtHandle, err error) {
+ return openLog(t, winlogbeatTestLogName), nil
+ }
+ itr, err := NewEventIterator(WithSubscriptionFactory(factory))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { assert.NoError(t, itr.Close()) }()
+
+ assert.NotNil(t, itr.subscriptionFactory)
+ assert.NotEqual(t, NilHandle, itr.subscription)
+ })
+
+ t.Run("with_batch_size", func(t *testing.T) {
+ log := openLog(t, winlogbeatTestLogName)
+ defer log.Close()
+
+ t.Run("default", func(t *testing.T) {
+ itr, err := NewEventIterator(WithSubscription(log))
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.EqualValues(t, evtNextDefaultHandles, itr.batchSize)
+ })
+
+ t.Run("custom", func(t *testing.T) {
+ itr, err := NewEventIterator(WithSubscription(log), WithBatchSize(128))
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.EqualValues(t, 128, itr.batchSize)
+ })
+
+ t.Run("too_small", func(t *testing.T) {
+ itr, err := NewEventIterator(WithSubscription(log), WithBatchSize(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.EqualValues(t, evtNextDefaultHandles, itr.batchSize)
+ })
+
+ t.Run("too_big", func(t *testing.T) {
+ itr, err := NewEventIterator(WithSubscription(log), WithBatchSize(evtNextMaxHandles+1))
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.EqualValues(t, evtNextMaxHandles, itr.batchSize)
+ })
+ })
+
+ t.Run("iterate", func(t *testing.T) {
+ log := openLog(t, winlogbeatTestLogName)
+ defer log.Close()
+
+ itr, err := NewEventIterator(WithSubscription(log), WithBatchSize(13))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { assert.NoError(t, itr.Close()) }()
+
+ var iterateCount int
+ for h, ok := itr.Next(); ok; h, ok = itr.Next() {
+ h.Close()
+
+ if !assert.NotZero(t, h) {
+ return
+ }
+
+ iterateCount++
+ }
+ if err := itr.Err(); err != nil {
+ t.Fatal(err)
+ }
+
+ assert.EqualValues(t, eventCount, iterateCount)
+ })
+
+ // Check for regressions of https://github.com/elastic/beats/issues/3076
+ // where EvtNext fails reading batch of large events.
+ //
+ // Note: As of 2020-03 Windows 2019 no longer exhibits this behavior.
+ // Instead EvtNext simply returns fewer handles that the requested size.
+ t.Run("rpc_error", func(t *testing.T) {
+ log := openLog(t, winlogbeatTestLogName)
+ defer log.Close()
+
+ // Mock the behavior to simplify testing since it's not reproducible
+ // on all Windows versions.
+ mockEvtNext := func(resultSet EvtHandle, eventArraySize uint32, eventArray *EvtHandle, timeout uint32, flags uint32, numReturned *uint32) (err error) {
+ if eventArraySize > 3 {
+ return windows.RPC_S_INVALID_BOUND
+ }
+ return _EvtNext(resultSet, eventArraySize, eventArray, timeout, flags, numReturned)
+ }
+
+ // If you create the iterator with only a subscription handle then
+ // no recovery is possible without data loss.
+ t.Run("no_recovery", func(t *testing.T) {
+ itr, err := NewEventIterator(WithSubscription(log))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { assert.NoError(t, itr.Close()) }()
+
+ itr.evtNext = mockEvtNext
+
+ h, ok := itr.Next()
+ assert.False(t, ok)
+ assert.Zero(t, h)
+ if assert.Error(t, itr.Err()) {
+ assert.Contains(t, itr.Err().Error(), "try reducing the batch size")
+ assert.Equal(t, windows.RPC_S_INVALID_BOUND, errors.Cause(itr.Err()))
+ }
+ })
+
+ t.Run("automated_recovery", func(t *testing.T) {
+ var numFactoryInvocations int
+ var bookmark Bookmark
+
+ // Create a proper subscription factor that resumes from the last
+ // read position by using bookmarks.
+ factory := func() (handle EvtHandle, err error) {
+ numFactoryInvocations++
+ log := openLog(t, winlogbeatTestLogName)
+
+ if bookmark != 0 {
+ // Seek to bookmark location.
+ err := EvtSeek(log, 0, EvtHandle(bookmark), EvtSeekRelativeToBookmark|EvtSeekStrict)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Seek to one event after bookmark (unread position).
+ if err = EvtSeek(log, 1, NilHandle, EvtSeekRelativeToCurrent); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ return log, err
+ }
+
+ itr, err := NewEventIterator(WithSubscriptionFactory(factory), WithBatchSize(7))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { assert.NoError(t, itr.Close()) }()
+
+ // Mock the EvtNext to cause the the RPC_S_INVALID_BOUND error.
+ itr.evtNext = mockEvtNext
+
+ var iterateCount int
+ for h, ok := itr.Next(); ok; h, ok = itr.Next() {
+ func() {
+ defer h.Close()
+
+ if !assert.NotZero(t, h) {
+ t.FailNow()
+ }
+
+ // Store last read position.
+ if bookmark != 0 {
+ bookmark.Close()
+ }
+ bookmark, err = NewBookmarkFromEvent(h)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ iterateCount++
+ }()
+ }
+ if err := itr.Err(); err != nil {
+ t.Fatal(err)
+ }
+
+ // Validate that the factory has been used to recover and
+ // that we received all the events.
+ assert.Greater(t, numFactoryInvocations, 1)
+ assert.EqualValues(t, eventCount, iterateCount)
+ })
+ })
+}
diff --git a/winlogbeat/sys/wineventlog/metadata_store.go b/winlogbeat/sys/wineventlog/metadata_store.go
new file mode 100644
index 00000000000..c16acf9d14f
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/metadata_store.go
@@ -0,0 +1,465 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "strconv"
+ "strings"
+ "sync"
+ "text/template"
+
+ "github.com/pkg/errors"
+ "go.uber.org/multierr"
+
+ "github.com/elastic/beats/v7/libbeat/logp"
+ "github.com/elastic/beats/v7/winlogbeat/sys"
+)
+
+var (
+ // eventDataNameTransform removes spaces from parameter names.
+ eventDataNameTransform = strings.NewReplacer(" ", "_")
+
+ // eventMessageTemplateFuncs contains functions for use in message templates.
+ eventMessageTemplateFuncs = template.FuncMap{
+ "eventParam": eventParam,
+ }
+)
+
+// publisherMetadataStore stores metadata from a publisher.
+type publisherMetadataStore struct {
+ Metadata *PublisherMetadata // Handle to the publisher metadata. May be nil.
+ Keywords map[int64]string // Keyword bit mask to keyword name.
+ Opcodes map[uint8]string // Opcode value to name.
+ Levels map[uint8]string // Level value to name.
+ Tasks map[uint16]string // Task value to name.
+
+ // Event ID to event metadata (message and event data param names).
+ Events map[uint16]*eventMetadata
+ // Event ID to map of fingerprints to event metadata. The fingerprint value
+ // is hash of the event data parameters count and types.
+ EventFingerprints map[uint16]map[uint64]*eventMetadata
+
+ mutex sync.RWMutex
+ log *logp.Logger
+}
+
+func newPublisherMetadataStore(session EvtHandle, provider string, log *logp.Logger) (*publisherMetadataStore, error) {
+ md, err := NewPublisherMetadata(session, provider)
+ if err != nil {
+ return nil, err
+ }
+ store := &publisherMetadataStore{
+ Metadata: md,
+ EventFingerprints: map[uint16]map[uint64]*eventMetadata{},
+ log: log.With("publisher", provider),
+ }
+
+ // Query the provider metadata to build an in-memory cache of the
+ // information to optimize event reading.
+ err = multierr.Combine(
+ store.initKeywords(),
+ store.initOpcodes(),
+ store.initLevels(),
+ store.initTasks(),
+ store.initEvents(),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return store, nil
+}
+
+// newEmptyPublisherMetadataStore creates an empty metadata store for cases
+// where no local publisher metadata exists.
+func newEmptyPublisherMetadataStore(provider string, log *logp.Logger) *publisherMetadataStore {
+ return &publisherMetadataStore{
+ Keywords: map[int64]string{},
+ Opcodes: map[uint8]string{},
+ Levels: map[uint8]string{},
+ Tasks: map[uint16]string{},
+ Events: map[uint16]*eventMetadata{},
+ EventFingerprints: map[uint16]map[uint64]*eventMetadata{},
+ log: log.With("publisher", provider, "empty", true),
+ }
+}
+
+func (s *publisherMetadataStore) initKeywords() error {
+ keywords, err := s.Metadata.Keywords()
+ if err != nil {
+ return err
+ }
+
+ s.Keywords = make(map[int64]string, len(keywords))
+ for _, keywordMeta := range keywords {
+ val := keywordMeta.Name
+ if val == "" {
+ val = keywordMeta.Message
+ }
+ s.Keywords[int64(keywordMeta.Mask)] = val
+ }
+ return nil
+}
+
+func (s *publisherMetadataStore) initOpcodes() error {
+ opcodes, err := s.Metadata.Opcodes()
+ if err != nil {
+ return err
+ }
+ s.Opcodes = make(map[uint8]string, len(opcodes))
+ for _, opcodeMeta := range opcodes {
+ val := opcodeMeta.Message
+ if val == "" {
+ val = opcodeMeta.Name
+ }
+ s.Opcodes[uint8(opcodeMeta.Mask)] = val
+ }
+ return nil
+}
+
+func (s *publisherMetadataStore) initLevels() error {
+ levels, err := s.Metadata.Levels()
+ if err != nil {
+ return err
+ }
+
+ s.Levels = make(map[uint8]string, len(levels))
+ for _, levelMeta := range levels {
+ val := levelMeta.Name
+ if val == "" {
+ val = levelMeta.Message
+ }
+ s.Levels[uint8(levelMeta.Mask)] = val
+ }
+ return nil
+}
+
+func (s *publisherMetadataStore) initTasks() error {
+ tasks, err := s.Metadata.Tasks()
+ if err != nil {
+ return err
+ }
+ s.Tasks = make(map[uint16]string, len(tasks))
+ for _, taskMeta := range tasks {
+ val := taskMeta.Message
+ if val == "" {
+ val = taskMeta.Name
+ }
+ s.Tasks[uint16(taskMeta.Mask)] = val
+ }
+ return nil
+}
+
+func (s *publisherMetadataStore) initEvents() error {
+ itr, err := s.Metadata.EventMetadataIterator()
+ if err != nil {
+ return err
+ }
+ defer itr.Close()
+
+ s.Events = map[uint16]*eventMetadata{}
+ for itr.Next() {
+ evt, err := newEventMetadataFromPublisherMetadata(itr, s.Metadata)
+ if err != nil {
+ s.log.Warnw("Failed to read event metadata from publisher. Continuing to next event.",
+ "error", err)
+ continue
+ }
+ s.Events[evt.EventID] = evt
+ }
+ return itr.Err()
+}
+
+func (s *publisherMetadataStore) getEventMetadata(eventID uint16, eventDataFingerprint uint64, eventHandle EvtHandle) *eventMetadata {
+ // Use a read lock to get a cached value.
+ s.mutex.RLock()
+ fingerprints, found := s.EventFingerprints[eventID]
+ if found {
+ em, found := fingerprints[eventDataFingerprint]
+ if found {
+ s.mutex.RUnlock()
+ return em
+ }
+ }
+
+ // Elevate to write lock.
+ s.mutex.RUnlock()
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ fingerprints, found = s.EventFingerprints[eventID]
+ if !found {
+ fingerprints = map[uint64]*eventMetadata{}
+ s.EventFingerprints[eventID] = fingerprints
+ }
+
+ em, found := fingerprints[eventDataFingerprint]
+ if found {
+ return em
+ }
+
+ // To ensure we always match the correct event data parameter names to
+ // values we will rely a fingerprint made of the number of event data
+ // properties and each of their EvtVariant type values.
+ //
+ // The first time we observe a new fingerprint value we get the XML
+ // representation of the event in order to know the parameter names.
+ // If they turn out to match the values that we got from the provider's
+ // metadata then we just associate the fingerprint with a pointer to the
+ // providers metadata for the event ID.
+
+ defaultEM := s.Events[eventID]
+
+ // Use XML to get the parameters names.
+ em, err := newEventMetadataFromEventHandle(s.Metadata, eventHandle)
+ if err != nil {
+ s.log.Debugw("Failed to make event metadata from event handle. Will "+
+ "use default event metadata from the publisher.",
+ "provider", s.Metadata.Name,
+ "event_id", eventID,
+ "fingerprint", eventDataFingerprint,
+ "error", err)
+
+ if defaultEM != nil {
+ fingerprints[eventDataFingerprint] = defaultEM
+ }
+ return defaultEM
+ }
+
+ // Are the parameters the same as what the provider metadata listed?
+ // (This ignores the message values.)
+ if em.equal(defaultEM) {
+ fingerprints[eventDataFingerprint] = defaultEM
+ return defaultEM
+ }
+
+ // If we couldn't get a message from the event handle use the one
+ // from the installed provider metadata.
+ if defaultEM != nil && em.MsgStatic == "" && em.MsgTemplate == nil {
+ em.MsgStatic = defaultEM.MsgStatic
+ em.MsgTemplate = defaultEM.MsgTemplate
+ }
+
+ s.log.Debugw("Obtained unique event metadata from event handle. "+
+ "It differed from what was listed in the publisher's metadata.",
+ "provider", s.Metadata.Name,
+ "event_id", eventID,
+ "fingerprint", eventDataFingerprint,
+ "default_event_metadata", defaultEM,
+ "event_metadata", em)
+
+ fingerprints[eventDataFingerprint] = em
+ return em
+}
+
+func (s *publisherMetadataStore) Close() error {
+ if s.Metadata != nil {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ return s.Metadata.Close()
+ }
+ return nil
+}
+
+type eventMetadata struct {
+ EventID uint16 // Event ID.
+ Version uint8 // Event format version.
+ MsgStatic string // Used when the message has no parameters.
+ MsgTemplate *template.Template `json:"-"` // Template that expects an array of values as its data.
+ EventData []eventData // Names of parameters from XML template.
+}
+
+// newEventMetadataFromEventHandle collects metadata about an event type using
+// the handle of an event.
+func newEventMetadataFromEventHandle(publisher *PublisherMetadata, eventHandle EvtHandle) (*eventMetadata, error) {
+ xml, err := getEventXML(publisher, eventHandle)
+ if err != nil {
+ return nil, err
+ }
+
+ // By parsing the XML we can get the names of the parameters even if the
+ // publisher metadata is unavailable or is out of sync with the events.
+ event, err := sys.UnmarshalEventXML([]byte(xml))
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to unmarshal XML")
+ }
+
+ em := &eventMetadata{
+ EventID: uint16(event.EventIdentifier.ID),
+ Version: uint8(event.Version),
+ }
+ if len(event.EventData.Pairs) > 0 {
+ for _, pair := range event.EventData.Pairs {
+ em.EventData = append(em.EventData, eventData{Name: pair.Key})
+ }
+ } else {
+ for _, pair := range event.UserData.Pairs {
+ em.EventData = append(em.EventData, eventData{Name: pair.Key})
+ }
+ }
+
+ // The message template is only available from the publisher metadata. This
+ // message template may not match up with the event data we got from the
+ // event's XML, but it's the only option available. Even forwarded events
+ // with "RenderedText" won't help because their messages are already
+ // rendered.
+ if publisher != nil {
+ msg, err := getMessageStringFromHandle(publisher, eventHandle, templateInserts.Slice())
+ if err != nil {
+ return nil, err
+ }
+ if err = em.setMessage(msg); err != nil {
+ return nil, err
+ }
+ }
+
+ return em, nil
+}
+
+// newEventMetadataFromPublisherMetadata collects metadata about an event type
+// using the publisher metadata.
+func newEventMetadataFromPublisherMetadata(itr *EventMetadataIterator, publisher *PublisherMetadata) (*eventMetadata, error) {
+ em := &eventMetadata{}
+ err := multierr.Combine(
+ em.initEventID(itr),
+ em.initVersion(itr),
+ em.initEventDataTemplate(itr),
+ em.initEventMessage(itr, publisher),
+ )
+ if err != nil {
+ return nil, err
+ }
+ return em, nil
+}
+
+func (em *eventMetadata) initEventID(itr *EventMetadataIterator) error {
+ id, err := itr.EventID()
+ if err != nil {
+ return err
+ }
+ // The upper 16 bits are the qualifier and lower 16 are the ID.
+ em.EventID = uint16(0xFFFF & id)
+ return nil
+}
+
+func (em *eventMetadata) initVersion(itr *EventMetadataIterator) error {
+ version, err := itr.Version()
+ if err != nil {
+ return err
+ }
+ em.Version = uint8(version)
+ return nil
+}
+
+func (em *eventMetadata) initEventDataTemplate(itr *EventMetadataIterator) error {
+ xml, err := itr.Template()
+ if err != nil {
+ return err
+ }
+ // Some events do not have templates.
+ if xml == "" {
+ return nil
+ }
+
+ tmpl := &eventTemplate{}
+ if err = tmpl.Unmarshal([]byte(xml)); err != nil {
+ return err
+ }
+
+ for _, kv := range tmpl.Data {
+ kv.Name = eventDataNameTransform.Replace(kv.Name)
+ }
+
+ em.EventData = tmpl.Data
+ return nil
+}
+
+func (em *eventMetadata) initEventMessage(itr *EventMetadataIterator, publisher *PublisherMetadata) error {
+ messageID, err := itr.MessageID()
+ if err != nil {
+ return err
+ }
+
+ msg, err := getMessageString(publisher, NilHandle, messageID, templateInserts.Slice())
+ if err != nil {
+ return err
+ }
+
+ return em.setMessage(msg)
+}
+
+func (em *eventMetadata) setMessage(msg string) error {
+ msg = sys.RemoveWindowsLineEndings(msg)
+ tmplID := strconv.Itoa(int(em.EventID))
+
+ tmpl, err := template.New(tmplID).Funcs(eventMessageTemplateFuncs).Parse(msg)
+ if err != nil {
+ return err
+ }
+
+ // One node means there were no parameters so this will optimize that case
+ // by using a static string rather than a text/template.
+ if len(tmpl.Root.Nodes) == 1 {
+ em.MsgStatic = msg
+ } else {
+ em.MsgTemplate = tmpl
+ }
+ return nil
+}
+
+func (em *eventMetadata) equal(other *eventMetadata) bool {
+ if em == other {
+ return true
+ }
+ if em == nil || other == nil {
+ return false
+ }
+
+ eventDataNamesEqual := func(a, b []eventData) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for n, v := range a {
+ if v.Name != b[n].Name {
+ return false
+ }
+ }
+ return true
+ }
+
+ return em.EventID == other.EventID &&
+ em.Version == other.Version &&
+ eventDataNamesEqual(em.EventData, other.EventData)
+}
+
+// --- Template Funcs
+
+// eventParam return an event data value inside a text/template.
+func eventParam(items []interface{}, paramNumber int) (interface{}, error) {
+ // Windows parameter values start at %1 so adjust index value by -1.
+ index := paramNumber - 1
+ if index < len(items) {
+ return items[index], nil
+ }
+ // Windows Event Viewer leaves the original placeholder (e.g. %22) in the
+ // rendered message when no value provided.
+ return "%" + strconv.Itoa(paramNumber), nil
+}
diff --git a/winlogbeat/sys/wineventlog/metadata_store_test.go b/winlogbeat/sys/wineventlog/metadata_store_test.go
new file mode 100644
index 00000000000..0c89251bb7a
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/metadata_store_test.go
@@ -0,0 +1,63 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/elastic/beats/v7/libbeat/logp"
+)
+
+func TestPublisherMetadataStore(t *testing.T) {
+ logp.TestingSetup()
+
+ s, err := newPublisherMetadataStore(
+ NilHandle,
+ "Microsoft-Windows-Security-Auditing",
+ logp.NewLogger("metadata"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+
+ assert.NotEmpty(t, s.Events)
+ assert.Empty(t, s.EventFingerprints)
+
+ t.Run("event_metadata_from_handle", func(t *testing.T) {
+ log := openLog(t, security4752File)
+ defer log.Close()
+
+ h := mustNextHandle(t, log)
+ defer h.Close()
+
+ em, err := newEventMetadataFromEventHandle(s.Metadata, h)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.EqualValues(t, 4752, em.EventID)
+ assert.EqualValues(t, 0, em.Version)
+ assert.Empty(t, em.MsgStatic)
+ assert.NotNil(t, em.MsgTemplate)
+ assert.NotEmpty(t, em.EventData)
+ })
+}
diff --git a/winlogbeat/sys/wineventlog/publisher_metadata.go b/winlogbeat/sys/wineventlog/publisher_metadata.go
new file mode 100644
index 00000000000..73be91e849c
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/publisher_metadata.go
@@ -0,0 +1,663 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "os"
+ "syscall"
+
+ "github.com/pkg/errors"
+ "go.uber.org/multierr"
+ "golang.org/x/sys/windows"
+)
+
+// PublisherMetadata provides methods to query metadata from an event log
+// publisher.
+type PublisherMetadata struct {
+ Name string // Name of the publisher/provider.
+ Handle EvtHandle // Handle to the publisher metadata from EvtOpenPublisherMetadata.
+}
+
+// Close releases the publisher metadata handle.
+func (m *PublisherMetadata) Close() error {
+ return m.Handle.Close()
+}
+
+// NewPublisherMetadata opens the publisher's metadata. Close must be called on
+// the returned PublisherMetadata to release its handle.
+func NewPublisherMetadata(session EvtHandle, name string) (*PublisherMetadata, error) {
+ var publisherName, logFile *uint16
+ if info, err := os.Stat(name); err == nil && info.Mode().IsRegular() {
+ logFile, err = syscall.UTF16PtrFromString(name)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ publisherName, err = syscall.UTF16PtrFromString(name)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ handle, err := _EvtOpenPublisherMetadata(session, publisherName, logFile, 0, 0)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed in EvtOpenPublisherMetadata")
+ }
+
+ return &PublisherMetadata{
+ Name: name,
+ Handle: handle,
+ }, nil
+}
+
+func (m *PublisherMetadata) stringProperty(propertyID EvtPublisherMetadataPropertyID) (string, error) {
+ v, err := EvtGetPublisherMetadataProperty(m.Handle, propertyID)
+ if err != nil {
+ return "", err
+ }
+ switch t := v.(type) {
+ case string:
+ return t, nil
+ case nil:
+ return "", nil
+ default:
+ return "", errors.Errorf("unexpected data type: %T", v)
+ }
+}
+
+func (m *PublisherMetadata) PublisherGUID() (windows.GUID, error) {
+ v, err := EvtGetPublisherMetadataProperty(m.Handle, EvtPublisherMetadataPublisherGuid)
+ if err != nil {
+ return windows.GUID{}, err
+ }
+ switch t := v.(type) {
+ case windows.GUID:
+ return t, nil
+ case nil:
+ return windows.GUID{}, nil
+ default:
+ return windows.GUID{}, errors.Errorf("unexpected data type: %T", v)
+ }
+}
+
+func (m *PublisherMetadata) ResourceFilePath() (string, error) {
+ return m.stringProperty(EvtPublisherMetadataResourceFilePath)
+}
+
+func (m *PublisherMetadata) ParameterFilePath() (string, error) {
+ return m.stringProperty(EvtPublisherMetadataParameterFilePath)
+}
+
+func (m *PublisherMetadata) MessageFilePath() (string, error) {
+ return m.stringProperty(EvtPublisherMetadataMessageFilePath)
+}
+
+func (m *PublisherMetadata) HelpLink() (string, error) {
+ return m.stringProperty(EvtPublisherMetadataHelpLink)
+}
+
+func (m *PublisherMetadata) PublisherMessageID() (uint32, error) {
+ v, err := EvtGetPublisherMetadataProperty(m.Handle, EvtPublisherMetadataPublisherMessageID)
+ if err != nil {
+ return 0, err
+ }
+ return v.(uint32), nil
+}
+
+func (m *PublisherMetadata) PublisherMessage() (string, error) {
+ messageID, err := m.PublisherMessageID()
+ if err != nil {
+ return "", err
+ }
+ if int32(messageID) == -1 {
+ return "", nil
+ }
+ return getMessageStringFromMessageID(m, messageID, nil)
+}
+
+func (m *PublisherMetadata) Keywords() ([]MetadataKeyword, error) {
+ return NewMetadataKeywords(m.Handle)
+}
+
+func (m *PublisherMetadata) Opcodes() ([]MetadataOpcode, error) {
+ return NewMetadataOpcodes(m.Handle)
+}
+
+func (m *PublisherMetadata) Levels() ([]MetadataLevel, error) {
+ return NewMetadataLevels(m.Handle)
+}
+
+func (m *PublisherMetadata) Tasks() ([]MetadataTask, error) {
+ return NewMetadataTasks(m.Handle)
+}
+
+func (m *PublisherMetadata) Channels() ([]MetadataChannel, error) {
+ return NewMetadataChannels(m.Handle)
+}
+
+func (m *PublisherMetadata) EventMetadataIterator() (*EventMetadataIterator, error) {
+ return NewEventMetadataIterator(m)
+}
+
+type MetadataKeyword struct {
+ Name string
+ Mask uint64
+ Message string
+ MessageID uint32
+}
+
+func NewMetadataKeywords(publisherMetadataHandle EvtHandle) ([]MetadataKeyword, error) {
+ v, err := EvtGetPublisherMetadataProperty(publisherMetadataHandle, EvtPublisherMetadataKeywords)
+ if err != nil {
+ return nil, err
+ }
+
+ arrayHandle, ok := v.(EvtObjectArrayPropertyHandle)
+ if !ok {
+ return nil, errors.Errorf("unexpected handle type: %T", v)
+ }
+ defer arrayHandle.Close()
+
+ arrayLen, err := EvtGetObjectArraySize(arrayHandle)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get keyword array length")
+ }
+
+ var values []MetadataKeyword
+ for i := uint32(0); i < arrayLen; i++ {
+ md, err := NewMetadataKeyword(publisherMetadataHandle, arrayHandle, i)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get keyword at array index %v", i)
+ }
+
+ values = append(values, *md)
+ }
+
+ return values, nil
+}
+
+func NewMetadataKeyword(publisherMetadataHandle EvtHandle, arrayHandle EvtObjectArrayPropertyHandle, index uint32) (*MetadataKeyword, error) {
+ v, err := EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataKeywordMessageID, index)
+ if err != nil {
+ return nil, err
+ }
+ messageID := v.(uint32)
+
+ // The value is -1 if the keyword did not specify a message attribute.
+ var message string
+ if int32(messageID) != -1 {
+ message, err = evtFormatMessage(publisherMetadataHandle, NilHandle, messageID, nil, EvtFormatMessageId)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataKeywordName, index)
+ if err != nil {
+ return nil, err
+ }
+ name := v.(string)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataKeywordValue, index)
+ if err != nil {
+ return nil, err
+ }
+ valueMask := v.(uint64)
+
+ return &MetadataKeyword{
+ Name: name,
+ Mask: valueMask,
+ MessageID: messageID,
+ Message: message,
+ }, nil
+}
+
+type MetadataOpcode struct {
+ Name string
+ Mask uint32
+ MessageID uint32
+ Message string
+}
+
+func NewMetadataOpcodes(publisherMetadataHandle EvtHandle) ([]MetadataOpcode, error) {
+ v, err := EvtGetPublisherMetadataProperty(publisherMetadataHandle, EvtPublisherMetadataOpcodes)
+ if err != nil {
+ return nil, err
+ }
+
+ arrayHandle, ok := v.(EvtObjectArrayPropertyHandle)
+ if !ok {
+ return nil, errors.Errorf("unexpected handle type: %T", v)
+ }
+ defer arrayHandle.Close()
+
+ arrayLen, err := EvtGetObjectArraySize(arrayHandle)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get opcode array length")
+ }
+
+ var values []MetadataOpcode
+ for i := uint32(0); i < arrayLen; i++ {
+ md, err := NewMetadataOpcode(publisherMetadataHandle, arrayHandle, i)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get opcode at array index %v", i)
+ }
+
+ values = append(values, *md)
+ }
+
+ return values, nil
+}
+
+func NewMetadataOpcode(publisherMetadataHandle EvtHandle, arrayHandle EvtObjectArrayPropertyHandle, index uint32) (*MetadataOpcode, error) {
+ v, err := EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataOpcodeMessageID, index)
+ if err != nil {
+ return nil, err
+ }
+ messageID := v.(uint32)
+
+ // The value is -1 if the opcode did not specify a message attribute.
+ var message string
+ if int32(messageID) != -1 {
+ message, err = evtFormatMessage(publisherMetadataHandle, NilHandle, messageID, nil, EvtFormatMessageId)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataOpcodeName, index)
+ if err != nil {
+ return nil, err
+ }
+ name := v.(string)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataOpcodeValue, index)
+ if err != nil {
+ return nil, err
+ }
+ valueMask := v.(uint32)
+
+ return &MetadataOpcode{
+ Name: name,
+ Mask: valueMask,
+ MessageID: messageID,
+ Message: message,
+ }, nil
+}
+
+type MetadataLevel struct {
+ Name string
+ Mask uint32
+ MessageID uint32
+ Message string
+}
+
+func NewMetadataLevels(publisherMetadataHandle EvtHandle) ([]MetadataLevel, error) {
+ v, err := EvtGetPublisherMetadataProperty(publisherMetadataHandle, EvtPublisherMetadataLevels)
+ if err != nil {
+ return nil, err
+ }
+
+ arrayHandle, ok := v.(EvtObjectArrayPropertyHandle)
+ if !ok {
+ return nil, errors.Errorf("unexpected handle type: %T", v)
+ }
+ defer arrayHandle.Close()
+
+ arrayLen, err := EvtGetObjectArraySize(arrayHandle)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get level array length")
+ }
+
+ var values []MetadataLevel
+ for i := uint32(0); i < arrayLen; i++ {
+ md, err := NewMetadataLevel(publisherMetadataHandle, arrayHandle, i)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get level at array index %v", i)
+ }
+
+ values = append(values, *md)
+ }
+
+ return values, nil
+}
+
+func NewMetadataLevel(publisherMetadataHandle EvtHandle, arrayHandle EvtObjectArrayPropertyHandle, index uint32) (*MetadataLevel, error) {
+ v, err := EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataLevelMessageID, index)
+ if err != nil {
+ return nil, err
+ }
+ messageID := v.(uint32)
+
+ // The value is -1 if the level did not specify a message attribute.
+ var message string
+ if int32(messageID) != -1 {
+ message, err = evtFormatMessage(publisherMetadataHandle, NilHandle, messageID, nil, EvtFormatMessageId)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataLevelName, index)
+ if err != nil {
+ return nil, err
+ }
+ name := v.(string)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataLevelValue, index)
+ if err != nil {
+ return nil, err
+ }
+ valueMask := v.(uint32)
+
+ return &MetadataLevel{
+ Name: name,
+ Mask: valueMask,
+ MessageID: messageID,
+ Message: message,
+ }, nil
+}
+
+type MetadataTask struct {
+ Name string
+ Mask uint32
+ MessageID uint32
+ Message string
+ EventGUID windows.GUID
+}
+
+func NewMetadataTasks(publisherMetadataHandle EvtHandle) ([]MetadataTask, error) {
+ v, err := EvtGetPublisherMetadataProperty(publisherMetadataHandle, EvtPublisherMetadataTasks)
+ if err != nil {
+ return nil, err
+ }
+
+ arrayHandle, ok := v.(EvtObjectArrayPropertyHandle)
+ if !ok {
+ return nil, errors.Errorf("unexpected handle type: %T", v)
+ }
+ defer arrayHandle.Close()
+
+ arrayLen, err := EvtGetObjectArraySize(arrayHandle)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get task array length")
+ }
+
+ var values []MetadataTask
+ for i := uint32(0); i < arrayLen; i++ {
+ md, err := NewMetadataTask(publisherMetadataHandle, arrayHandle, i)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get task at array index %v", i)
+ }
+
+ values = append(values, *md)
+ }
+
+ return values, nil
+}
+
+func NewMetadataTask(publisherMetadataHandle EvtHandle, arrayHandle EvtObjectArrayPropertyHandle, index uint32) (*MetadataTask, error) {
+ v, err := EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataTaskMessageID, index)
+ if err != nil {
+ return nil, err
+ }
+ messageID := v.(uint32)
+
+ // The value is -1 if the task did not specify a message attribute.
+ var message string
+ if int32(messageID) != -1 {
+ message, err = evtFormatMessage(publisherMetadataHandle, NilHandle, messageID, nil, EvtFormatMessageId)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataTaskName, index)
+ if err != nil {
+ return nil, err
+ }
+ name := v.(string)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataTaskValue, index)
+ if err != nil {
+ return nil, err
+ }
+ valueMask := v.(uint32)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataTaskEventGuid, index)
+ if err != nil {
+ return nil, err
+ }
+ guid := v.(windows.GUID)
+
+ return &MetadataTask{
+ Name: name,
+ Mask: valueMask,
+ MessageID: messageID,
+ Message: message,
+ EventGUID: guid,
+ }, nil
+}
+
+type MetadataChannel struct {
+ Name string
+ Index uint32
+ ID uint32
+ Message string
+ MessageID uint32
+}
+
+func NewMetadataChannels(publisherMetadataHandle EvtHandle) ([]MetadataChannel, error) {
+ v, err := EvtGetPublisherMetadataProperty(publisherMetadataHandle, EvtPublisherMetadataChannelReferences)
+ if err != nil {
+ return nil, err
+ }
+
+ arrayHandle, ok := v.(EvtObjectArrayPropertyHandle)
+ if !ok {
+ return nil, errors.Errorf("unexpected handle type: %T", v)
+ }
+ defer arrayHandle.Close()
+
+ arrayLen, err := EvtGetObjectArraySize(arrayHandle)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get task array length")
+ }
+
+ var values []MetadataChannel
+ for i := uint32(0); i < arrayLen; i++ {
+ md, err := NewMetadataChannel(publisherMetadataHandle, arrayHandle, i)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get task at array index %v", i)
+ }
+
+ values = append(values, *md)
+ }
+
+ return values, nil
+}
+
+func NewMetadataChannel(publisherMetadataHandle EvtHandle, arrayHandle EvtObjectArrayPropertyHandle, index uint32) (*MetadataChannel, error) {
+ v, err := EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataChannelReferenceMessageID, index)
+ if err != nil {
+ return nil, err
+ }
+ messageID := v.(uint32)
+
+ // The value is -1 if the task did not specify a message attribute.
+ var message string
+ if int32(messageID) != -1 {
+ message, err = evtFormatMessage(publisherMetadataHandle, NilHandle, messageID, nil, EvtFormatMessageId)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Channel name.
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataChannelReferencePath, index)
+ if err != nil {
+ return nil, err
+ }
+ name := v.(string)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataChannelReferenceIndex, index)
+ if err != nil {
+ return nil, err
+ }
+ channelIndex := v.(uint32)
+
+ v, err = EvtGetObjectArrayProperty(arrayHandle, EvtPublisherMetadataChannelReferenceID, index)
+ if err != nil {
+ return nil, err
+ }
+ id := v.(uint32)
+
+ return &MetadataChannel{
+ Name: name,
+ Index: channelIndex,
+ ID: id,
+ MessageID: messageID,
+ Message: message,
+ }, nil
+}
+
+type EventMetadataIterator struct {
+ Publisher *PublisherMetadata
+ eventMetadataEnumHandle EvtHandle
+ currentEvent EvtHandle
+ lastErr error
+}
+
+func NewEventMetadataIterator(publisher *PublisherMetadata) (*EventMetadataIterator, error) {
+ eventMetadataEnumHandle, err := _EvtOpenEventMetadataEnum(publisher.Handle, 0)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to open event metadata enumerator with EvtOpenEventMetadataEnum")
+ }
+
+ return &EventMetadataIterator{
+ Publisher: publisher,
+ eventMetadataEnumHandle: eventMetadataEnumHandle,
+ }, nil
+}
+
+func (itr *EventMetadataIterator) Close() error {
+ return multierr.Combine(
+ _EvtClose(itr.eventMetadataEnumHandle),
+ _EvtClose(itr.currentEvent),
+ )
+}
+
+// Next advances to the next event handle. It returns false when there are
+// no more items or an error occurred. You should call Err() to check for an
+// error.
+func (itr *EventMetadataIterator) Next() bool {
+ // Close existing handle.
+ itr.currentEvent.Close()
+
+ var err error
+ itr.currentEvent, err = _EvtNextEventMetadata(itr.eventMetadataEnumHandle, 0)
+ if err != nil {
+ if err != windows.ERROR_NO_MORE_ITEMS {
+ itr.lastErr = errors.Wrap(err, "failed advancing to next event metadata handle")
+ }
+ return false
+ }
+ return true
+}
+
+// Err returns an error if Next() failed due to an error.
+func (itr *EventMetadataIterator) Err() error {
+ return itr.lastErr
+}
+
+func (itr *EventMetadataIterator) uint32Property(propertyID EvtEventMetadataPropertyID) (uint32, error) {
+ v, err := GetEventMetadataProperty(itr.currentEvent, propertyID)
+ if err != nil {
+ return 0, err
+ }
+ return v.(uint32), nil
+}
+
+func (itr *EventMetadataIterator) uint64Property(propertyID EvtEventMetadataPropertyID) (uint64, error) {
+ v, err := GetEventMetadataProperty(itr.currentEvent, propertyID)
+ if err != nil {
+ return 0, err
+ }
+ return v.(uint64), nil
+}
+
+func (itr *EventMetadataIterator) stringProperty(propertyID EvtEventMetadataPropertyID) (string, error) {
+ v, err := GetEventMetadataProperty(itr.currentEvent, propertyID)
+ if err != nil {
+ return "", err
+ }
+ return v.(string), nil
+}
+
+func (itr *EventMetadataIterator) EventID() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventID)
+}
+
+func (itr *EventMetadataIterator) Version() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventVersion)
+}
+
+func (itr *EventMetadataIterator) Channel() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventVersion)
+}
+
+func (itr *EventMetadataIterator) Level() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventLevel)
+}
+
+func (itr *EventMetadataIterator) Opcode() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventOpcode)
+}
+
+func (itr *EventMetadataIterator) Task() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventTask)
+}
+
+func (itr *EventMetadataIterator) Keyword() (uint64, error) {
+ return itr.uint64Property(EventMetadataEventKeyword)
+}
+
+func (itr *EventMetadataIterator) MessageID() (uint32, error) {
+ return itr.uint32Property(EventMetadataEventMessageID)
+}
+
+func (itr *EventMetadataIterator) Template() (string, error) {
+ return itr.stringProperty(EventMetadataEventTemplate)
+}
+
+// Message returns the raw event description without doing any substitutions
+// (e.g. the message will contain %1, %2, etc. as parameter placeholders).
+func (itr *EventMetadataIterator) Message() (string, error) {
+ messageID, err := itr.MessageID()
+ if err != nil {
+ return "", err
+ }
+ // If the event definition does not specify a message, the value is –1.
+ if int32(messageID) == -1 {
+ return "", nil
+ }
+
+ return getMessageStringFromMessageID(itr.Publisher, messageID, nil)
+}
diff --git a/winlogbeat/sys/wineventlog/publisher_metadata_test.go b/winlogbeat/sys/wineventlog/publisher_metadata_test.go
new file mode 100644
index 00000000000..1f5f5a2b85c
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/publisher_metadata_test.go
@@ -0,0 +1,230 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/sys/windows"
+)
+
+func TestPublisherMetadata(t *testing.T) {
+ // Modern Application
+ testPublisherMetadata(t, "Microsoft-Windows-PowerShell")
+ // Modern Application that uses UserData in XML
+ testPublisherMetadata(t, "Microsoft-Windows-Eventlog")
+ // Classic with messages (no event-data XML templates).
+ testPublisherMetadata(t, "Microsoft-Windows-Security-SPP")
+ // Classic without message metadata (no event-data XML templates).
+ testPublisherMetadata(t, "Windows Error Reporting")
+}
+
+func testPublisherMetadata(t *testing.T, provider string) {
+ t.Run(provider, func(t *testing.T) {
+ md, err := NewPublisherMetadata(NilHandle, provider)
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ defer md.Close()
+
+ t.Run("publisher_guid", func(t *testing.T) {
+ v, err := md.PublisherGUID()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("PublisherGUID: %v", v)
+ })
+
+ t.Run("resource_file_path", func(t *testing.T) {
+ v, err := md.ResourceFilePath()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("ResourceFilePath: %v", v)
+ })
+
+ t.Run("parameter_file_path", func(t *testing.T) {
+ v, err := md.ParameterFilePath()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("ParameterFilePath: %v", v)
+ })
+
+ t.Run("message_file_path", func(t *testing.T) {
+ v, err := md.MessageFilePath()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("MessageFilePath: %v", v)
+ })
+
+ t.Run("help_link", func(t *testing.T) {
+ v, err := md.HelpLink()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("HelpLink: %v", v)
+ })
+
+ t.Run("publisher_message_id", func(t *testing.T) {
+ v, err := md.PublisherMessageID()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("PublisherMessageID: %v", v)
+ })
+
+ t.Run("publisher_message", func(t *testing.T) {
+ v, err := md.PublisherMessage()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ t.Logf("PublisherMessage: %v", v)
+ })
+
+ t.Run("keywords", func(t *testing.T) {
+ values, err := md.Keywords()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+
+ if testing.Verbose() {
+ for _, value := range values {
+ t.Logf("%+v", value)
+ }
+ }
+ })
+
+ t.Run("opcodes", func(t *testing.T) {
+ values, err := md.Opcodes()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+
+ if testing.Verbose() {
+ for _, value := range values {
+ t.Logf("%+v", value)
+ }
+ }
+ })
+
+ t.Run("levels", func(t *testing.T) {
+ values, err := md.Levels()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+
+ if testing.Verbose() {
+ for _, value := range values {
+ t.Logf("%+v", value)
+ }
+ }
+ })
+
+ t.Run("tasks", func(t *testing.T) {
+ values, err := md.Tasks()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+
+ if testing.Verbose() {
+ for _, value := range values {
+ t.Logf("%+v", value)
+ }
+ }
+ })
+
+ t.Run("channels", func(t *testing.T) {
+ values, err := md.Channels()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+
+ if testing.Verbose() {
+ for _, value := range values {
+ t.Logf("%+v", value)
+ }
+ }
+ })
+
+ t.Run("event_metadata", func(t *testing.T) {
+ itr, err := md.EventMetadataIterator()
+ if err != nil {
+ t.Fatalf("%+v", err)
+ }
+ defer itr.Close()
+
+ for itr.Next() {
+ eventID, err := itr.EventID()
+ assert.NoError(t, err)
+ t.Logf("eventID=%v (id=%v, qualifier=%v)", eventID,
+ 0xFFFF&eventID, // Lower 16 bits are the event ID.
+ (0xFFFF0000&eventID)>>16) // Upper 16 bits are the qualifier.
+
+ version, err := itr.Version()
+ assert.NoError(t, err)
+ t.Logf("version=%v", version)
+
+ channel, err := itr.Channel()
+ assert.NoError(t, err)
+ t.Logf("channel=%v", channel)
+
+ level, err := itr.Level()
+ assert.NoError(t, err)
+ t.Logf("level=%v", level)
+
+ opcode, err := itr.Opcode()
+ assert.NoError(t, err)
+ t.Logf("opcode=%v", opcode)
+
+ task, err := itr.Task()
+ assert.NoError(t, err)
+ t.Logf("task=%v", task)
+
+ keyword, err := itr.Keyword()
+ assert.NoError(t, err)
+ t.Logf("keyword=%v", keyword)
+
+ messageID, err := itr.MessageID()
+ assert.NoError(t, err)
+ t.Logf("messageID=%v", messageID)
+
+ template, err := itr.Template()
+ assert.NoError(t, err)
+ t.Logf("template=%v", template)
+
+ message, err := itr.Message()
+ assert.NoError(t, err)
+ t.Logf("message=%v", message)
+ }
+ if err = itr.Err(); err != nil {
+ t.Fatalf("%+v", err)
+ }
+ })
+ })
+}
+
+func TestNewPublisherMetadataUnknown(t *testing.T) {
+ _, err := NewPublisherMetadata(NilHandle, "Fake-Publisher")
+ assert.Equal(t, windows.ERROR_FILE_NOT_FOUND, errors.Cause(err))
+}
diff --git a/winlogbeat/sys/wineventlog/query.go b/winlogbeat/sys/wineventlog/query.go
index 76512747c44..8b822600425 100644
--- a/winlogbeat/sys/wineventlog/query.go
+++ b/winlogbeat/sys/wineventlog/query.go
@@ -49,8 +49,8 @@ var (
// Query that identifies the source of the events and one or more selectors or
// suppressors.
type Query struct {
- // Name of the channel or the path to the log file that contains the events
- // to query.
+ // Name of the channel or the URI path to the log file that contains the
+ // events to query. The path to files must be a URI like file://C:/log.evtx.
Log string
IgnoreOlder time.Duration // Ignore records older than this time period.
@@ -209,7 +209,7 @@ func (qp *queryParams) providerSelect(q Query) error {
return nil
}
- var selects []string
+ selects := make([]string, 0, len(q.Provider))
for _, p := range q.Provider {
selects = append(selects, fmt.Sprintf("@Name='%s'", p))
}
diff --git a/winlogbeat/sys/wineventlog/renderer.go b/winlogbeat/sys/wineventlog/renderer.go
new file mode 100644
index 00000000000..8dbafd56ad4
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/renderer.go
@@ -0,0 +1,436 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "encoding/binary"
+ "fmt"
+ "strconv"
+ "sync"
+ "text/template"
+ "time"
+ "unsafe"
+
+ "github.com/cespare/xxhash/v2"
+ "github.com/pkg/errors"
+ "go.uber.org/multierr"
+ "golang.org/x/sys/windows"
+
+ "github.com/elastic/beats/v7/libbeat/logp"
+ "github.com/elastic/beats/v7/winlogbeat/sys"
+)
+
+const (
+ // keywordClassic indicates the log was published with the "classic" event
+ // logging API.
+ // https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.eventing.reader.standardeventkeywords?view=netframework-4.8
+ keywordClassic = 0x80000000000000
+)
+
+// Renderer is used for converting event log handles into complete events.
+type Renderer struct {
+ // Cache of publisher metadata. Maps publisher names to stored metadata.
+ metadataCache map[string]*publisherMetadataStore
+ // Mutex to guard the metadataCache. The other members are immutable.
+ mutex sync.RWMutex
+
+ session EvtHandle // Session handle if working with remote log.
+ systemContext EvtHandle // Render context for system values.
+ userContext EvtHandle // Render context for user values (event data).
+ log *logp.Logger
+}
+
+// NewRenderer returns a new Renderer.
+func NewRenderer(session EvtHandle, log *logp.Logger) (*Renderer, error) {
+ systemContext, err := _EvtCreateRenderContext(0, 0, EvtRenderContextSystem)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed in EvtCreateRenderContext for system context")
+ }
+
+ userContext, err := _EvtCreateRenderContext(0, 0, EvtRenderContextUser)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed in EvtCreateRenderContext for user context")
+ }
+
+ return &Renderer{
+ metadataCache: map[string]*publisherMetadataStore{},
+ session: session,
+ systemContext: systemContext,
+ userContext: userContext,
+ log: log.Named("renderer"),
+ }, nil
+}
+
+// Close closes all handles held by the Renderer.
+func (r *Renderer) Close() error {
+ r.mutex.Lock()
+ defer r.mutex.Unlock()
+
+ errs := []error{r.systemContext.Close(), r.userContext.Close()}
+ for _, md := range r.metadataCache {
+ if err := md.Close(); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ return multierr.Combine(errs...)
+}
+
+// Render renders the event handle into an Event.
+func (r *Renderer) Render(handle EvtHandle) (*sys.Event, error) {
+ event := &sys.Event{}
+
+ if err := r.renderSystem(handle, event); err != nil {
+ return nil, errors.Wrap(err, "failed to render system properties")
+ }
+
+ // From this point on it will return both the event and any errors. It's
+ // critical to not drop data.
+ var errs []error
+
+ // This always returns a non-nil value (even on error).
+ md, err := r.getPublisherMetadata(event.Provider.Name)
+ if err != nil {
+ errs = append(errs, err)
+ }
+
+ // Associate raw system properties to names (e.g. level=2 to Error).
+ enrichRawValuesWithNames(md, event)
+
+ eventData, fingerprint, err := r.renderUser(handle, event)
+ if err != nil {
+ errs = append(errs, errors.Wrap(err, "failed to render event data"))
+ }
+
+ // Load cached event metadata or try to bootstrap it from the event's XML.
+ eventMeta := md.getEventMetadata(uint16(event.EventIdentifier.ID), fingerprint, handle)
+
+ if err = r.addEventData(eventMeta, eventData, event); err != nil {
+ errs = append(errs, err)
+ }
+
+ if event.Message, err = r.formatMessage(md, eventMeta, handle, eventData, uint16(event.EventIdentifier.ID)); err != nil {
+ errs = append(errs, err)
+ }
+
+ if len(errs) > 0 {
+ return event, multierr.Combine(errs...)
+ }
+ return event, nil
+}
+
+// getPublisherMetadata return a publisherMetadataStore for the provider. It
+// never returns nil, but may return an error if it couldn't open a publisher.
+func (r *Renderer) getPublisherMetadata(publisher string) (*publisherMetadataStore, error) {
+ var err error
+
+ // NOTE: This code uses double-check locking to elevate to a write-lock
+ // when a cache value needs initialized.
+ r.mutex.RLock()
+
+ // Lookup cached value.
+ md, found := r.metadataCache[publisher]
+ if !found {
+ // Elevate to write lock.
+ r.mutex.RUnlock()
+ r.mutex.Lock()
+ defer r.mutex.Unlock()
+
+ // Double-check if the condition changed while upgrading the lock.
+ md, found = r.metadataCache[publisher]
+ if found {
+ return md, nil
+ }
+
+ // Load metadata from the publisher.
+ md, err = newPublisherMetadataStore(r.session, publisher, r.log)
+ if err != nil {
+ // Return an empty store on error (can happen in cases where the
+ // log was forwarded and the provider doesn't exist on collector).
+ md = newEmptyPublisherMetadataStore(publisher, r.log)
+ }
+ r.metadataCache[publisher] = md
+ } else {
+ r.mutex.RUnlock()
+ }
+
+ return md, err
+}
+
+// renderSystem writes all the system context properties into the event.
+func (r *Renderer) renderSystem(handle EvtHandle, event *sys.Event) error {
+ bb, propertyCount, err := r.render(r.systemContext, handle)
+ if err != nil {
+ return errors.Wrap(err, "failed to get system values")
+ }
+ defer bb.free()
+
+ for i := 0; i < int(propertyCount); i++ {
+ property := EvtSystemPropertyID(i)
+ offset := i * int(sizeofEvtVariant)
+ evtVar := (*EvtVariant)(unsafe.Pointer(&bb.buf[offset]))
+
+ data, err := evtVar.Data(bb.buf)
+ if err != nil || data == nil {
+ continue
+ }
+
+ switch property {
+ case EvtSystemProviderName:
+ event.Provider.Name = data.(string)
+ case EvtSystemProviderGuid:
+ event.Provider.GUID = data.(windows.GUID).String()
+ case EvtSystemEventID:
+ event.EventIdentifier.ID = uint32(data.(uint16))
+ case EvtSystemQualifiers:
+ event.EventIdentifier.Qualifiers = data.(uint16)
+ case EvtSystemLevel:
+ event.LevelRaw = data.(uint8)
+ case EvtSystemTask:
+ event.TaskRaw = data.(uint16)
+ case EvtSystemOpcode:
+ event.OpcodeRaw = data.(uint8)
+ case EvtSystemKeywords:
+ event.KeywordsRaw = sys.HexInt64(data.(hexInt64))
+ case EvtSystemTimeCreated:
+ event.TimeCreated.SystemTime = data.(time.Time)
+ case EvtSystemEventRecordId:
+ event.RecordID = data.(uint64)
+ case EvtSystemActivityID:
+ event.Correlation.ActivityID = data.(windows.GUID).String()
+ case EvtSystemRelatedActivityID:
+ event.Correlation.RelatedActivityID = data.(windows.GUID).String()
+ case EvtSystemProcessID:
+ event.Execution.ProcessID = data.(uint32)
+ case EvtSystemThreadID:
+ event.Execution.ThreadID = data.(uint32)
+ case EvtSystemChannel:
+ event.Channel = data.(string)
+ case EvtSystemComputer:
+ event.Computer = data.(string)
+ case EvtSystemUserID:
+ sid := data.(*windows.SID)
+ event.User.Identifier = sid.String()
+ var accountType uint32
+ event.User.Name, event.User.Domain, accountType, _ = sid.LookupAccount("")
+ event.User.Type = sys.SIDType(accountType)
+ case EvtSystemVersion:
+ event.Version = sys.Version(data.(uint8))
+ }
+ }
+
+ return nil
+}
+
+// renderUser returns the event/user data values. This does not provide the
+// parameter names. It computes a fingerprint of the values types to help the
+// caller match the correct names to the returned values.
+func (r *Renderer) renderUser(handle EvtHandle, event *sys.Event) (values []interface{}, fingerprint uint64, err error) {
+ bb, propertyCount, err := r.render(r.userContext, handle)
+ if err != nil {
+ return nil, 0, errors.Wrap(err, "failed to get user values")
+ }
+ defer bb.free()
+
+ if propertyCount == 0 {
+ return nil, 0, nil
+ }
+
+ // Fingerprint the argument types to help ensure we match these values with
+ // the correct event data parameter names.
+ argumentHash := xxhash.New()
+ binary.Write(argumentHash, binary.LittleEndian, propertyCount)
+
+ values = make([]interface{}, propertyCount)
+ for i := 0; i < propertyCount; i++ {
+ offset := i * int(sizeofEvtVariant)
+ evtVar := (*EvtVariant)(unsafe.Pointer(&bb.buf[offset]))
+ binary.Write(argumentHash, binary.LittleEndian, uint32(evtVar.Type))
+
+ values[i], err = evtVar.Data(bb.buf)
+ if err != nil {
+ r.log.Warnw("Failed to read event/user data value. Using nil.",
+ "provider", event.Provider.Name,
+ "event_id", event.EventIdentifier.ID,
+ "value_index", i,
+ "value_type", evtVar.Type.String(),
+ "error", err,
+ )
+ }
+ }
+
+ return values, argumentHash.Sum64(), nil
+}
+
+// render uses EvtRender to event data. The caller must free() the returned when
+// done accessing the bytes.
+func (r *Renderer) render(context EvtHandle, eventHandle EvtHandle) (*byteBuffer, int, error) {
+ var bufferUsed, propertyCount uint32
+
+ err := _EvtRender(context, eventHandle, EvtRenderEventValues, 0, nil, &bufferUsed, &propertyCount)
+ if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return nil, 0, errors.Wrap(err, "failed in EvtRender")
+ }
+
+ if propertyCount == 0 {
+ return nil, 0, nil
+ }
+
+ bb := newByteBuffer()
+ bb.SetLength(int(bufferUsed))
+
+ err = _EvtRender(context, eventHandle, EvtRenderEventValues, uint32(len(bb.buf)), &bb.buf[0], &bufferUsed, &propertyCount)
+ if err != nil {
+ bb.free()
+ return nil, 0, errors.Wrap(err, "failed in EvtRender")
+ }
+
+ return bb, int(propertyCount), nil
+}
+
+// addEventData adds the event/user data values to the event.
+func (r *Renderer) addEventData(evtMeta *eventMetadata, values []interface{}, event *sys.Event) error {
+ if len(values) == 0 {
+ return nil
+ }
+
+ if evtMeta == nil {
+ r.log.Warnw("Event metadata not found.",
+ "provider", event.Provider.Name,
+ "event_id", event.EventIdentifier.ID)
+ } else if len(values) != len(evtMeta.EventData) {
+ r.log.Warnw("The number of event data parameters doesn't match the number "+
+ "of parameters in the template.",
+ "provider", event.Provider.Name,
+ "event_id", event.EventIdentifier.ID,
+ "event_parameter_count", len(values),
+ "template_parameter_count", len(evtMeta.EventData),
+ "template_version", evtMeta.Version,
+ "event_version", event.Version)
+ }
+
+ // Fallback to paramN naming when the value does not exist in event data.
+ // This can happen for legacy providers without manifests. This can also
+ // happen if the installed provider manifest doesn't match the version that
+ // produced the event (forwarded events, reading from evtx, or software was
+ // updated). If software was updated it could also be that this cached
+ // template is now stale.
+ paramName := func(idx int) string {
+ if evtMeta != nil && idx < len(evtMeta.EventData) {
+ return evtMeta.EventData[idx].Name
+ }
+ return "param" + strconv.Itoa(idx)
+ }
+
+ for i, v := range values {
+ var strVal string
+ switch t := v.(type) {
+ case string:
+ strVal = t
+ case *windows.SID:
+ strVal = t.String()
+ default:
+ strVal = fmt.Sprintf("%v", v)
+ }
+
+ event.EventData.Pairs = append(event.EventData.Pairs, sys.KeyValue{
+ Key: paramName(i),
+ Value: strVal,
+ })
+ }
+
+ return nil
+}
+
+// formatMessage adds the message to the event.
+func (r *Renderer) formatMessage(publisherMeta *publisherMetadataStore,
+ eventMeta *eventMetadata, eventHandle EvtHandle, values []interface{},
+ eventID uint16) (string, error) {
+
+ if eventMeta != nil {
+ if eventMeta.MsgStatic != "" {
+ return eventMeta.MsgStatic, nil
+ } else if eventMeta.MsgTemplate != nil {
+ return r.formatMessageFromTemplate(eventMeta.MsgTemplate, values)
+ }
+ }
+
+ // Fallback to the trying EvtFormatMessage mechanism.
+ // This is the path for forwarded events in RenderedText mode where the
+ // local publisher metadata is not present. NOTE that if the local publisher
+ // metadata exists it will be preferred over the RenderedText. A config
+ // option might be desirable to control this behavior.
+ r.log.Debugf("Falling back to EvtFormatMessage for event ID %d.", eventID)
+ return getMessageString(publisherMeta.Metadata, eventHandle, 0, nil)
+}
+
+// formatMessageFromTemplate creates the message by executing the stored Go
+// text/template with the event/user data values.
+func (r *Renderer) formatMessageFromTemplate(msgTmpl *template.Template, values []interface{}) (string, error) {
+ bb := newByteBuffer()
+ defer bb.free()
+
+ if err := msgTmpl.Execute(bb, values); err != nil {
+ return "", errors.Wrapf(err, "failed to execute template with data=%#v template=%v", values, msgTmpl.Root.String())
+ }
+
+ return string(bb.Bytes()), nil
+}
+
+// enrichRawValuesWithNames adds the names associated with the raw system
+// property values. It enriches the event with keywords, opcode, level, and
+// task. The search order is defined in the EvtFormatMessage documentation.
+func enrichRawValuesWithNames(publisherMeta *publisherMetadataStore, event *sys.Event) {
+ // Keywords. Each bit in the value can represent a keyword.
+ rawKeyword := int64(event.KeywordsRaw)
+ isClassic := keywordClassic&rawKeyword > 0
+ for mask, keyword := range winMeta.Keywords {
+ if rawKeyword&mask > 0 {
+ event.Keywords = append(event.Keywords, keyword)
+ rawKeyword -= mask
+ }
+ }
+ for mask, keyword := range publisherMeta.Keywords {
+ if rawKeyword&mask > 0 {
+ event.Keywords = append(event.Keywords, keyword)
+ rawKeyword -= mask
+ }
+ }
+
+ // Opcode (search in winmeta first).
+ var found bool
+ if !isClassic {
+ event.Opcode, found = winMeta.Opcodes[event.OpcodeRaw]
+ if !found {
+ event.Opcode = publisherMeta.Opcodes[event.OpcodeRaw]
+ }
+ }
+
+ // Level (search in winmeta first).
+ event.Level, found = winMeta.Levels[event.LevelRaw]
+ if !found {
+ event.Level = publisherMeta.Levels[event.LevelRaw]
+ }
+
+ // Task (fall-back to winmeta if not found).
+ event.Task, found = publisherMeta.Tasks[event.TaskRaw]
+ if !found {
+ event.Task = winMeta.Tasks[event.TaskRaw]
+ }
+}
diff --git a/winlogbeat/sys/wineventlog/renderer_test.go b/winlogbeat/sys/wineventlog/renderer_test.go
new file mode 100644
index 00000000000..c6cfc582bb5
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/renderer_test.go
@@ -0,0 +1,290 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "bytes"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "testing"
+ "text/template"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/elastic/beats/v7/libbeat/common/atomic"
+ "github.com/elastic/beats/v7/libbeat/logp"
+ "github.com/elastic/beats/v7/winlogbeat/sys"
+)
+
+func TestRenderer(t *testing.T) {
+ logp.TestingSetup()
+
+ t.Run(filepath.Base(sysmon9File), func(t *testing.T) {
+ log := openLog(t, sysmon9File)
+ defer log.Close()
+
+ r, err := NewRenderer(NilHandle, logp.L())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ events := renderAllEvents(t, log, r)
+ assert.NotEmpty(t, events)
+
+ if t.Failed() {
+ logAsJSON(t, events)
+ }
+ })
+
+ t.Run(filepath.Base(security4752File), func(t *testing.T) {
+ log := openLog(t, security4752File)
+ defer log.Close()
+
+ r, err := NewRenderer(NilHandle, logp.L())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ events := renderAllEvents(t, log, r)
+ if !assert.Len(t, events, 1) {
+ return
+ }
+ e := events[0]
+
+ assert.EqualValues(t, 4752, e.EventIdentifier.ID)
+ assert.Equal(t, "Microsoft-Windows-Security-Auditing", e.Provider.Name)
+ assertEqualIgnoreCase(t, "{54849625-5478-4994-a5ba-3e3b0328c30d}", e.Provider.GUID)
+ assert.Equal(t, "DC_TEST2k12.TEST.SAAS", e.Computer)
+ assert.Equal(t, "Security", e.Channel)
+ assert.EqualValues(t, 3707686, e.RecordID)
+
+ assert.Equal(t, e.Keywords, []string{"Audit Success"})
+
+ assert.EqualValues(t, 0, e.OpcodeRaw)
+ assert.Equal(t, "Info", e.Opcode)
+
+ assert.EqualValues(t, 0, e.LevelRaw)
+ assert.Equal(t, "Information", e.Level)
+
+ assert.EqualValues(t, 13827, e.TaskRaw)
+ assert.Equal(t, "Distribution Group Management", e.Task)
+
+ assert.EqualValues(t, 492, e.Execution.ProcessID)
+ assert.EqualValues(t, 1076, e.Execution.ThreadID)
+ assert.Len(t, e.EventData.Pairs, 10)
+
+ assert.NotEmpty(t, e.Message)
+
+ if t.Failed() {
+ logAsJSON(t, events)
+ }
+ })
+
+ t.Run(filepath.Base(winErrorReportingFile), func(t *testing.T) {
+ log := openLog(t, winErrorReportingFile)
+ defer log.Close()
+
+ r, err := NewRenderer(NilHandle, logp.L())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer r.Close()
+
+ events := renderAllEvents(t, log, r)
+ if !assert.Len(t, events, 1) {
+ return
+ }
+ e := events[0]
+
+ assert.EqualValues(t, 1001, e.EventIdentifier.ID)
+ assert.Equal(t, "Windows Error Reporting", e.Provider.Name)
+ assert.Empty(t, e.Provider.GUID)
+ assert.Equal(t, "vagrant", e.Computer)
+ assert.Equal(t, "Application", e.Channel)
+ assert.EqualValues(t, 420107, e.RecordID)
+
+ assert.Equal(t, e.Keywords, []string{"Classic"})
+
+ assert.EqualValues(t, 0, e.OpcodeRaw)
+ assert.Equal(t, "", e.Opcode)
+
+ assert.EqualValues(t, 4, e.LevelRaw)
+ assert.Equal(t, "Information", e.Level)
+
+ assert.EqualValues(t, 0, e.TaskRaw)
+ assert.Equal(t, "None", e.Task)
+
+ assert.EqualValues(t, 0, e.Execution.ProcessID)
+ assert.EqualValues(t, 0, e.Execution.ThreadID)
+ assert.Len(t, e.EventData.Pairs, 23)
+
+ assert.NotEmpty(t, e.Message)
+
+ if t.Failed() {
+ logAsJSON(t, events)
+ }
+ })
+}
+
+func TestTemplateFunc(t *testing.T) {
+ tmpl := template.Must(template.New("").
+ Funcs(eventMessageTemplateFuncs).
+ Parse(`Hello {{ eventParam $ 1 }}! Foo {{ eventParam $ 2 }}.`))
+
+ buf := new(bytes.Buffer)
+ err := tmpl.Execute(buf, []interface{}{"world"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, "Hello world! Foo %2.", buf.String())
+}
+
+// renderAllEvents reads all events and renders them.
+func renderAllEvents(t *testing.T, log EvtHandle, renderer *Renderer) []*sys.Event {
+ t.Helper()
+
+ var events []*sys.Event
+ for {
+ h, done := nextHandle(t, log)
+ if done {
+ break
+ }
+
+ func() {
+ defer h.Close()
+
+ evt, err := renderer.Render(h)
+ if err != nil {
+ t.Fatalf("Render failed: %+v", err)
+ }
+
+ events = append(events, evt)
+ }()
+ }
+
+ return events
+}
+
+// setLogSize set the maximum number of bytes that an event log can hold.
+func setLogSize(t testing.TB, provider string, sizeBytes int) {
+ output, err := exec.Command("wevtutil.exe", "sl", "/ms:"+strconv.Itoa(sizeBytes), provider).CombinedOutput()
+ if err != nil {
+ t.Fatal("failed to set log size", err, string(output))
+ }
+}
+
+func BenchmarkRenderer(b *testing.B) {
+ writer, teardown := createLog(b)
+ defer teardown()
+
+ setLogSize(b, winlogbeatTestLogName, 1024*1024*1024) // 1 GiB
+
+ const totalEvents = 1000000
+ msg := strings.Repeat("Hello world! ", 21)
+ for i := 0; i < totalEvents; i++ {
+ writer.Info(10, msg)
+ }
+
+ setup := func() (*EventIterator, *Renderer) {
+ log := openLog(b, winlogbeatTestLogName)
+
+ itr, err := NewEventIterator(WithSubscription(log), WithBatchSize(1024))
+ if err != nil {
+ log.Close()
+ b.Fatal(err)
+ }
+
+ r, err := NewRenderer(NilHandle, logp.NewLogger("bench"))
+ if err != nil {
+ log.Close()
+ itr.Close()
+ b.Fatal(err)
+ }
+
+ return itr, r
+ }
+
+ b.Run("single_thread", func(b *testing.B) {
+ itr, r := setup()
+ defer itr.Close()
+ defer r.Close()
+
+ count := atomic.NewUint64(0)
+ start := time.Now()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ // Get next handle.
+ h, ok := itr.Next()
+ if !ok {
+ b.Fatal("Ran out of events before benchmark was done.", itr.Err())
+ }
+
+ // Render it.
+ _, err := r.Render(h)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ count.Inc()
+ }
+
+ elapsed := time.Since(start)
+ b.ReportMetric(float64(count.Load())/elapsed.Seconds(), "events/sec")
+ })
+
+ b.Run("parallel8", func(b *testing.B) {
+ itr, r := setup()
+ defer itr.Close()
+ defer r.Close()
+
+ count := atomic.NewUint64(0)
+ start := time.Now()
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ // Get next handle.
+ h, ok := itr.Next()
+ if !ok {
+ b.Fatal("Ran out of events before benchmark was done.", itr.Err())
+ }
+
+ // Render it.
+ _, err := r.Render(h)
+ if err != nil {
+ b.Fatal(err)
+ }
+ count.Inc()
+ }
+ })
+
+ elapsed := time.Since(start)
+ b.ReportMetric(float64(count.Load())/elapsed.Seconds(), "events/sec")
+ b.ReportMetric(float64(runtime.GOMAXPROCS(0)), "gomaxprocs")
+ })
+}
diff --git a/winlogbeat/sys/wineventlog/stringinserts.go b/winlogbeat/sys/wineventlog/stringinserts.go
new file mode 100644
index 00000000000..347e478b9bd
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/stringinserts.go
@@ -0,0 +1,85 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "strconv"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+const (
+ // maxInsertStrings is the maximum number of parameters supported in a
+ // Windows event message.
+ maxInsertStrings = 99
+)
+
+// templateInserts contains EvtVariant values that can be used to substitute
+// Go text/template expressions into a Windows event message.
+var templateInserts = newTemplateStringInserts()
+
+// stringsInserts holds EvtVariant values with type EvtVarTypeString.
+type stringInserts struct {
+ // insertStrings are slices holding the strings in the EvtVariant (this must
+ // keep a reference to these to prevent GC of the strings as there is
+ // an unsafe reference to them in the evtVariants).
+ insertStrings [maxInsertStrings][]uint16
+ evtVariants [maxInsertStrings]EvtVariant
+}
+
+// Pointer returns a pointer the EvtVariant array.
+func (si *stringInserts) Slice() []EvtVariant {
+ return si.evtVariants[:]
+}
+
+// clear clears the pointers (and unsafe pointers) so that the memory can be
+// garbage collected.
+func (si *stringInserts) clear() {
+ for i := 0; i < len(si.evtVariants); i++ {
+ si.evtVariants[i] = EvtVariant{}
+ si.insertStrings[i] = nil
+ }
+}
+
+// newTemplateStringInserts returns a stringInserts where each value is a
+// Go text/template expression that references an event data parameter.
+func newTemplateStringInserts() *stringInserts {
+ si := &stringInserts{}
+
+ for i := 0; i < len(si.evtVariants); i++ {
+ // Use i+1 to keep our inserts numbered the same as Window's inserts.
+ strSlice, err := windows.UTF16FromString(`{{eventParam $ ` + strconv.Itoa(i+1) + `}}`)
+ if err != nil {
+ // This will never happen.
+ panic(err)
+ }
+
+ si.insertStrings[i] = strSlice
+ si.evtVariants[i] = EvtVariant{
+ Value: uintptr(unsafe.Pointer(&strSlice[0])),
+ Count: uint32(len(strSlice)),
+ Type: EvtVarTypeString,
+ }
+ si.evtVariants[i].Type = EvtVarTypeString
+ }
+
+ return si
+}
diff --git a/winlogbeat/eventlog/common_test.go b/winlogbeat/sys/wineventlog/stringinserts_test.go
similarity index 50%
rename from winlogbeat/eventlog/common_test.go
rename to winlogbeat/sys/wineventlog/stringinserts_test.go
index a7f3f8b2cf1..ffeb2a473d6 100644
--- a/winlogbeat/eventlog/common_test.go
+++ b/winlogbeat/sys/wineventlog/stringinserts_test.go
@@ -15,36 +15,32 @@
// specific language governing permissions and limitations
// under the License.
-package eventlog
+// +build windows
+
+package wineventlog
import (
"testing"
+ "unsafe"
- "github.com/elastic/beats/v7/libbeat/common"
- "github.com/elastic/beats/v7/winlogbeat/checkpoint"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/sys/windows"
)
-type factory func(*common.Config) (EventLog, error)
-type teardown func()
+func TestStringInserts(t *testing.T) {
+ assert.NotNil(t, templateInserts)
-func fatalErr(t *testing.T, err error) {
- if err != nil {
- t.Fatal(err)
- }
-}
+ si := newTemplateStringInserts()
+ defer si.clear()
-func newTestEventLog(t *testing.T, factory factory, options map[string]interface{}) EventLog {
- config, err := common.NewConfigFrom(options)
- fatalErr(t, err)
- eventLog, err := factory(config)
- fatalErr(t, err)
- return eventLog
-}
+ // "The value of n can be a number between 1 and 99."
+ // https://docs.microsoft.com/en-us/windows/win32/eventlog/message-text-files
+ assert.Contains(t, windows.UTF16ToString(si.insertStrings[0]), " 1}")
+ assert.Contains(t, windows.UTF16ToString(si.insertStrings[maxInsertStrings-1]), " 99}")
-func setupEventLog(t *testing.T, factory factory, recordID uint64, options map[string]interface{}) (EventLog, teardown) {
- eventLog := newTestEventLog(t, factory, options)
- fatalErr(t, eventLog.Open(checkpoint.EventLogState{
- RecordNumber: recordID,
- }))
- return eventLog, func() { fatalErr(t, eventLog.Close()) }
+ for i, evtVariant := range si.evtVariants {
+ assert.EqualValues(t, uintptr(unsafe.Pointer(&si.insertStrings[i][0])), evtVariant.Value)
+ assert.Len(t, si.insertStrings[i], int(evtVariant.Count))
+ assert.Equal(t, evtVariant.Type, EvtVarTypeString)
+ }
}
diff --git a/winlogbeat/sys/wineventlog/syscall_windows.go b/winlogbeat/sys/wineventlog/syscall_windows.go
index ed7dfa67224..13beb04fd85 100644
--- a/winlogbeat/sys/wineventlog/syscall_windows.go
+++ b/winlogbeat/sys/wineventlog/syscall_windows.go
@@ -18,24 +18,31 @@
package wineventlog
import (
+ "fmt"
"syscall"
+ "time"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "golang.org/x/sys/windows"
)
// EvtHandle is a handle to the event log.
type EvtHandle uintptr
+func (h EvtHandle) Close() error {
+ return _EvtClose(h)
+}
+
+const NilHandle EvtHandle = 0
+
// Event log error codes.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
const (
- ERROR_INSUFFICIENT_BUFFER syscall.Errno = 122
- ERROR_NO_MORE_ITEMS syscall.Errno = 259
- ERROR_NONE_MAPPED syscall.Errno = 1332
- RPC_S_INVALID_BOUND syscall.Errno = 1734
- ERROR_INVALID_OPERATION syscall.Errno = 4317
- ERROR_EVT_MESSAGE_NOT_FOUND syscall.Errno = 15027
- ERROR_EVT_MESSAGE_ID_NOT_FOUND syscall.Errno = 15028
- ERROR_EVT_UNRESOLVED_VALUE_INSERT syscall.Errno = 15029
- ERROR_EVT_UNRESOLVED_PARAMETER_INSERT syscall.Errno = 15030
+ ERROR_INSUFFICIENT_BUFFER syscall.Errno = 122
+ ERROR_NO_MORE_ITEMS syscall.Errno = 259
+ RPC_S_INVALID_BOUND syscall.Errno = 1734
+ ERROR_INVALID_OPERATION syscall.Errno = 4317
)
// EvtSubscribeFlag defines the possible values that specify when to start subscribing to events.
@@ -184,7 +191,7 @@ const (
var evtSystemMap = map[EvtSystemPropertyID]string{
EvtSystemProviderName: "Provider Name",
- EvtSystemProviderGuid: "Provider GUID",
+ EvtSystemProviderGuid: "Provider PublisherGUID",
EvtSystemEventID: "Event ID",
EvtSystemQualifiers: "Qualifiers",
EvtSystemLevel: "Level",
@@ -299,11 +306,308 @@ const (
EvtSeekStrict EvtSeekFlag = 0x10000
)
-// Add -trace to enable debug prints around syscalls.
-//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go
+type EvtVariantType uint32
+
+const (
+ EvtVarTypeNull EvtVariantType = iota
+ EvtVarTypeString
+ EvtVarTypeAnsiString
+ EvtVarTypeSByte
+ EvtVarTypeByte
+ EvtVarTypeInt16
+ EvtVarTypeUInt16
+ EvtVarTypeInt32
+ EvtVarTypeUInt32
+ EvtVarTypeInt64
+ EvtVarTypeUInt64
+ EvtVarTypeSingle
+ EvtVarTypeDouble
+ EvtVarTypeBoolean
+ EvtVarTypeBinary
+ EvtVarTypeGuid
+ EvtVarTypeSizeT
+ EvtVarTypeFileTime
+ EvtVarTypeSysTime
+ EvtVarTypeSid
+ EvtVarTypeHexInt32
+ EvtVarTypeHexInt64
+ EvtVarTypeEvtHandle EvtVariantType = 32
+ EvtVarTypeEvtXml EvtVariantType = 35
+)
+
+var evtVariantTypeNames = map[EvtVariantType]string{
+ EvtVarTypeNull: "null",
+ EvtVarTypeString: "string",
+ EvtVarTypeAnsiString: "ansi_string",
+ EvtVarTypeSByte: "signed_byte",
+ EvtVarTypeByte: "unsigned byte",
+ EvtVarTypeInt16: "int16",
+ EvtVarTypeUInt16: "uint16",
+ EvtVarTypeInt32: "int32",
+ EvtVarTypeUInt32: "uint32",
+ EvtVarTypeInt64: "int64",
+ EvtVarTypeUInt64: "uint64",
+ EvtVarTypeSingle: "float32",
+ EvtVarTypeDouble: "float64",
+ EvtVarTypeBoolean: "boolean",
+ EvtVarTypeBinary: "binary",
+ EvtVarTypeGuid: "guid",
+ EvtVarTypeSizeT: "size_t",
+ EvtVarTypeFileTime: "filetime",
+ EvtVarTypeSysTime: "systemtime",
+ EvtVarTypeSid: "sid",
+ EvtVarTypeHexInt32: "hex_int32",
+ EvtVarTypeHexInt64: "hex_int64",
+ EvtVarTypeEvtHandle: "evt_handle",
+ EvtVarTypeEvtXml: "evt_xml",
+}
+
+func (t EvtVariantType) Mask() EvtVariantType {
+ return t & EvtVariantTypeMask
+}
+
+func (t EvtVariantType) IsArray() bool {
+ return t&EvtVariantTypeArray > 0
+}
+
+func (t EvtVariantType) String() string {
+ return evtVariantTypeNames[t.Mask()]
+}
+
+const (
+ EvtVariantTypeMask = 0x7f
+ EvtVariantTypeArray = 128
+)
+
+type EvtVariant struct {
+ Value uintptr
+ Count uint32
+ Type EvtVariantType
+}
+
+var sizeofEvtVariant = unsafe.Sizeof(EvtVariant{})
+
+type hexInt32 int32
+
+func (n hexInt32) String() string {
+ return fmt.Sprintf("%#x", uint32(n))
+}
+
+type hexInt64 int64
+
+func (n hexInt64) String() string {
+ return fmt.Sprintf("%#x", uint64(n))
+}
+
+func (v EvtVariant) Data(buf []byte) (interface{}, error) {
+ typ := v.Type.Mask()
+ switch typ {
+ case EvtVarTypeNull:
+ return nil, nil
+ case EvtVarTypeString:
+ addr := unsafe.Pointer(&buf[0])
+ offset := v.Value - uintptr(addr)
+ s, err := UTF16BytesToString(buf[offset:])
+ return s, err
+ case EvtVarTypeSByte:
+ return int8(v.Value), nil
+ case EvtVarTypeByte:
+ return uint8(v.Value), nil
+ case EvtVarTypeInt16:
+ return int16(v.Value), nil
+ case EvtVarTypeInt32:
+ return int32(v.Value), nil
+ case EvtVarTypeHexInt32:
+ return hexInt32(v.Value), nil
+ case EvtVarTypeInt64:
+ return int64(v.Value), nil
+ case EvtVarTypeHexInt64:
+ return hexInt64(v.Value), nil
+ case EvtVarTypeUInt16:
+ return uint16(v.Value), nil
+ case EvtVarTypeUInt32:
+ return uint32(v.Value), nil
+ case EvtVarTypeUInt64:
+ return uint64(v.Value), nil
+ case EvtVarTypeSingle:
+ return float32(v.Value), nil
+ case EvtVarTypeDouble:
+ return float64(v.Value), nil
+ case EvtVarTypeBoolean:
+ if v.Value == 0 {
+ return false, nil
+ }
+ return true, nil
+ case EvtVarTypeGuid:
+ addr := unsafe.Pointer(&buf[0])
+ offset := v.Value - uintptr(addr)
+ guid := (*windows.GUID)(unsafe.Pointer(&buf[offset]))
+ copy := *guid
+ return copy, nil
+ case EvtVarTypeFileTime:
+ ft := (*windows.Filetime)(unsafe.Pointer(&v.Value))
+ return time.Unix(0, ft.Nanoseconds()).UTC(), nil
+ case EvtVarTypeSid:
+ addr := unsafe.Pointer(&buf[0])
+ offset := v.Value - uintptr(addr)
+ sidPtr := (*windows.SID)(unsafe.Pointer(&buf[offset]))
+ return sidPtr.Copy()
+ case EvtVarTypeEvtHandle:
+ return EvtHandle(v.Value), nil
+ default:
+ return nil, errors.Errorf("unhandled type: %d", typ)
+ }
+}
+
+type EvtEventMetadataPropertyID uint32
+
+const (
+ EventMetadataEventID EvtEventMetadataPropertyID = iota
+ EventMetadataEventVersion
+ EventMetadataEventChannel
+ EventMetadataEventLevel
+ EventMetadataEventOpcode
+ EventMetadataEventTask
+ EventMetadataEventKeyword
+ EventMetadataEventMessageID
+ EventMetadataEventTemplate
+)
+
+type EvtPublisherMetadataPropertyID uint32
+
+const (
+ EvtPublisherMetadataPublisherGuid EvtPublisherMetadataPropertyID = iota
+ EvtPublisherMetadataResourceFilePath
+ EvtPublisherMetadataParameterFilePath
+ EvtPublisherMetadataMessageFilePath
+ EvtPublisherMetadataHelpLink
+ EvtPublisherMetadataPublisherMessageID
+ EvtPublisherMetadataChannelReferences
+ EvtPublisherMetadataChannelReferencePath
+ EvtPublisherMetadataChannelReferenceIndex
+ EvtPublisherMetadataChannelReferenceID
+ EvtPublisherMetadataChannelReferenceFlags
+ EvtPublisherMetadataChannelReferenceMessageID
+ EvtPublisherMetadataLevels
+ EvtPublisherMetadataLevelName
+ EvtPublisherMetadataLevelValue
+ EvtPublisherMetadataLevelMessageID
+ EvtPublisherMetadataTasks
+ EvtPublisherMetadataTaskName
+ EvtPublisherMetadataTaskEventGuid
+ EvtPublisherMetadataTaskValue
+ EvtPublisherMetadataTaskMessageID
+ EvtPublisherMetadataOpcodes
+ EvtPublisherMetadataOpcodeName
+ EvtPublisherMetadataOpcodeValue
+ EvtPublisherMetadataOpcodeMessageID
+ EvtPublisherMetadataKeywords
+ EvtPublisherMetadataKeywordName
+ EvtPublisherMetadataKeywordValue
+ EvtPublisherMetadataKeywordMessageID
+)
+
+func EvtGetPublisherMetadataProperty(publisherMetadataHandle EvtHandle, propertyID EvtPublisherMetadataPropertyID) (interface{}, error) {
+ var bufferUsed uint32
+ err := _EvtGetPublisherMetadataProperty(publisherMetadataHandle, propertyID, 0, 0, nil, &bufferUsed)
+ if err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return "", errors.Errorf("expected ERROR_INSUFFICIENT_BUFFER but got %v", err)
+ }
+
+ buf := make([]byte, bufferUsed)
+ pEvtVariant := (*EvtVariant)(unsafe.Pointer(&buf[0]))
+ err = _EvtGetPublisherMetadataProperty(publisherMetadataHandle, propertyID, 0, uint32(len(buf)), pEvtVariant, &bufferUsed)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed in EvtGetPublisherMetadataProperty")
+ }
+
+ v, err := pEvtVariant.Data(buf)
+ if err != nil {
+ return nil, err
+ }
+
+ switch t := v.(type) {
+ case EvtHandle:
+ return EvtObjectArrayPropertyHandle(t), nil
+ default:
+ return v, nil
+ }
+}
+
+func EvtGetObjectArrayProperty(arrayHandle EvtObjectArrayPropertyHandle, propertyID EvtPublisherMetadataPropertyID, index uint32) (interface{}, error) {
+ var bufferUsed uint32
+ err := _EvtGetObjectArrayProperty(arrayHandle, propertyID, index, 0, 0, nil, &bufferUsed)
+ if err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return nil, errors.Wrap(err, "failed in EvtGetObjectArrayProperty, expected ERROR_INSUFFICIENT_BUFFER")
+ }
+
+ buf := make([]byte, bufferUsed)
+ pEvtVariant := (*EvtVariant)(unsafe.Pointer(&buf[0]))
+ err = _EvtGetObjectArrayProperty(arrayHandle, propertyID, index, 0, uint32(len(buf)), pEvtVariant, &bufferUsed)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed in EvtGetObjectArrayProperty")
+ }
+
+ value, err := pEvtVariant.Data(buf)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to read EVT_VARIANT value")
+ }
+ return value, nil
+}
+
+type EvtObjectArrayPropertyHandle uint32
+
+func (h EvtObjectArrayPropertyHandle) Close() error {
+ return _EvtClose(EvtHandle(h))
+}
+
+func EvtGetObjectArraySize(handle EvtObjectArrayPropertyHandle) (uint32, error) {
+ var arrayLen uint32
+ if err := _EvtGetObjectArraySize(handle, &arrayLen); err != nil {
+ return 0, err
+ }
+ return arrayLen, nil
+}
+
+func GetEventMetadataProperty(metadataHandle EvtHandle, propertyID EvtEventMetadataPropertyID) (interface{}, error) {
+ var bufferUsed uint32
+ err := _EvtGetEventMetadataProperty(metadataHandle, 8, 0, 0, nil, &bufferUsed)
+ if err != windows.ERROR_INSUFFICIENT_BUFFER {
+ return nil, errors.Errorf("expected ERROR_INSUFFICIENT_BUFFER but got %v", err)
+ }
+
+ buf := make([]byte, bufferUsed)
+ pEvtVariant := (*EvtVariant)(unsafe.Pointer(&buf[0]))
+ err = _EvtGetEventMetadataProperty(metadataHandle, propertyID, 0, uint32(len(buf)), pEvtVariant, &bufferUsed)
+ if err != nil {
+ return nil, errors.Wrap(err, "_EvtGetEventMetadataProperty")
+ }
+
+ return pEvtVariant.Data(buf)
+}
+
+// EvtClearLog removes all events from the specified channel and writes them to
+// the target log file.
+func EvtClearLog(session EvtHandle, channelPath string, targetFilePath string) error {
+ channel, err := windows.UTF16PtrFromString(channelPath)
+ if err != nil {
+ return err
+ }
+
+ var target *uint16
+ if targetFilePath != "" {
+ target, err = windows.UTF16PtrFromString(targetFilePath)
+ if err != nil {
+ return err
+ }
+ }
+
+ return _EvtClearLog(session, channel, target, 0)
+}
// Windows API calls
//sys _EvtOpenLog(session EvtHandle, path *uint16, flags uint32) (handle EvtHandle, err error) = wevtapi.EvtOpenLog
+//sys _EvtClearLog(session EvtHandle, channelPath *uint16, targetFilePath *uint16, flags uint32) (err error) = wevtapi.EvtClearLog
//sys _EvtQuery(session EvtHandle, path *uint16, query *uint16, flags uint32) (handle EvtHandle, err error) = wevtapi.EvtQuery
//sys _EvtSubscribe(session EvtHandle, signalEvent uintptr, channelPath *uint16, query *uint16, bookmark EvtHandle, context uintptr, callback syscall.Handle, flags EvtSubscribeFlag) (handle EvtHandle, err error) = wevtapi.EvtSubscribe
//sys _EvtCreateBookmark(bookmarkXML *uint16) (handle EvtHandle, err error) = wevtapi.EvtCreateBookmark
@@ -317,5 +621,9 @@ const (
//sys _EvtNextChannelPath(channelEnum EvtHandle, channelPathBufferSize uint32, channelPathBuffer *uint16, channelPathBufferUsed *uint32) (err error) = wevtapi.EvtNextChannelPath
//sys _EvtFormatMessage(publisherMetadata EvtHandle, event EvtHandle, messageID uint32, valueCount uint32, values uintptr, flags EvtFormatMessageFlag, bufferSize uint32, buffer *byte, bufferUsed *uint32) (err error) = wevtapi.EvtFormatMessage
//sys _EvtOpenPublisherMetadata(session EvtHandle, publisherIdentity *uint16, logFilePath *uint16, locale uint32, flags uint32) (handle EvtHandle, err error) = wevtapi.EvtOpenPublisherMetadata
-
-//sys _StringFromGUID2(rguid *syscall.GUID, pStr *uint16, strSize uint32) (err error) = ole32.StringFromGUID2
+//sys _EvtGetPublisherMetadataProperty(publisherMetadata EvtHandle, propertyID EvtPublisherMetadataPropertyID, flags uint32, bufferSize uint32, variant *EvtVariant, bufferUsed *uint32) (err error) = wevtapi.EvtGetPublisherMetadataProperty
+//sys _EvtGetEventMetadataProperty(eventMetadata EvtHandle, propertyID EvtEventMetadataPropertyID, flags uint32, bufferSize uint32, variant *EvtVariant, bufferUsed *uint32) (err error) = wevtapi.EvtGetEventMetadataProperty
+//sys _EvtOpenEventMetadataEnum(publisherMetadata EvtHandle, flags uint32) (handle EvtHandle, err error) = wevtapi.EvtOpenEventMetadataEnum
+//sys _EvtNextEventMetadata(enumerator EvtHandle, flags uint32) (handle EvtHandle, err error) = wevtapi.EvtNextEventMetadata
+//sys _EvtGetObjectArrayProperty(objectArray EvtObjectArrayPropertyHandle, propertyID EvtPublisherMetadataPropertyID, arrayIndex uint32, flags uint32, bufferSize uint32, evtVariant *EvtVariant, bufferUsed *uint32) (err error) = wevtapi.EvtGetObjectArrayProperty
+//sys _EvtGetObjectArraySize(objectArray EvtObjectArrayPropertyHandle, arraySize *uint32) (err error) = wevtapi.EvtGetObjectArraySize
diff --git a/winlogbeat/sys/wineventlog/template.go b/winlogbeat/sys/wineventlog/template.go
new file mode 100644
index 00000000000..e5b5c9b99ae
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/template.go
@@ -0,0 +1,35 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package wineventlog
+
+import (
+ "encoding/xml"
+)
+
+type eventTemplate struct {
+ Data []eventData `xml:"data"`
+}
+
+type eventData struct {
+ Name string `xml:"name,attr"`
+ Type string `xml:"outType,attr"`
+}
+
+func (t *eventTemplate) Unmarshal(xmlData []byte) error {
+ return xml.Unmarshal(xmlData, t)
+}
diff --git a/winlogbeat/sys/wineventlog/template_test.go b/winlogbeat/sys/wineventlog/template_test.go
new file mode 100644
index 00000000000..94b23fb9d1d
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/template_test.go
@@ -0,0 +1,43 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package wineventlog
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEventTemplateUnmarshal(t *testing.T) {
+ const xmlTemplate = `
+
+
+
+
+
+
+
+
+
+
+`
+
+ et := &eventTemplate{}
+ assert.NoError(t, et.Unmarshal([]byte(xmlTemplate)))
+ assert.Len(t, et.Data, 8)
+}
diff --git a/winlogbeat/sys/wineventlog/testdata/application-windows-error-reporting.evtx b/winlogbeat/sys/wineventlog/testdata/application-windows-error-reporting.evtx
new file mode 100644
index 00000000000..c37324d05be
Binary files /dev/null and b/winlogbeat/sys/wineventlog/testdata/application-windows-error-reporting.evtx differ
diff --git a/winlogbeat/sys/wineventlog/util_test.go b/winlogbeat/sys/wineventlog/util_test.go
new file mode 100644
index 00000000000..2f6a121a096
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/util_test.go
@@ -0,0 +1,151 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/andrewkroh/sys/windows/svc/eventlog"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ winlogbeatTestLogName = "WinlogbeatTestGo"
+
+ security4752File = "../../../x-pack/winlogbeat/module/security/test/testdata/4752.evtx"
+ sysmon9File = "../../../x-pack/winlogbeat/module/sysmon/test/testdata/sysmon-9.01.evtx"
+ winErrorReportingFile = "testdata/application-windows-error-reporting.evtx"
+)
+
+// createLog creates a new event log and returns a handle for writing events
+// to the log.
+func createLog(t testing.TB) (log *eventlog.Log, tearDown func()) {
+ const name = winlogbeatTestLogName
+ const source = "wineventlog_pkg"
+
+ existed, err := eventlog.InstallAsEventCreate(name, source, eventlog.Error|eventlog.Warning|eventlog.Info)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if existed {
+ EvtClearLog(NilHandle, name, "")
+ }
+
+ log, err = eventlog.Open(source)
+ if err != nil {
+ eventlog.RemoveSource(name, source)
+ eventlog.RemoveProvider(name)
+ t.Fatal(err)
+ }
+
+ tearDown = func() {
+ log.Close()
+ EvtClearLog(NilHandle, name, "")
+ eventlog.RemoveSource(name, source)
+ eventlog.RemoveProvider(name)
+ }
+
+ return log, tearDown
+}
+
+// openLog opens an event log or .evtx file for reading.
+func openLog(t testing.TB, log string, eventIDFilters ...string) EvtHandle {
+ var (
+ err error
+ path = log
+ flags EvtQueryFlag = EvtQueryReverseDirection
+ )
+
+ if info, err := os.Stat(log); err == nil && info.Mode().IsRegular() {
+ flags |= EvtQueryFilePath
+ } else {
+ flags |= EvtQueryChannelPath
+ }
+
+ var query string
+ if len(eventIDFilters) > 0 {
+ // Convert to URI.
+ abs, err := filepath.Abs(log)
+ if err != nil {
+ t.Fatal(err)
+ }
+ path = "file://" + filepath.ToSlash(abs)
+
+ query, err = Query{Log: path, EventID: strings.Join(eventIDFilters, ",")}.Build()
+ if err != nil {
+ t.Fatal(err)
+ }
+ path = ""
+ }
+
+ h, err := EvtQuery(NilHandle, path, query, flags)
+ if err != nil {
+ t.Fatal("Failed to open log", log, err)
+ }
+ return h
+}
+
+// nextHandle reads one handle from the log. It returns done=true when there
+// are no more items to read.
+func nextHandle(t *testing.T, log EvtHandle) (handle EvtHandle, done bool) {
+ var numReturned uint32
+ var handles [1]EvtHandle
+
+ err := _EvtNext(log, 1, &handles[0], 0, 0, &numReturned)
+ if err != nil {
+ if err == windows.ERROR_NO_MORE_ITEMS {
+ return NilHandle, true
+ }
+ t.Fatal(err)
+ }
+
+ return handles[0], false
+}
+
+// mustNextHandle reads one handle from the log.
+func mustNextHandle(t *testing.T, log EvtHandle) EvtHandle {
+ h, done := nextHandle(t, log)
+ if done {
+ t.Fatal("No more items to read.")
+ }
+ return h
+}
+
+func logAsJSON(t testing.TB, object interface{}) {
+ data, err := json.MarshalIndent(object, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(string(data))
+}
+
+func assertEqualIgnoreCase(t *testing.T, expected, actual string) {
+ t.Helper()
+ assert.Equal(t,
+ strings.ToLower(expected),
+ strings.ToLower(actual),
+ )
+}
diff --git a/winlogbeat/sys/wineventlog/wineventlog_windows_test.go b/winlogbeat/sys/wineventlog/wineventlog_windows_test.go
index f5411f446b9..9701cfd3679 100644
--- a/winlogbeat/sys/wineventlog/wineventlog_windows_test.go
+++ b/winlogbeat/sys/wineventlog/wineventlog_windows_test.go
@@ -108,7 +108,3 @@ func TestChannels(t *testing.T) {
}
}
}
-
-func TestExtension(t *testing.T) {
- filepath.Ext("sysmon")
-}
diff --git a/winlogbeat/sys/wineventlog/winmeta.go b/winlogbeat/sys/wineventlog/winmeta.go
new file mode 100644
index 00000000000..f6f90b6c19b
--- /dev/null
+++ b/winlogbeat/sys/wineventlog/winmeta.go
@@ -0,0 +1,58 @@
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// +build windows
+
+package wineventlog
+
+// winMeta contains the static values are a common across Windows. These valuesi
+// are from winmeta.xml inside the Windows SDK.
+var winMeta = &publisherMetadataStore{
+ Keywords: map[int64]string{
+ 0: "AnyKeyword",
+ 0x1000000000000: "Response Time",
+ 0x4000000000000: "WDI Diag",
+ 0x8000000000000: "SQM",
+ 0x10000000000000: "Audit Failure",
+ 0x20000000000000: "Audit Success",
+ 0x40000000000000: "Correlation Hint",
+ 0x80000000000000: "Classic",
+ },
+ Opcodes: map[uint8]string{
+ 0: "Info",
+ 1: "Start",
+ 2: "Stop",
+ 3: "DCStart",
+ 4: "DCStop",
+ 5: "Extension",
+ 6: "Reply",
+ 7: "Resume",
+ 8: "Suspend",
+ 9: "Send",
+ },
+ Levels: map[uint8]string{
+ 0: "Information", // "Log Always", but Event Viewer shows Information.
+ 1: "Critical",
+ 2: "Error",
+ 3: "Warning",
+ 4: "Information",
+ 5: "Verbose",
+ },
+ Tasks: map[uint16]string{
+ 0: "None",
+ },
+}
diff --git a/winlogbeat/sys/wineventlog/zsyscall_windows.go b/winlogbeat/sys/wineventlog/zsyscall_windows.go
index 0e15ef6a3d6..2dbe865c1a3 100644
--- a/winlogbeat/sys/wineventlog/zsyscall_windows.go
+++ b/winlogbeat/sys/wineventlog/zsyscall_windows.go
@@ -55,23 +55,28 @@ func errnoErr(e syscall.Errno) error {
var (
modwevtapi = windows.NewLazySystemDLL("wevtapi.dll")
- modole32 = windows.NewLazySystemDLL("ole32.dll")
- procEvtOpenLog = modwevtapi.NewProc("EvtOpenLog")
- procEvtQuery = modwevtapi.NewProc("EvtQuery")
- procEvtSubscribe = modwevtapi.NewProc("EvtSubscribe")
- procEvtCreateBookmark = modwevtapi.NewProc("EvtCreateBookmark")
- procEvtUpdateBookmark = modwevtapi.NewProc("EvtUpdateBookmark")
- procEvtCreateRenderContext = modwevtapi.NewProc("EvtCreateRenderContext")
- procEvtRender = modwevtapi.NewProc("EvtRender")
- procEvtClose = modwevtapi.NewProc("EvtClose")
- procEvtSeek = modwevtapi.NewProc("EvtSeek")
- procEvtNext = modwevtapi.NewProc("EvtNext")
- procEvtOpenChannelEnum = modwevtapi.NewProc("EvtOpenChannelEnum")
- procEvtNextChannelPath = modwevtapi.NewProc("EvtNextChannelPath")
- procEvtFormatMessage = modwevtapi.NewProc("EvtFormatMessage")
- procEvtOpenPublisherMetadata = modwevtapi.NewProc("EvtOpenPublisherMetadata")
- procStringFromGUID2 = modole32.NewProc("StringFromGUID2")
+ procEvtOpenLog = modwevtapi.NewProc("EvtOpenLog")
+ procEvtClearLog = modwevtapi.NewProc("EvtClearLog")
+ procEvtQuery = modwevtapi.NewProc("EvtQuery")
+ procEvtSubscribe = modwevtapi.NewProc("EvtSubscribe")
+ procEvtCreateBookmark = modwevtapi.NewProc("EvtCreateBookmark")
+ procEvtUpdateBookmark = modwevtapi.NewProc("EvtUpdateBookmark")
+ procEvtCreateRenderContext = modwevtapi.NewProc("EvtCreateRenderContext")
+ procEvtRender = modwevtapi.NewProc("EvtRender")
+ procEvtClose = modwevtapi.NewProc("EvtClose")
+ procEvtSeek = modwevtapi.NewProc("EvtSeek")
+ procEvtNext = modwevtapi.NewProc("EvtNext")
+ procEvtOpenChannelEnum = modwevtapi.NewProc("EvtOpenChannelEnum")
+ procEvtNextChannelPath = modwevtapi.NewProc("EvtNextChannelPath")
+ procEvtFormatMessage = modwevtapi.NewProc("EvtFormatMessage")
+ procEvtOpenPublisherMetadata = modwevtapi.NewProc("EvtOpenPublisherMetadata")
+ procEvtGetPublisherMetadataProperty = modwevtapi.NewProc("EvtGetPublisherMetadataProperty")
+ procEvtGetEventMetadataProperty = modwevtapi.NewProc("EvtGetEventMetadataProperty")
+ procEvtOpenEventMetadataEnum = modwevtapi.NewProc("EvtOpenEventMetadataEnum")
+ procEvtNextEventMetadata = modwevtapi.NewProc("EvtNextEventMetadata")
+ procEvtGetObjectArrayProperty = modwevtapi.NewProc("EvtGetObjectArrayProperty")
+ procEvtGetObjectArraySize = modwevtapi.NewProc("EvtGetObjectArraySize")
)
func _EvtOpenLog(session EvtHandle, path *uint16, flags uint32) (handle EvtHandle, err error) {
@@ -87,6 +92,18 @@ func _EvtOpenLog(session EvtHandle, path *uint16, flags uint32) (handle EvtHandl
return
}
+func _EvtClearLog(session EvtHandle, channelPath *uint16, targetFilePath *uint16, flags uint32) (err error) {
+ r1, _, e1 := syscall.Syscall6(procEvtClearLog.Addr(), 4, uintptr(session), uintptr(unsafe.Pointer(channelPath)), uintptr(unsafe.Pointer(targetFilePath)), uintptr(flags), 0, 0)
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
func _EvtQuery(session EvtHandle, path *uint16, query *uint16, flags uint32) (handle EvtHandle, err error) {
r0, _, e1 := syscall.Syscall6(procEvtQuery.Addr(), 4, uintptr(session), uintptr(unsafe.Pointer(path)), uintptr(unsafe.Pointer(query)), uintptr(flags), 0, 0)
handle = EvtHandle(r0)
@@ -250,8 +267,70 @@ func _EvtOpenPublisherMetadata(session EvtHandle, publisherIdentity *uint16, log
return
}
-func _StringFromGUID2(rguid *syscall.GUID, pStr *uint16, strSize uint32) (err error) {
- r1, _, e1 := syscall.Syscall(procStringFromGUID2.Addr(), 3, uintptr(unsafe.Pointer(rguid)), uintptr(unsafe.Pointer(pStr)), uintptr(strSize))
+func _EvtGetPublisherMetadataProperty(publisherMetadata EvtHandle, propertyID EvtPublisherMetadataPropertyID, flags uint32, bufferSize uint32, variant *EvtVariant, bufferUsed *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall6(procEvtGetPublisherMetadataProperty.Addr(), 6, uintptr(publisherMetadata), uintptr(propertyID), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(variant)), uintptr(unsafe.Pointer(bufferUsed)))
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func _EvtGetEventMetadataProperty(eventMetadata EvtHandle, propertyID EvtEventMetadataPropertyID, flags uint32, bufferSize uint32, variant *EvtVariant, bufferUsed *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall6(procEvtGetEventMetadataProperty.Addr(), 6, uintptr(eventMetadata), uintptr(propertyID), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(variant)), uintptr(unsafe.Pointer(bufferUsed)))
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func _EvtOpenEventMetadataEnum(publisherMetadata EvtHandle, flags uint32) (handle EvtHandle, err error) {
+ r0, _, e1 := syscall.Syscall(procEvtOpenEventMetadataEnum.Addr(), 2, uintptr(publisherMetadata), uintptr(flags), 0)
+ handle = EvtHandle(r0)
+ if handle == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func _EvtNextEventMetadata(enumerator EvtHandle, flags uint32) (handle EvtHandle, err error) {
+ r0, _, e1 := syscall.Syscall(procEvtNextEventMetadata.Addr(), 2, uintptr(enumerator), uintptr(flags), 0)
+ handle = EvtHandle(r0)
+ if handle == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func _EvtGetObjectArrayProperty(objectArray EvtObjectArrayPropertyHandle, propertyID EvtPublisherMetadataPropertyID, arrayIndex uint32, flags uint32, bufferSize uint32, evtVariant *EvtVariant, bufferUsed *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall9(procEvtGetObjectArrayProperty.Addr(), 7, uintptr(objectArray), uintptr(propertyID), uintptr(arrayIndex), uintptr(flags), uintptr(bufferSize), uintptr(unsafe.Pointer(evtVariant)), uintptr(unsafe.Pointer(bufferUsed)), 0, 0)
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
+func _EvtGetObjectArraySize(objectArray EvtObjectArrayPropertyHandle, arraySize *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall(procEvtGetObjectArraySize.Addr(), 2, uintptr(objectArray), uintptr(unsafe.Pointer(arraySize)), 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)