Skip to content

feat(telemetry): Process token attributes #520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/etw/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error {
eventsUnknown.Add(1)
return nil
}
if event.IsCurrentProcDropped(ev.Header.ProcessID) {
if event.IsCurrentProcDropped(ev.Header.ProcessID) && ev.Header.ProviderID != etw.WindowsKernelProcessGUID {
return nil
}
if c.config.EventSource.ExcludeEvent(ev.ID()) {
Expand Down
5 changes: 5 additions & 0 deletions internal/etw/processors/image_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ func newImageProcessor(psnap ps.Snapshotter) Processor {
func (*imageProcessor) Name() ProcessorType { return Image }

func (m *imageProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) {
if e.IsLoadImageInternal() {
// state management
return e, false, m.psnap.AddModule(e)
}

if e.IsLoadImage() {
// is image characteristics data cached?
path := e.GetParamAsString(params.ImagePath)
Expand Down
6 changes: 5 additions & 1 deletion internal/etw/processors/ps_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func newPsProcessor(psnap ps.Snapshotter, regionProber *va.RegionProber) Process

func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) {
switch e.Type {
case event.CreateProcess, event.TerminateProcess, event.ProcessRundown:
case event.CreateProcess, event.CreateProcessInternal, event.TerminateProcess, event.ProcessRundown, event.ProcessRundownInternal:
evt, err := p.processEvent(e)
if evt.IsTerminateProcess() {
p.regionProber.Remove(evt.Params.MustGetPid())
Expand Down Expand Up @@ -86,6 +86,10 @@ func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) {

//nolint:unparam
func (p psProcessor) processEvent(e *event.Event) (*event.Event, error) {
if e.IsCreateProcessInternal() || e.IsProcessRundownInternal() {
return e, nil
}

cmndline := cmdline.New(e.GetParamAsString(params.Cmdline)).
// get rid of leading/trailing quotes in the executable path
CleanExe().
Expand Down
46 changes: 28 additions & 18 deletions internal/etw/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,15 @@ func (e *EventSource) Open(config *config.Config) error {
}
}

// add the core NT Kernel Logger trace
e.addTrace(NewKernelTrace(config))

// security telemetry trace hosts remaining ETW providers
// security telemetry trace hosts all ETW providers but NT Kernel Logger
trace := NewTrace(etw.SecurityTelemetrySession, config)

// Windows Kernel Process session permits enriching event state with
// additional attributes and guaranteeing that any event published by
// the security telemetry session doesn't miss its respective process
// from the snapshotter
trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState())

if config.EventSource.EnableDNSEvents {
trace.AddProvider(etw.DNSClientGUID, false)
}
Expand All @@ -186,21 +189,21 @@ func (e *EventSource) Open(config *config.Config) error {
trace.AddProvider(etw.ThreadpoolGUID, config.EventSource.StackEnrichment, WithStackExts(stackexts))
}

if trace.HasProviders() {
// add security telemetry trace
e.addTrace(trace)
}
// add security telemetry trace
e.addTrace(trace)
// add the core NT Kernel Logger trace
e.addTrace(NewKernelTrace(config))

for _, trace := range e.traces {
err := trace.Start()
for _, t := range e.traces {
err := t.Start()
switch err {
case errs.ErrTraceAlreadyRunning:
log.Debugf("%s trace is already running. Trying to restart...", trace.Name)
if err := trace.Stop(); err != nil {
log.Debugf("%s trace is already running. Trying to restart...", t.Name)
if err := t.Stop(); err != nil {
return err
}
time.Sleep(time.Millisecond * 100)
if err := trace.Start(); err != nil {
if err := t.Start(); err != nil {
return multierror.Wrap(errs.ErrRestartTrace, err)
}
case errs.ErrTraceNoSysResources:
Expand Down Expand Up @@ -234,15 +237,22 @@ func (e *EventSource) Open(config *config.Config) error {
}
e.consumers = append(e.consumers, consumer)

err = trace.Open(consumer, e.errs)
// Open the trace and assign a consumer
err = t.Open(consumer, e.errs)
if err != nil {
return fmt.Errorf("unable to open %s trace: %v", t.Name, err)
}
log.Infof("starting [%s] trace processing", t.Name)

// Instruct the provider to emit state information
err = t.CaptureState()
if err != nil {
return fmt.Errorf("unable to open %s trace: %v", trace.Name, err)
log.Warn(err)
}
log.Infof("starting [%s] trace processing", trace.Name)

// Start event processing loop
errch := make(chan error)
go trace.Process(errch)
go t.Process(errch)

go func(trace *Trace) {
select {
Expand All @@ -254,7 +264,7 @@ func (e *EventSource) Open(config *config.Config) error {
e.errs <- fmt.Errorf("unable to process %s trace: %v", trace.Name, err)
}
}
}(trace)
}(t)
}

return nil
Expand Down
10 changes: 6 additions & 4 deletions internal/etw/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func TestEventSourceStartTraces(t *testing.T) {
},
Filters: &config.Filters{},
},
1,
2,
[]etw.EventTraceFlags{0x6018203, 0},
},
{"start kernel and security telemetry logger sessions",
Expand Down Expand Up @@ -199,10 +199,10 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) {
require.NoError(t, evs.Open(cfg))
defer evs.Close()

flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource)

require.Len(t, evs.(*EventSource).traces, 2)

flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource)

require.True(t, flags&etw.FileIO != 0)
require.True(t, flags&etw.Process != 0)
// rules compile result doesn't have the thread event
Expand Down Expand Up @@ -284,7 +284,9 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) {
require.NoError(t, evs.Open(cfg))
defer evs.Close()

flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource)
require.Len(t, evs.(*EventSource).traces, 2)

flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource)

// rules compile result doesn't have file events
// but Yara file scanning is enabled
Expand Down
37 changes: 34 additions & 3 deletions internal/etw/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func initEventTraceProps(c config.EventSourceConfig) etw.EventTraceProperties {
if flushTimer < time.Second {
flushTimer = time.Second
}

mode := uint32(etw.ProcessTraceModeRealtime)

return etw.EventTraceProperties{
Expand Down Expand Up @@ -88,6 +89,9 @@ type ProviderInfo struct {
// EnableStacks indicates if callstacks are enabled for
// this provider.
EnableStacks bool
// CaptureState requests that the provider log its state
// information, such as rundown events.
CaptureState bool
// stackExtensions manager stack tracing enablement.
// For each event present in the stack identifiers,
// the StackWalk event is published by the provider.
Expand Down Expand Up @@ -146,8 +150,9 @@ type Trace struct {
}

type opts struct {
stackexts *StackExtensions
keywords uint64
stackexts *StackExtensions
keywords uint64
captureState bool
}

// Option represents the option for the trace.
Expand All @@ -168,6 +173,14 @@ func WithKeywords(keywords uint64) Option {
}
}

// WithCaptureState indicates that the provider should
// emit its state information.
func WithCaptureState() Option {
return func(o *opts) {
o.captureState = true
}
}

// NewKernelTrace creates a new NT Kernel Logger trace.
func NewKernelTrace(config *config.Config) *Trace {
t := &Trace{Name: etw.KernelLoggerSession, GUID: etw.KernelTraceControlGUID, stackExtensions: NewStackExtensions(config.EventSource), config: config}
Expand All @@ -193,7 +206,10 @@ func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Opt
opt(&opts)
}

t.Providers = append(t.Providers, ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, stackExtensions: opts.stackexts})
t.Providers = append(
t.Providers,
ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts},
)
}

// HasProviders determines if this trace contains providers.
Expand Down Expand Up @@ -235,6 +251,7 @@ func (t *Trace) Start() error {
cfg := t.config.EventSource
props := initEventTraceProps(cfg)
flags := t.enableFlagsDynamically(cfg)

if t.IsKernelTrace() {
props.EnableFlags = flags
props.Wnode.GUID = t.GUID
Expand Down Expand Up @@ -408,6 +425,20 @@ func (t *Trace) Close() error {
return etw.CloseTrace(t.openHandle)
}

// CaptureState forces the provider to publish state
// information such as rundown events.
func (t *Trace) CaptureState() error {
for _, provider := range t.Providers {
if !provider.CaptureState {
continue
}
if err := etw.CaptureProviderState(provider.GUID, t.startHandle); err != nil {
return fmt.Errorf("unable to capture %s provider state: %v", provider.GUID, err)
}
}
return nil
}

// IsKernelTrace determines if this is the system logger trace.
func (t *Trace) IsKernelTrace() bool { return t.GUID == etw.KernelTraceControlGUID }

Expand Down
30 changes: 16 additions & 14 deletions pkg/alertsender/alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,17 @@ func TestAlertString(t *testing.T) {
Name: "CreateProcess",
PID: 1023,
PS: &pstypes.PS{
Name: "svchost.exe",
Cmdline: "C:\\Windows\\System32\\svchost.exe",
Ppid: 345,
Username: "SYSTEM",
Domain: "NT AUTHORITY",
SID: "S-1-5-18",
Name: "svchost.exe",
Cmdline: "C:\\Windows\\System32\\svchost.exe",
Ppid: 345,
Username: "SYSTEM",
Domain: "NT AUTHORITY",
SID: "S-1-5-18",
TokenIntegrityLevel: "HIGH",
},
}}),
true,
"Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n",
"Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n",
},
{
NewAlertWithEvents("Credential discovery via VaultCmd.exe", "", nil, Normal, []*event.Event{{
Expand All @@ -73,16 +74,17 @@ func TestAlertString(t *testing.T) {
Name: "CreateProcess",
PID: 1023,
PS: &pstypes.PS{
Name: "svchost.exe",
Cmdline: "C:\\Windows\\System32\\svchost.exe",
Ppid: 345,
Username: "SYSTEM",
Domain: "NT AUTHORITY",
SID: "S-1-5-18",
Name: "svchost.exe",
Cmdline: "C:\\Windows\\System32\\svchost.exe",
Ppid: 345,
Username: "SYSTEM",
Domain: "NT AUTHORITY",
SID: "S-1-5-18",
TokenIntegrityLevel: "HIGH",
},
}}),
true,
"Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n",
"Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n",
},
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/event/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,16 @@ var DNSResponseCodes = ParamEnum{
uint32(windows.ERROR_INVALID_PARAMETER): "INVALID",
uint32(windows.DNS_INFO_NO_RECORDS): "NXDOMAIN",
}

const (
TokenElevationTypeDefault uint32 = iota + 1
TokenElevationTypeFull
TokenElevationTypeLimited
)

// PsTokenElevationTypes describes process token elevation types
var PsTokenElevationTypes = ParamEnum{
TokenElevationTypeDefault: "DEFAULT",
TokenElevationTypeFull: "FULL",
TokenElevationTypeLimited: "LIMITED",
}
47 changes: 25 additions & 22 deletions pkg/event/event_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ func (e *Event) IsSuccess() bool {
// the tracing session to induce the arrival of rundown events
// by calling into the `etw.SetTraceInformation` Windows API
// function which causes duplicate rundown events.
// For more pointers check `kstream/controller_windows.go`
// and the `etw.SetTraceInformation` API function.
// For more pointers check `internal/etw/trace.go` and the
// `etw.SetTraceInformation` API function.
func (e *Event) IsRundownProcessed() bool {
mu.Lock()
defer mu.Unlock()
Expand All @@ -216,26 +216,29 @@ func (e *Event) IsRundownProcessed() bool {
return false
}

func (e *Event) IsCreateFile() bool { return e.Type == CreateFile }
func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess }
func (e *Event) IsCreateThread() bool { return e.Type == CreateThread }
func (e *Event) IsCloseFile() bool { return e.Type == CloseFile }
func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle }
func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle }
func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile }
func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory }
func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess }
func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread }
func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage }
func (e *Event) IsLoadImage() bool { return e.Type == LoadImage }
func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown }
func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd }
func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue }
func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown }
func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc }
func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile }
func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile }
func (e *Event) IsStackWalk() bool { return e.Type == StackWalk }
func (e *Event) IsCreateFile() bool { return e.Type == CreateFile }
func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess }
func (e *Event) IsCreateProcessInternal() bool { return e.Type == CreateProcessInternal }
func (e *Event) IsCreateThread() bool { return e.Type == CreateThread }
func (e *Event) IsCloseFile() bool { return e.Type == CloseFile }
func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle }
func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle }
func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile }
func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory }
func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess }
func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread }
func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage }
func (e *Event) IsLoadImage() bool { return e.Type == LoadImage }
func (e *Event) IsLoadImageInternal() bool { return e.Type == LoadImageInternal }
func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown }
func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd }
func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue }
func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown }
func (e *Event) IsProcessRundownInternal() bool { return e.Type == ProcessRundownInternal }
func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc }
func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile }
func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile }
func (e *Event) IsStackWalk() bool { return e.Type == StackWalk }

// InvalidPid indicates if the process generating the event is invalid.
func (e *Event) InvalidPid() bool { return e.PID == sys.InvalidProcessID }
Expand Down
3 changes: 3 additions & 0 deletions pkg/event/metainfo_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ func AllWithState() []Type {
s = append(s, ReleaseFile)
s = append(s, MapFileRundown)
s = append(s, StackWalk)
s = append(s, CreateProcessInternal)
s = append(s, ProcessRundownInternal)
s = append(s, LoadImageInternal)

return s
}
Expand Down
Loading