bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// Windows drivers for computer:*.
|
||||
//
|
||||
// Inventory: the Uninstall registry keys — HKLM 64-bit, HKLM 32-bit (WOW6432),
|
||||
// HKCU — the same source "Apps & features" reads. Entries flagged
|
||||
// SystemComponent, or with no DisplayName, are not applications. DisplayIcon
|
||||
// usually names the executable, InstallLocation the folder; the first is the
|
||||
// path when it ends in .exe.
|
||||
//
|
||||
// Windows: EnumWindows over visible top-level windows with a title, the owning
|
||||
// pid from GetWindowThreadProcessId, the app from the process image name, and
|
||||
// GetForegroundWindow for focus. user32 through the same LazyDLL pattern as
|
||||
// iphlpapi in agents_windows.go — no new module.
|
||||
|
||||
const (
|
||||
computerObserveSupported = true
|
||||
computerInventoryKind = "registry-uninstall"
|
||||
computerWindowsKind = "user32-enumwindows"
|
||||
)
|
||||
|
||||
var uninstallKeys = []struct {
|
||||
root registry.Key
|
||||
path string
|
||||
}{
|
||||
{registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
{registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`},
|
||||
}
|
||||
|
||||
func listInstalledApps() ([]computerApp, error) {
|
||||
var apps []computerApp
|
||||
opened := false
|
||||
for _, k := range uninstallKeys {
|
||||
key, err := registry.OpenKey(k.root, k.path, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
opened = true
|
||||
names, err := key.ReadSubKeyNames(-1)
|
||||
key.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, n := range names {
|
||||
sub, err := registry.OpenKey(k.root, k.path+`\`+n, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if a, ok := appFromUninstallKey(sub); ok {
|
||||
apps = append(apps, a)
|
||||
}
|
||||
sub.Close()
|
||||
}
|
||||
}
|
||||
if !opened {
|
||||
return nil, errors.New("could not open the Uninstall registry keys")
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func appFromUninstallKey(k registry.Key) (computerApp, bool) {
|
||||
name, _, err := k.GetStringValue("DisplayName")
|
||||
if err != nil || strings.TrimSpace(name) == "" {
|
||||
return computerApp{}, false
|
||||
}
|
||||
if sc, _, err := k.GetIntegerValue("SystemComponent"); err == nil && sc == 1 {
|
||||
return computerApp{}, false
|
||||
}
|
||||
a := computerApp{Name: name, Source: "registry"}
|
||||
a.Version, _, _ = k.GetStringValue("DisplayVersion")
|
||||
a.Publisher, _, _ = k.GetStringValue("Publisher")
|
||||
a.Path = exePathFromUninstall(k)
|
||||
return a, true
|
||||
}
|
||||
|
||||
// exePathFromUninstall picks the program: DisplayIcon when it is an .exe
|
||||
// (strip a trailing ",0" icon index), else InstallLocation.
|
||||
func exePathFromUninstall(k registry.Key) string {
|
||||
if icon, _, err := k.GetStringValue("DisplayIcon"); err == nil {
|
||||
icon = strings.Trim(strings.TrimSpace(icon), `"`)
|
||||
if i := strings.LastIndex(icon, ","); i > 0 {
|
||||
icon = icon[:i]
|
||||
}
|
||||
icon = strings.Trim(icon, `"`)
|
||||
if strings.EqualFold(filepath.Ext(icon), ".exe") {
|
||||
return icon
|
||||
}
|
||||
}
|
||||
if loc, _, err := k.GetStringValue("InstallLocation"); err == nil {
|
||||
return strings.Trim(strings.TrimSpace(loc), `"`)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
modUser32 = windows.NewLazySystemDLL("user32.dll")
|
||||
procEnumWindows = modUser32.NewProc("EnumWindows")
|
||||
procIsWindowVisible = modUser32.NewProc("IsWindowVisible")
|
||||
procGetWindowTextW = modUser32.NewProc("GetWindowTextW")
|
||||
procGetWindowThreadProcess = modUser32.NewProc("GetWindowThreadProcessId")
|
||||
procGetForegroundWindow = modUser32.NewProc("GetForegroundWindow")
|
||||
)
|
||||
|
||||
// ONE callback for the life of the process. The Go runtime registers every
|
||||
// distinct func value handed to syscall.NewCallback in a fixed table of 2000
|
||||
// slots and never frees a slot (runtime/syscall_windows.go compileCallback);
|
||||
// a closure created per call would exhaust it — a host polling the window
|
||||
// list every 30 s would kill the bastion inside a day. So the callback is a
|
||||
// package-level function created once, and
|
||||
// the per-call state lives in winEnum under a mutex: EnumWindows is
|
||||
// synchronous, so one enumeration at a time is exactly what happens.
|
||||
var (
|
||||
enumWindowsCB = syscall.NewCallback(enumWindowsProc)
|
||||
winEnum struct {
|
||||
sync.Mutex
|
||||
wins []computerWindow
|
||||
names map[uint32]string
|
||||
fg uintptr
|
||||
}
|
||||
)
|
||||
|
||||
func enumWindowsProc(hwnd uintptr, _ uintptr) uintptr {
|
||||
if v, _, _ := procIsWindowVisible.Call(hwnd); v == 0 {
|
||||
return 1
|
||||
}
|
||||
var buf [512]uint16
|
||||
n, _, _ := procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
|
||||
if n == 0 {
|
||||
return 1
|
||||
}
|
||||
title := windows.UTF16ToString(buf[:n])
|
||||
var pid uint32
|
||||
procGetWindowThreadProcess.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
app, ok := winEnum.names[pid]
|
||||
if !ok {
|
||||
app = processImageBase(pid)
|
||||
winEnum.names[pid] = app
|
||||
}
|
||||
winEnum.wins = append(winEnum.wins, computerWindow{Title: title, PID: int(pid), App: app,
|
||||
Focused: hwnd == winEnum.fg})
|
||||
return 1
|
||||
}
|
||||
|
||||
func listWindows() ([]computerWindow, error) {
|
||||
winEnum.Lock()
|
||||
defer winEnum.Unlock()
|
||||
winEnum.wins = nil
|
||||
winEnum.names = map[uint32]string{}
|
||||
winEnum.fg, _, _ = procGetForegroundWindow.Call()
|
||||
r, _, err := procEnumWindows.Call(enumWindowsCB, 0)
|
||||
wins := winEnum.wins
|
||||
winEnum.wins, winEnum.names = nil, nil
|
||||
if r == 0 && len(wins) == 0 {
|
||||
return nil, errors.New("EnumWindows failed: " + err.Error())
|
||||
}
|
||||
return wins, nil
|
||||
}
|
||||
|
||||
// processImageBase is the executable's basename for a pid, "" when the process
|
||||
// cannot be opened (an elevated window from a non-elevated bastion, for one).
|
||||
func processImageBase(pid uint32) string {
|
||||
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer windows.CloseHandle(h)
|
||||
var buf [windows.MAX_LONG_PATH]uint16
|
||||
size := uint32(len(buf))
|
||||
if err := windows.QueryFullProcessImageName(h, 0, &buf[0], &size); err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Base(windows.UTF16ToString(buf[:size]))
|
||||
}
|
||||
Reference in New Issue
Block a user