Skip to content

Commit 7fbc689

Browse files
authored
add descriptor to proc data (#43549)
### What does this PR do? This PR adds the process name from the file property of the executable to the process data we send. ### Motivation https://datadoghq.atlassian.net/browse/WINA-2023 ### Describe how you validated your changes Adds unit tests. ### Additional Notes How big should the cache for this property be? I have made it 512, which seems appropriate. Notes on Live Processes changes: https://docs.google.com/document/d/1o6gVZRMP4rp69qu4Z4MbudXiLe3dqnPPufg7lInM_8I/edit?tab=t.0 Co-authored-by: jack.phillips <jack.phillips@datadoghq.com>
1 parent 4e6b4c9 commit 7fbc689

7 files changed

Lines changed: 259 additions & 2 deletions

File tree

pkg/process/procutil/process_model.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
package procutil
77

88
import (
9+
"github.com/DataDog/gopsutil/cpu"
10+
911
"github.com/DataDog/datadog-agent/pkg/discovery/tracermetadata"
1012
"github.com/DataDog/datadog-agent/pkg/languagedetection/languagemodels"
11-
"github.com/DataDog/gopsutil/cpu"
1213

1314
// using process.FilledProcess
1415
"github.com/DataDog/gopsutil/process"
@@ -77,6 +78,7 @@ func (p *Process) DeepCopy() *Process {
7778
Name: p.Name,
7879
Cwd: p.Cwd,
7980
Exe: p.Exe,
81+
Comm: p.Comm,
8082
Username: p.Username,
8183
PortsCollected: p.PortsCollected,
8284
}

pkg/process/procutil/process_windows.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616

1717
"golang.org/x/sys/windows"
1818

19+
"github.com/hashicorp/golang-lru/v2/simplelru"
20+
1921
"github.com/DataDog/datadog-agent/pkg/util/log"
2022
"github.com/DataDog/datadog-agent/pkg/util/pdhutil"
2123
"github.com/DataDog/datadog-agent/pkg/util/winutil"
@@ -44,6 +46,41 @@ var (
4446
PIDBufferIncrement uint32 = 1024
4547
)
4648

49+
var fileDescCache *simplelru.LRU[string, string]
50+
51+
func init() {
52+
var err error
53+
fileDescCache, err = simplelru.NewLRU[string, string](512, nil)
54+
if err != nil {
55+
log.Errorf("Failed to create file description cache: %v", err)
56+
}
57+
}
58+
59+
// getFileDescriptionCached gets the file description for a given executable path
60+
func getFileDescriptionCached(exePath string) string {
61+
if exePath == "" {
62+
return ""
63+
}
64+
65+
// Check cache first
66+
if cached, ok := fileDescCache.Get(exePath); ok {
67+
return cached
68+
}
69+
70+
// Cache miss - get from Windows API
71+
desc, err := winutil.GetFileDescription(exePath)
72+
if err != nil {
73+
log.Debugf("Could not get file description for %s: %v", exePath, err)
74+
// for now cache these as a blank string as it could mean they
75+
// don't have a description
76+
desc = ""
77+
}
78+
79+
// Cache the result
80+
fileDescCache.Add(exePath, desc)
81+
return desc
82+
}
83+
4784
// NewProcessProbe returns a Probe object
4885
func NewProcessProbe(...Option) Probe {
4986
p := &probe{}
@@ -520,6 +557,7 @@ func fillProcessDetails(pid int32, proc *Process) error {
520557
if processCmdParams != nil {
521558
proc.Cmdline = ParseCmdLineArgs(processCmdParams.CmdLine)
522559
proc.Exe = processCmdParams.ImagePath
560+
proc.Comm = getFileDescriptionCached(processCmdParams.ImagePath)
523561
if len(processCmdParams.CmdLine) > 0 && len(proc.Cmdline) == 0 {
524562
log.Warnf("Failed to parse the cmdline:%s for pid:%d", processCmdParams.CmdLine, pid)
525563
}

pkg/process/procutil/process_windows_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func TestWindowsProbe(t *testing.T) {
115115
assert.Equal(t, []string{"powershell.exe", "-c", `"sleep 10; foo bar baz"`}, p.Cmdline)
116116
assert.Equal(t, int32(os.Getpid()), p.Ppid)
117117
assert.Equal(t, int32(cmd.Process.Pid), p.Pid)
118+
assert.Equal(t, "Windows PowerShell", p.Comm)
118119

119120
assert.WithinRange(t, time.Unix(0, p.Stats.CreateTime*1000_000), now, now.Add(5*time.Second))
120121

pkg/process/procutil/process_windows_toolhelp.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ func (p *windowsToolhelpProbe) ProcessesByPID(_ time.Time, collectStats bool) (m
211211
Stats: stats,
212212
Exe: cp.executablePath,
213213
Username: cp.userName,
214+
Comm: cp.comm,
214215
}
215216
}
216217
for pid := range knownPids {
@@ -226,6 +227,7 @@ type cachedProcess struct {
226227
userName string
227228
executablePath string
228229
commandLine string
230+
comm string
229231
procHandle windows.Handle
230232
parsedArgs []string
231233
}
@@ -243,7 +245,7 @@ func (cp *cachedProcess) fillFromProcEntry(pe32 *w32.PROCESSENTRY32) (err error)
243245
}
244246
cp.executablePath = winutil.ConvertWindowsString16(pe32.SzExeFile[:])
245247
cp.commandLine = cp.executablePath
246-
248+
cp.comm = getFileDescriptionCached(cp.executablePath)
247249
// we cannot read the command line if the process is protected
248250
if !isProtected {
249251
commandParams, cmderr := winutil.GetCommandParamsForProcess(cp.procHandle, false)

pkg/util/winutil/process.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ import (
2020
var (
2121
modntdll = windows.NewLazyDLL("ntdll.dll")
2222
modkernel = windows.NewLazyDLL("kernel32.dll")
23+
modversion = windows.NewLazyDLL("version.dll")
2324
procNtQueryInformationProcess = modntdll.NewProc("NtQueryInformationProcess")
2425
procReadProcessMemory = modkernel.NewProc("ReadProcessMemory")
2526
procIsWow64Process = modkernel.NewProc("IsWow64Process")
2627
procQueryFullProcessImageNameW = modkernel.NewProc("QueryFullProcessImageNameW")
28+
procGetFileVersionInfoSizeW = modversion.NewProc("GetFileVersionInfoSizeW")
29+
procGetFileVersionInfoW = modversion.NewProc("GetFileVersionInfoW")
30+
procVerQueryValueW = modversion.NewProc("VerQueryValueW")
2731
)
2832

2933
// C definition from winternl.h
@@ -480,3 +484,92 @@ func IsCurrentProcessLocalSystem() (bool, error) {
480484

481485
return currentUser.Equals(localSystem), nil
482486
}
487+
488+
// GetFileDescription returns the file description for given executable path
489+
func GetFileDescription(executablePath string) (string, error) {
490+
// Convert path to UTF16
491+
pathPtr, err := syscall.UTF16PtrFromString(executablePath)
492+
if err != nil {
493+
return "", fmt.Errorf("failed to convert path to UTF16: %w", err)
494+
}
495+
496+
// Get the size of the version information
497+
var handle uint32
498+
size, _, err := procGetFileVersionInfoSizeW.Call(
499+
uintptr(unsafe.Pointer(pathPtr)),
500+
uintptr(unsafe.Pointer(&handle)),
501+
)
502+
if size == 0 {
503+
if err != nil && err != syscall.Errno(0) {
504+
return "", fmt.Errorf("GetFileVersionInfoSizeW failed: %w", err)
505+
}
506+
return "", fmt.Errorf("no version information available for %s", executablePath)
507+
}
508+
509+
// Allocate buffer for version info
510+
data := make([]byte, size)
511+
512+
// Get the version information
513+
ret, _, err := procGetFileVersionInfoW.Call(
514+
uintptr(unsafe.Pointer(pathPtr)),
515+
uintptr(handle),
516+
uintptr(size),
517+
uintptr(unsafe.Pointer(&data[0])),
518+
)
519+
// returns non-zero if successful, and zero if not
520+
if ret == 0 {
521+
return "", fmt.Errorf("GetFileVersionInfoW failed: %w", err)
522+
}
523+
524+
// Query the language and code page
525+
// First get the translation table
526+
subBlockPtr, err := syscall.UTF16PtrFromString("\\VarFileInfo\\Translation")
527+
if err != nil {
528+
return "", fmt.Errorf("failed to create subblock string: %w", err)
529+
}
530+
531+
var langCodePagePtr *uint16
532+
var langCodePageLen uint32
533+
ret, _, err = procVerQueryValueW.Call(
534+
uintptr(unsafe.Pointer(&data[0])),
535+
uintptr(unsafe.Pointer(subBlockPtr)),
536+
uintptr(unsafe.Pointer(&langCodePagePtr)),
537+
uintptr(unsafe.Pointer(&langCodePageLen)),
538+
)
539+
540+
var langCodePage string
541+
if ret == 0 || langCodePageLen < 4 {
542+
return "", fmt.Errorf("no language code page found: %w", err)
543+
}
544+
545+
pair := (*[2]uint16)(unsafe.Pointer(langCodePagePtr))
546+
547+
// Extract the first language/codepage pair
548+
langCode := pair[0]
549+
codePage := pair[1]
550+
langCodePage = fmt.Sprintf("%04x%04x", langCode, codePage)
551+
552+
// Query for FileDescription
553+
fileDescQuery := fmt.Sprintf("\\StringFileInfo\\%s\\FileDescription", langCodePage)
554+
fileDescQueryPtr, err := syscall.UTF16PtrFromString(fileDescQuery)
555+
if err != nil {
556+
return "", fmt.Errorf("failed to create file description query: %w", err)
557+
}
558+
559+
var fileDescPtr *uint16
560+
var fileDescLen uint32
561+
ret, _, err = procVerQueryValueW.Call(
562+
uintptr(unsafe.Pointer(&data[0])),
563+
uintptr(unsafe.Pointer(fileDescQueryPtr)),
564+
uintptr(unsafe.Pointer(&fileDescPtr)),
565+
uintptr(unsafe.Pointer(&fileDescLen)),
566+
)
567+
568+
if ret == 0 || fileDescLen == 0 {
569+
return "", fmt.Errorf("FileDescription not found in version info: %w", err)
570+
}
571+
572+
// Convert the UTF16 string to Go string
573+
fileDesc := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(fileDescPtr)))
574+
return fileDesc, nil
575+
}

pkg/util/winutil/process_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,113 @@ func TestIsProcessProtected(t *testing.T) {
116116
})
117117
}
118118
}
119+
120+
func TestGetFileDescription(t *testing.T) {
121+
tests := []struct {
122+
name string
123+
path string
124+
expectError bool
125+
validateFunc func(t *testing.T, desc string)
126+
}{
127+
{
128+
name: "notepad.exe",
129+
path: "C:\\Windows\\System32\\notepad.exe",
130+
expectError: false,
131+
validateFunc: func(t *testing.T, desc string) {
132+
assert.Contains(t, desc, "Notepad", "notepad.exe does not match expected description")
133+
t.Logf("notepad.exe description: %s", desc)
134+
},
135+
},
136+
{
137+
name: "cmd.exe",
138+
path: "C:\\Windows\\System32\\cmd.exe",
139+
expectError: false,
140+
validateFunc: func(t *testing.T, desc string) {
141+
assert.NotEmpty(t, desc)
142+
assert.Contains(t, desc, "Command", "cmd.exe does mention not match expected description")
143+
t.Logf("cmd.exe description: %s", desc)
144+
},
145+
},
146+
{
147+
name: "explorer.exe",
148+
path: "C:\\Windows\\explorer.exe",
149+
expectError: false,
150+
validateFunc: func(t *testing.T, desc string) {
151+
assert.Contains(t, desc, "Windows Explorer", "explorer.exe does not match expected description")
152+
t.Logf("explorer.exe description: %s", desc)
153+
},
154+
},
155+
{
156+
name: "powershell.exe",
157+
path: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
158+
expectError: false,
159+
validateFunc: func(t *testing.T, desc string) {
160+
assert.Contains(t, desc, "Windows PowerShell", "powershell.exe does not match expected description")
161+
t.Logf("powershell.exe description: %s", desc)
162+
},
163+
},
164+
{
165+
name: "kernel32.dll",
166+
path: "C:\\Windows\\System32\\kernel32.dll",
167+
expectError: false,
168+
validateFunc: func(t *testing.T, desc string) {
169+
assert.NotEmpty(t, desc)
170+
t.Logf("kernel32.dll description: %s", desc)
171+
},
172+
},
173+
{
174+
name: "non-existent file",
175+
path: "C:\\DoesNotExist\\fake.exe",
176+
expectError: true,
177+
validateFunc: func(t *testing.T, desc string) {
178+
assert.Empty(t, desc)
179+
},
180+
},
181+
{
182+
name: "empty path",
183+
path: "",
184+
expectError: true,
185+
validateFunc: func(t *testing.T, desc string) {
186+
assert.Empty(t, desc)
187+
},
188+
},
189+
}
190+
191+
for _, tt := range tests {
192+
t.Run(tt.name, func(t *testing.T) {
193+
desc, err := GetFileDescription(tt.path)
194+
195+
if tt.expectError {
196+
assert.Error(t, err, "Expected error for path: %s", tt.path)
197+
t.Logf("Expected error: %v", err)
198+
} else {
199+
if err != nil {
200+
t.Logf("Warning: Could not get file description for %s: %v", tt.path, err)
201+
// Some systems might not have all files, so just log warning
202+
return
203+
}
204+
assert.NoError(t, err, "Should not error for valid path: %s", tt.path)
205+
}
206+
207+
if tt.validateFunc != nil {
208+
tt.validateFunc(t, desc)
209+
}
210+
})
211+
}
212+
}
213+
214+
func TestGetFileDescriptionMultipleCalls(t *testing.T) {
215+
// Test that multiple calls to the same file return consistent results
216+
path := "C:\\Windows\\System32\\notepad.exe"
217+
218+
desc1, err1 := GetFileDescription(path)
219+
if err1 != nil {
220+
t.Skipf("Skipping test, notepad.exe not accessible: %v", err1)
221+
}
222+
223+
desc2, err2 := GetFileDescription(path)
224+
require.NoError(t, err2)
225+
226+
assert.Equal(t, desc1, desc2, "Multiple calls should return same description")
227+
t.Logf("Consistent description: %s", desc1)
228+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Each section from every release note are combined when the
2+
# CHANGELOG.rst is rendered. So the text needs to be worded so that
3+
# it does not depend on any information only available in another
4+
# section. This may mean repeating some details, but each section
5+
# must be readable independently of the other.
6+
#
7+
# Each section note must be formatted as reStructuredText.
8+
---
9+
enhancements:
10+
- |
11+
On Windows, adds process name to live processes via file properties.

0 commit comments

Comments
 (0)