557 lines
15 KiB
Go
557 lines
15 KiB
Go
|
// 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 (
|
||
|
"fmt"
|
||
|
"os/exec"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
|
||
|
elog "github.com/andrewkroh/sys/windows/svc/eventlog"
|
||
|
"github.com/joeshaw/multierror"
|
||
|
"github.com/stretchr/testify/assert"
|
||
|
|
||
|
"github.com/elastic/beats/libbeat/logp"
|
||
|
"github.com/elastic/beats/winlogbeat/checkpoint"
|
||
|
"github.com/elastic/beats/winlogbeat/sys/eventlogging"
|
||
|
)
|
||
|
|
||
|
// Names that are registered by the test for logging events.
|
||
|
const (
|
||
|
providerName = "WinlogbeatTestGo"
|
||
|
sourceName = "Integration Test"
|
||
|
)
|
||
|
|
||
|
// Event message files used when logging events.
|
||
|
const (
|
||
|
// EventCreate.exe has valid event IDs in the range of 1-1000 where each
|
||
|
// event message requires a single parameter.
|
||
|
eventCreateMsgFile = "%SystemRoot%\\System32\\EventCreate.exe"
|
||
|
// services.exe is used by the Service Control Manager as its event message
|
||
|
// file; these tests use it to log messages with more than one parameter.
|
||
|
servicesMsgFile = "%SystemRoot%\\System32\\services.exe"
|
||
|
// netevent.dll has messages that require no message parameters.
|
||
|
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,
|
||
|
message: "Hmmmm.",
|
||
|
},
|
||
|
2: {
|
||
|
eventType: elog.Success,
|
||
|
message: "I am so blue I'm greener than purple.",
|
||
|
},
|
||
|
3: {
|
||
|
eventType: elog.Warning,
|
||
|
message: "I stepped on a Corn Flake, now I'm a Cereal Killer.",
|
||
|
},
|
||
|
4: {
|
||
|
eventType: elog.Error,
|
||
|
message: "The quick brown fox jumps over the lazy dog.",
|
||
|
},
|
||
|
5: {
|
||
|
eventType: elog.AuditSuccess,
|
||
|
message: "Where do random thoughts come from?",
|
||
|
},
|
||
|
6: {
|
||
|
eventType: elog.AuditFailure,
|
||
|
message: "Login failure for user xyz!",
|
||
|
},
|
||
|
}
|
||
|
|
||
|
var oneTimeLogpInit sync.Once
|
||
|
|
||
|
// Initializes logp if the verbose flag was set.
|
||
|
func configureLogp() {
|
||
|
oneTimeLogpInit.Do(func() {
|
||
|
if testing.Verbose() {
|
||
|
logp.DevelopmentSetup(logp.WithSelectors("eventlog"))
|
||
|
logp.Info("DEBUG enabled for eventlog.")
|
||
|
} 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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Publish test messages:
|
||
|
for k, m := range messages {
|
||
|
err = log.Report(m.eventType, k, []string{m.message})
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Validate messages:
|
||
|
assert.Len(t, records, len(messages))
|
||
|
for _, record := range records {
|
||
|
t.Log(record)
|
||
|
m, exists := messages[record.EventIdentifier.ID]
|
||
|
if !exists {
|
||
|
t.Errorf("Unknown EventId %d Read() from event log. %v", record.EventIdentifier.ID, record)
|
||
|
continue
|
||
|
}
|
||
|
assert.Equal(t, eventlogging.EventType(m.eventType).String(), record.Level)
|
||
|
assert.Equal(t, m.message, strings.TrimRight(record.Message, "\r\n"))
|
||
|
}
|
||
|
|
||
|
// Validate getNumberOfEventLogRecords returns the correct number of messages.
|
||
|
numMessages, err := eventlogging.GetNumberOfEventLogRecords(eventlogging.Handle(log.Handle))
|
||
|
assert.NoError(t, err)
|
||
|
assert.Equal(t, len(messages), int(numMessages))
|
||
|
}
|
||
|
|
||
|
// Verify that messages whose text is larger than the read buffer cause a
|
||
|
// message error to be returned. Normally Winlogbeat is run with the largest
|
||
|
// 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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
message := "Hello"
|
||
|
err = log.Report(elog.Info, 1, []string{message})
|
||
|
if 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
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{
|
||
|
"name": providerName,
|
||
|
// Use a buffer smaller than what is required.
|
||
|
"format_buffer_size": requiredBufferSize / 2,
|
||
|
})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Validate messages:
|
||
|
assert.Len(t, records, 1)
|
||
|
for _, record := range records {
|
||
|
t.Log(record)
|
||
|
assert.Equal(t, "The data area passed to a system call is too small.", record.RenderErr)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test that when an unknown Event ID is found, that a message containing the
|
||
|
// 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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var eventID uint32 = 1000
|
||
|
msg := "Test Message"
|
||
|
err = log.Success(eventID, msg)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Verify the error message:
|
||
|
assert.Len(t, records, 1)
|
||
|
if len(records) != 1 {
|
||
|
t.FailNow()
|
||
|
}
|
||
|
assert.Equal(t, eventID, records[0].EventIdentifier.ID)
|
||
|
assert.Equal(t, msg, records[0].EventData.Pairs[0].Value)
|
||
|
assert.NotNil(t, records[0].RenderErr)
|
||
|
assert.Equal(t, "", records[0].Message)
|
||
|
}
|
||
|
|
||
|
// Test that multiple event message files are searched for an event ID. This
|
||
|
// test configures the "EventMessageFile" registry value as a semi-color
|
||
|
// separated list of files. If the message for an event ID is not found in one
|
||
|
// 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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var eventID uint32 = 1000
|
||
|
msg := "Test Message"
|
||
|
err = log.Success(eventID, msg)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Verify the error message:
|
||
|
assert.Len(t, records, 1)
|
||
|
if len(records) != 1 {
|
||
|
t.FailNow()
|
||
|
}
|
||
|
assert.Equal(t, eventID, records[0].EventIdentifier.ID)
|
||
|
assert.Equal(t, msg, strings.TrimRight(records[0].Message, "\r\n"))
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// EventID observed by exporting system event log to XML and doing calculation.
|
||
|
// <EventID Qualifiers="16384">7036</EventID>
|
||
|
// 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."
|
||
|
msgs := []string{"Windows Update", "running"}
|
||
|
err = log.Report(elog.Info, eventID, msgs)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Verify the message contents:
|
||
|
assert.Len(t, records, 1)
|
||
|
if len(records) != 1 {
|
||
|
t.FailNow()
|
||
|
}
|
||
|
assert.Equal(t, eventID&0xFFFF, records[0].EventIdentifier.ID)
|
||
|
assert.Equal(t, fmt.Sprintf(template, msgs[0], msgs[1]),
|
||
|
strings.TrimRight(records[0].Message, "\r\n"))
|
||
|
}
|
||
|
|
||
|
// Verify that opening an invalid provider succeeds. Windows opens the
|
||
|
// Application event log provider when this happens (unfortunately).
|
||
|
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()
|
||
|
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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var eventID uint32 = 2147489654 // 1<<31 + 6006
|
||
|
template := "The Event log service was stopped."
|
||
|
msgs := []string{}
|
||
|
err = log.Report(elog.Info, eventID, msgs)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Verify the message contents:
|
||
|
assert.Len(t, records, 1)
|
||
|
if len(records) != 1 {
|
||
|
t.FailNow()
|
||
|
}
|
||
|
assert.Equal(t, eventID&0xFFFF, records[0].EventIdentifier.ID)
|
||
|
assert.Equal(t, template,
|
||
|
strings.TrimRight(records[0].Message, "\r\n"))
|
||
|
}
|
||
|
|
||
|
// TestReadWhileCleared tests that the Read method recovers from the event log
|
||
|
// 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})
|
||
|
defer teardown()
|
||
|
|
||
|
log.Info(1, "Message 1")
|
||
|
log.Info(2, "Message 2")
|
||
|
lr, err := eventlog.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, err, "Expected 0 messages but received error")
|
||
|
assert.Len(t, lr, 0, "Expected 0 message")
|
||
|
|
||
|
log.Info(3, "Message 3")
|
||
|
lr, err = eventlog.Read()
|
||
|
assert.NoError(t, err, "Expected 1 message but received error")
|
||
|
assert.Len(t, lr, 1, "Expected 1 message")
|
||
|
if len(lr) > 0 {
|
||
|
assert.Equal(t, uint32(3), lr[0].EventIdentifier.ID)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Test event messages that include less parameters than required for message
|
||
|
// formating (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)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var eventID uint32 = 1073748860
|
||
|
// Missing parameters will be substituted by "(null)"
|
||
|
template := "The %s service entered the (null) state."
|
||
|
msgs := []string{"Windows Update"}
|
||
|
err = log.Report(elog.Info, eventID, msgs)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Read messages:
|
||
|
eventlog, teardown := setupEventLogging(t, 0, map[string]interface{}{"name": providerName})
|
||
|
defer teardown()
|
||
|
|
||
|
records, err := eventlog.Read()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Verify the message contents:
|
||
|
assert.Len(t, records, 1)
|
||
|
if len(records) != 1 {
|
||
|
t.FailNow()
|
||
|
}
|
||
|
assert.Equal(t, eventID&0xFFFF, records[0].EventIdentifier.ID)
|
||
|
assert.Equal(t, fmt.Sprintf(template, msgs[0]),
|
||
|
strings.TrimRight(records[0].Message, "\r\n"))
|
||
|
}
|
||
|
|
||
|
func newTestEventLogging(t *testing.T, options map[string]interface{}) EventLog {
|
||
|
return newTestEventLog(t, newEventLogging, 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.
|