Skip to content

Commit b01a62c

Browse files
authored
add kernel power query (#44298)
### What does this PR do? Add query for kernel power events. make room for more queries to be added. ### Motivation https://datadoghq.atlassian.net/browse/WINA-1970 Co-authored-by: branden.clark <branden.clark@datadoghq.com>
1 parent d73b4e9 commit b01a62c

6 files changed

Lines changed: 341 additions & 70 deletions

File tree

comp/notableevents/impl/collector.go

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,48 @@ package notableeventsimpl
1010
import (
1111
"context"
1212
"fmt"
13+
"strings"
1314
"sync"
1415
"time"
1516

1617
"github.com/cenkalti/backoff"
18+
"golang.org/x/sys/windows"
1719

20+
"github.com/DataDog/datadog-agent/pkg/logs/util/windowsevent"
1821
"github.com/DataDog/datadog-agent/pkg/util/log"
1922
evtapi "github.com/DataDog/datadog-agent/pkg/util/winutil/eventlog/api"
2023
winevtapi "github.com/DataDog/datadog-agent/pkg/util/winutil/eventlog/api/windows"
2124
evtsubscribe "github.com/DataDog/datadog-agent/pkg/util/winutil/eventlog/subscription"
2225
)
2326

27+
// eventDefinition describes a notable event type and how to collect/process it
28+
type eventDefinition struct {
29+
// Event identification (for lookup after receiving event)
30+
Provider string
31+
EventID uint
32+
33+
// Event metadata (for payload)
34+
EventType string
35+
Title string
36+
Message string
37+
38+
// Query definition - inner content of <Query> block
39+
Channel string
40+
QueryBody string
41+
}
42+
43+
// eventKey uniquely identifies an event by provider and event ID
44+
type eventKey struct {
45+
Provider string
46+
EventID uint
47+
}
48+
2449
// collector monitors Windows Event Log for notable events
2550
type collector struct {
2651
// in
2752
api evtapi.API
28-
channelPath string
2953
query string
54+
eventLookup map[eventKey]*eventDefinition
3055
// out
3156
outChan chan<- eventPayload
3257
// internal
@@ -35,15 +60,63 @@ type collector struct {
3560
wg sync.WaitGroup
3661
}
3762

63+
// getEventDefinitions returns the list of notable events to collect
64+
func getEventDefinitions() []eventDefinition {
65+
e := []eventDefinition{
66+
{
67+
Provider: "Microsoft-Windows-Kernel-Power",
68+
EventID: 41,
69+
Channel: "System",
70+
QueryBody: ` <Select Path="System">*[System[Provider[@Name='Microsoft-Windows-Kernel-Power'] and EventID=41]]</Select>`,
71+
EventType: "Unexpected reboot",
72+
Title: "Unexpected reboot",
73+
Message: "The system has rebooted without cleanly shutting down first",
74+
},
75+
}
76+
return e
77+
}
78+
79+
// buildEventLookup creates a map for fast event definition lookup.
80+
// Returns error if duplicate event keys are found.
81+
func buildEventLookup(events []eventDefinition) (map[eventKey]*eventDefinition, error) {
82+
lookup := make(map[eventKey]*eventDefinition)
83+
for i := range events {
84+
def := &events[i]
85+
key := eventKey{Provider: def.Provider, EventID: def.EventID}
86+
if _, exists := lookup[key]; exists {
87+
return nil, fmt.Errorf("duplicate event definition: %s/%d", def.Provider, def.EventID)
88+
}
89+
lookup[key] = def
90+
}
91+
return lookup, nil
92+
}
93+
94+
// buildQuery generates full XML query from event definitions.
95+
// Each event gets its own <Query> block with auto-generated ID.
96+
func buildQuery(events []eventDefinition) string {
97+
var queries []string
98+
for i, def := range events {
99+
query := fmt.Sprintf(` <Query Id="%d" Path="%s">
100+
%s
101+
</Query>`, i, def.Channel, def.QueryBody)
102+
queries = append(queries, query)
103+
}
104+
return fmt.Sprintf("<QueryList>\n%s\n</QueryList>", strings.Join(queries, "\n"))
105+
}
106+
38107
// newCollector creates a new collector instance
39-
func newCollector(outChan chan<- eventPayload) *collector {
40-
// TODO(WINA-1970): make real query
108+
func newCollector(outChan chan<- eventPayload) (*collector, error) {
109+
events := getEventDefinitions()
110+
lookup, err := buildEventLookup(events)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to build event lookup: %w", err)
113+
}
41114
return &collector{
42115
api: winevtapi.New(),
43-
channelPath: "System",
44-
query: "*[System[(Level=1 or Level=2 or Level=3)]]",
116+
query: buildQuery(events),
45117
outChan: outChan,
46-
}
118+
eventLookup: lookup,
119+
}, nil
47120
}
48121

49122
// start begins monitoring Windows Event Log
@@ -54,12 +127,12 @@ func (c *collector) start() error {
54127

55128
// Create subscription object (will be started in the event loop)
56129
c.sub = evtsubscribe.NewPullSubscription(
57-
c.channelPath,
130+
"", // empty chennel path when XML query is used
58131
c.query,
59132
evtsubscribe.WithWindowsEventLogAPI(c.api),
60133
)
61134

62-
log.Infof("Initialized Windows Event Log subscription: channel=%s, query=%s", c.channelPath, c.query)
135+
log.Debugf("Initialized Windows Event Log subscription: query=%s", c.query)
63136

64137
// Start processing events in background
65138
c.wg.Add(1)
@@ -109,7 +182,7 @@ func (c *collector) run(ctx context.Context) {
109182
// Check if loop should exit
110183
select {
111184
case <-ctx.Done():
112-
log.Info("Notable events collector context cancelled, shutting down")
185+
log.Debug("Notable events collector context cancelled, shutting down")
113186
return
114187
default:
115188
}
@@ -123,7 +196,7 @@ func (c *collector) run(ctx context.Context) {
123196
return err
124197
}
125198
// Subscription started successfully
126-
log.Infof("Started Windows Event Log subscription: channel=%s, query=%s", c.channelPath, c.query)
199+
log.Debugf("Started Windows Event Log subscription: query=%s", c.query)
127200
return nil
128201
})
129202
if err != nil {
@@ -136,7 +209,7 @@ func (c *collector) run(ctx context.Context) {
136209
// Subscription is running, wait for events or cancellation
137210
select {
138211
case <-ctx.Done():
139-
log.Info("Notable events collector context cancelled, shutting down")
212+
log.Debug("Notable events collector context cancelled, shutting down")
140213
return
141214
case events, ok := <-c.sub.GetEvents():
142215
if !ok {
@@ -169,16 +242,45 @@ func (c *collector) processEvent(renderCtx evtapi.EventRenderContextHandle, even
169242
}
170243
defer vals.Close()
171244

172-
// Extract Event ID
245+
// Extract provider and event ID for lookup
246+
providerName, err := vals.String(evtapi.EvtSystemProviderName)
247+
if err != nil {
248+
return fmt.Errorf("failed to get provider name: %w", err)
249+
}
173250
eventID, err := vals.UInt(evtapi.EvtSystemEventID)
174251
if err != nil {
175252
return fmt.Errorf("failed to get event ID: %w", err)
176253
}
177-
providerName, err := vals.String(evtapi.EvtSystemProviderName)
254+
255+
// Lookup event definition
256+
def, found := c.eventLookup[eventKey{Provider: providerName, EventID: uint(eventID)}]
257+
if !found {
258+
return fmt.Errorf("unknown event: %s/%d", providerName, eventID)
259+
}
260+
261+
// Render full event XML
262+
xmlUTF16, err := c.api.EvtRenderEventXml(eventRecord.EventRecordHandle)
178263
if err != nil {
179-
return fmt.Errorf("failed to get provider name: %w", err)
264+
return fmt.Errorf("failed to render event XML: %w", err)
265+
}
266+
xmlString := windows.UTF16ToString(xmlUTF16)
267+
268+
// Convert XML to JSON map using windowsevent package
269+
eventMap, err := windowsevent.NewMapXMLWithOptions([]byte(xmlString), windowsevent.TransformOptions{
270+
FormatEventData: true,
271+
FormatBinaryData: false, // Skip buggy binary transform
272+
NormalizeEventID: true,
273+
})
274+
if err != nil {
275+
return fmt.Errorf("failed to parse event XML: %w", err)
276+
}
277+
278+
// Build custom data with windows_event_log
279+
customData := map[string]interface{}{
280+
"windows_event_log": eventMap.Map,
180281
}
181-
// TODO: get nanoseconds precision from event log
282+
283+
// Get timestamp
182284
var timestamp time.Time
183285
unixTimestamp, err := vals.Time(evtapi.EvtSystemTimeCreated)
184286
if err != nil {
@@ -188,13 +290,15 @@ func (c *collector) processEvent(renderCtx evtapi.EventRenderContextHandle, even
188290
timestamp = time.Unix(unixTimestamp, 0)
189291
}
190292

191-
log.Debugf("Collected notable event: channel=%s, event_id=%d", c.channelPath, eventID)
293+
log.Debugf("Collected notable event: provider=%s, event_id=%d, title=%s", providerName, eventID, def.Title)
192294

295+
// Build and send payload
193296
payload := eventPayload{
194-
Channel: c.channelPath,
195-
Provider: providerName,
196-
EventID: uint(eventID),
197297
Timestamp: timestamp,
298+
EventType: def.EventType,
299+
Title: def.Title,
300+
Message: def.Message,
301+
Custom: customData,
198302
}
199303
c.outChan <- payload
200304

comp/notableevents/impl/collector_test.go

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package notableeventsimpl
99

1010
import (
11+
"fmt"
1112
"slices"
1213
"testing"
1314
"time"
@@ -18,6 +19,102 @@ import (
1819
eventlog_test "github.com/DataDog/datadog-agent/pkg/util/winutil/eventlog/test"
1920
)
2021

22+
// TestBuildEventLookup asserts that events are correctly indexed by provider and event ID.
23+
func TestBuildEventLookup(t *testing.T) {
24+
events := []eventDefinition{
25+
{Provider: "Provider-A", EventID: 1, EventType: "Type1", Title: "Title1", Message: "Msg1"},
26+
{Provider: "Provider-A", EventID: 2, EventType: "Type2", Title: "Title2", Message: "Msg2"},
27+
{Provider: "Provider-B", EventID: 1, EventType: "Type3", Title: "Title3", Message: "Msg3"},
28+
}
29+
30+
lookup, err := buildEventLookup(events)
31+
require.NoError(t, err)
32+
require.Len(t, lookup, 3)
33+
34+
// Verify lookups
35+
def, found := lookup[eventKey{Provider: "Provider-A", EventID: 1}]
36+
require.True(t, found)
37+
assert.Equal(t, "Type1", def.EventType)
38+
39+
def, found = lookup[eventKey{Provider: "Provider-A", EventID: 2}]
40+
require.True(t, found)
41+
assert.Equal(t, "Type2", def.EventType)
42+
43+
def, found = lookup[eventKey{Provider: "Provider-B", EventID: 1}]
44+
require.True(t, found)
45+
assert.Equal(t, "Type3", def.EventType)
46+
47+
// Non-existent key
48+
_, found = lookup[eventKey{Provider: "Provider-B", EventID: 999}]
49+
assert.False(t, found)
50+
}
51+
52+
// TestBuildEventLookup_DuplicateKey asserts that duplicate event definitions are not allowed.
53+
func TestBuildEventLookup_DuplicateKey(t *testing.T) {
54+
events := []eventDefinition{
55+
{Provider: "Provider-A", EventID: 1, EventType: "Type1"},
56+
{Provider: "Provider-A", EventID: 1, EventType: "Type2"}, // Duplicate
57+
}
58+
59+
lookup, err := buildEventLookup(events)
60+
require.Error(t, err)
61+
assert.Nil(t, lookup)
62+
assert.Contains(t, err.Error(), "duplicate event definition")
63+
assert.Contains(t, err.Error(), "Provider-A/1")
64+
}
65+
66+
func TestBuildQuery(t *testing.T) {
67+
events := []eventDefinition{
68+
{
69+
Channel: "System",
70+
QueryBody: ` <Select Path="System">*[System[Provider[@Name='Test-Provider'] and EventID=123]]</Select>`,
71+
},
72+
}
73+
74+
query := buildQuery(events)
75+
76+
expected := `<QueryList>
77+
<Query Id="0" Path="System">
78+
<Select Path="System">*[System[Provider[@Name='Test-Provider'] and EventID=123]]</Select>
79+
</Query>
80+
</QueryList>`
81+
assert.Equal(t, expected, query)
82+
}
83+
84+
func TestBuildQuery_MultipleEvents(t *testing.T) {
85+
events := []eventDefinition{
86+
{
87+
Channel: "System",
88+
QueryBody: ` <Select Path="System">*[System[EventID=1]]</Select>`,
89+
},
90+
{
91+
Channel: "Application",
92+
QueryBody: ` <Select Path="Application">*[System[EventID=2]]</Select>`,
93+
},
94+
{
95+
Channel: "Security",
96+
QueryBody: ` <Select Path="Security">*[System[EventID=3]]</Select>
97+
<Suppress Path="Security">*[EventData[Data='exclude']]</Suppress>`,
98+
},
99+
}
100+
101+
query := buildQuery(events)
102+
103+
expected := `<QueryList>
104+
<Query Id="0" Path="System">
105+
<Select Path="System">*[System[EventID=1]]</Select>
106+
</Query>
107+
<Query Id="1" Path="Application">
108+
<Select Path="Application">*[System[EventID=2]]</Select>
109+
</Query>
110+
<Query Id="2" Path="Security">
111+
<Select Path="Security">*[System[EventID=3]]</Select>
112+
<Suppress Path="Security">*[EventData[Data='exclude']]</Suppress>
113+
</Query>
114+
</QueryList>`
115+
assert.Equal(t, expected, query)
116+
}
117+
21118
func TestCollector_CollectEvents(t *testing.T) {
22119
ctx := t.Context()
23120

@@ -48,12 +145,23 @@ func TestCollector_CollectEvents(t *testing.T) {
48145
outChan := make(chan eventPayload)
49146

50147
// Create collector using constructor
51-
collector := newCollector(outChan)
148+
collector, err := newCollector(outChan)
149+
require.NoError(t, err)
52150

53151
// Customize for testing with test API and test channel
54152
collector.api = ti.API()
55-
collector.channelPath = channelPath
56-
collector.query = "*" // Collect all events from the test channel
153+
collector.query = fmt.Sprintf(`<QueryList><Query Id="0"><Select Path="%s">*</Select></Query></QueryList>`, channelPath) // Collect all events from the test channel
154+
155+
// Add test event source to the lookup so test events can be processed
156+
testEventDef := &eventDefinition{
157+
Provider: eventSource,
158+
EventID: 1000,
159+
EventType: "Test event",
160+
Title: "Test event title",
161+
Message: "Test event message",
162+
Channel: channelPath,
163+
}
164+
collector.eventLookup[eventKey{Provider: testEventDef.Provider, EventID: testEventDef.EventID}] = testEventDef
57165

58166
// Start collector
59167
err = collector.start()
@@ -95,10 +203,13 @@ func TestCollector_CollectEvents(t *testing.T) {
95203
// Verify we received all expected events
96204
require.Len(t, receivedEvents, 3, "Should have received 3 events")
97205

98-
// Verify event payloads have correct channel
206+
// Verify event payloads have correct metadata from test event definition
99207
for i, event := range receivedEvents {
100-
assert.Equal(t, channelPath, event.Channel, "Event %d should have correct channel", i)
101-
assert.NotZero(t, event.EventID, "Event %d should have non-zero Event ID", i)
208+
assert.Equal(t, "Test event title", event.Title, "Event %d should have correct title", i)
209+
assert.Equal(t, "Test event", event.EventType, "Event %d should have correct event type", i)
210+
assert.Equal(t, "Test event message", event.Message, "Event %d should have correct message", i)
211+
assert.NotNil(t, event.Custom, "Event %d should have custom data", i)
212+
assert.Contains(t, event.Custom, "windows_event_log", "Event %d custom data should contain windows_event_log", i)
102213
}
103214

104215
// Verify no more events are in the channel

0 commit comments

Comments
 (0)