mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-02-18 10:46:35 +00:00
logon: BREAKING: replace wmi query by Win32 API calls and expose detailed logon sessions. (click PR for more information) (#1687)
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
package logon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/secur32"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/yusufpapurcu/wmi"
|
||||
@@ -20,10 +21,9 @@ var ConfigDefaults = Config{}
|
||||
|
||||
// A Collector is a Prometheus Collector for WMI metrics.
|
||||
type Collector struct {
|
||||
config Config
|
||||
wmiClient *wmi.Client
|
||||
config Config
|
||||
|
||||
logonType *prometheus.Desc
|
||||
sessionInfo *prometheus.Desc
|
||||
}
|
||||
|
||||
func New(config *Config) *Collector {
|
||||
@@ -54,16 +54,11 @@ func (c *Collector) Close(_ *slog.Logger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) Build(_ *slog.Logger, wmiClient *wmi.Client) error {
|
||||
if wmiClient == nil || wmiClient.SWbemServicesClient == nil {
|
||||
return errors.New("wmiClient or SWbemServicesClient is nil")
|
||||
}
|
||||
|
||||
c.wmiClient = wmiClient
|
||||
c.logonType = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "logon_type"),
|
||||
"Number of active logon sessions (LogonSession.LogonType)",
|
||||
[]string{"status"},
|
||||
func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error {
|
||||
c.sessionInfo = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "session_logon_timestamp_seconds"),
|
||||
"timestamp of the logon session in seconds.",
|
||||
[]string{"id", "username", "domain", "type"},
|
||||
nil,
|
||||
)
|
||||
|
||||
@@ -72,171 +67,28 @@ func (c *Collector) Build(_ *slog.Logger, wmiClient *wmi.Client) error {
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) Collect(_ *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error {
|
||||
logger = logger.With(slog.String("collector", Name))
|
||||
func (c *Collector) Collect(_ *types.ScrapeContext, _ *slog.Logger, ch chan<- prometheus.Metric) error {
|
||||
if err := c.collect(ch); err != nil {
|
||||
logger.Error("failed collecting user metrics",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Win32_LogonSession docs:
|
||||
// - https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logonsession
|
||||
type Win32_LogonSession struct {
|
||||
LogonType uint32
|
||||
}
|
||||
|
||||
func (c *Collector) collect(ch chan<- prometheus.Metric) error {
|
||||
var dst []Win32_LogonSession
|
||||
if err := c.wmiClient.Query("SELECT * FROM Win32_LogonSession", &dst); err != nil {
|
||||
return err
|
||||
logonSessions, err := secur32.GetLogonSessions()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get logon sessions: %w", err)
|
||||
}
|
||||
|
||||
if len(dst) == 0 {
|
||||
return errors.New("WMI query returned empty result set")
|
||||
for _, session := range logonSessions {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.sessionInfo,
|
||||
prometheus.GaugeValue,
|
||||
float64(session.LogonTime.Unix()),
|
||||
session.LogonId.String(), session.UserName, session.LogonDomain, session.LogonType.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// Init counters
|
||||
system := 0
|
||||
interactive := 0
|
||||
network := 0
|
||||
batch := 0
|
||||
service := 0
|
||||
proxy := 0
|
||||
unlock := 0
|
||||
networkcleartext := 0
|
||||
newcredentials := 0
|
||||
remoteinteractive := 0
|
||||
cachedinteractive := 0
|
||||
cachedremoteinteractive := 0
|
||||
cachedunlock := 0
|
||||
|
||||
for _, entry := range dst {
|
||||
switch entry.LogonType {
|
||||
case 0:
|
||||
system++
|
||||
case 2:
|
||||
interactive++
|
||||
case 3:
|
||||
network++
|
||||
case 4:
|
||||
batch++
|
||||
case 5:
|
||||
service++
|
||||
case 6:
|
||||
proxy++
|
||||
case 7:
|
||||
unlock++
|
||||
case 8:
|
||||
networkcleartext++
|
||||
case 9:
|
||||
newcredentials++
|
||||
case 10:
|
||||
remoteinteractive++
|
||||
case 11:
|
||||
cachedinteractive++
|
||||
case 12:
|
||||
cachedremoteinteractive++
|
||||
case 13:
|
||||
cachedunlock++
|
||||
}
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(system),
|
||||
"system",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(interactive),
|
||||
"interactive",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(network),
|
||||
"network",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(batch),
|
||||
"batch",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(service),
|
||||
"service",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(proxy),
|
||||
"proxy",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(unlock),
|
||||
"unlock",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(networkcleartext),
|
||||
"network_clear_text",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(newcredentials),
|
||||
"new_credentials",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(remoteinteractive),
|
||||
"remote_interactive",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(cachedinteractive),
|
||||
"cached_interactive",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(remoteinteractive),
|
||||
"cached_remote_interactive",
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.logonType,
|
||||
prometheus.GaugeValue,
|
||||
float64(cachedunlock),
|
||||
"cached_unlock",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
119
internal/headers/secur32/secur32.go
Normal file
119
internal/headers/secur32/secur32.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package secur32
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// based on https://github.com/carlpett/winlsa/blob/master/winlsa.go
|
||||
|
||||
var (
|
||||
secur32 = windows.NewLazySystemDLL("Secur32.dll")
|
||||
advapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
|
||||
procLsaEnumerateLogonSessions = secur32.NewProc("LsaEnumerateLogonSessions")
|
||||
procLsaGetLogonSessionData = secur32.NewProc("LsaGetLogonSessionData")
|
||||
procLsaFreeReturnBuffer = secur32.NewProc("LsaFreeReturnBuffer")
|
||||
procLsaNtStatusToWinError = advapi32.NewProc("LsaNtStatusToWinError")
|
||||
)
|
||||
|
||||
func GetLogonSessions() ([]*LogonSessionData, error) {
|
||||
var (
|
||||
buffer uintptr
|
||||
sessionCount uint32
|
||||
)
|
||||
|
||||
err := LsaEnumerateLogonSessions(&sessionCount, &buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buffer != 0 {
|
||||
defer func(buffer uintptr) {
|
||||
_ = LsaFreeReturnBuffer(buffer)
|
||||
}(buffer)
|
||||
}
|
||||
|
||||
sizeLUID := unsafe.Sizeof(windows.LUID{})
|
||||
|
||||
sessionDataSlice := make([]*LogonSessionData, 0, sessionCount)
|
||||
|
||||
for i := range sessionCount {
|
||||
curPtr := unsafe.Pointer(buffer + (uintptr(i) * sizeLUID))
|
||||
luid := (*windows.LUID)(curPtr)
|
||||
|
||||
sessionData, err := GetLogonSessionData(luid)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
// Skip logon sessions that we don't have access to
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionDataSlice = append(sessionDataSlice, sessionData)
|
||||
}
|
||||
|
||||
return sessionDataSlice, nil
|
||||
}
|
||||
|
||||
func GetLogonSessionData(luid *windows.LUID) (*LogonSessionData, error) {
|
||||
var dataBuffer *SECURITY_LOGON_SESSION_DATA
|
||||
if err := LsaGetLogonSessionData(luid, &dataBuffer); err != nil {
|
||||
return nil, fmt.Errorf("failed to get logon session data: %w", err)
|
||||
}
|
||||
|
||||
defer func(buffer uintptr) {
|
||||
_ = LsaFreeReturnBuffer(buffer)
|
||||
}(uintptr(unsafe.Pointer(dataBuffer)))
|
||||
|
||||
return newLogonSessionData(dataBuffer), nil
|
||||
}
|
||||
|
||||
func LsaEnumerateLogonSessions(sessionCount *uint32, sessions *uintptr) error {
|
||||
r0, _, _ := procLsaEnumerateLogonSessions.Call(uintptr(unsafe.Pointer(sessionCount)), uintptr(unsafe.Pointer(sessions)))
|
||||
|
||||
return LsaNtStatusToWinError(r0)
|
||||
}
|
||||
|
||||
func LsaGetLogonSessionData(luid *windows.LUID, ppLogonSessionData **SECURITY_LOGON_SESSION_DATA) error {
|
||||
r0, _, _ := procLsaGetLogonSessionData.Call(uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(ppLogonSessionData)))
|
||||
|
||||
return LsaNtStatusToWinError(r0)
|
||||
}
|
||||
|
||||
func LsaFreeReturnBuffer(buffer uintptr) error {
|
||||
r0, _, _ := procLsaFreeReturnBuffer.Call(buffer)
|
||||
|
||||
return LsaNtStatusToWinError(r0)
|
||||
}
|
||||
|
||||
func LsaNtStatusToWinError(ntstatus uintptr) error {
|
||||
r0, _, err := procLsaNtStatusToWinError.Call(ntstatus)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, windows.ERROR_SUCCESS):
|
||||
if r0 == 0 {
|
||||
return nil
|
||||
}
|
||||
case errors.Is(err, windows.ERROR_MR_MID_NOT_FOUND):
|
||||
return fmt.Errorf("unknown LSA NTSTATUS code %x", ntstatus)
|
||||
}
|
||||
|
||||
return windows.Errno(r0)
|
||||
}
|
||||
|
||||
func newLogonSessionData(data *SECURITY_LOGON_SESSION_DATA) *LogonSessionData {
|
||||
return &LogonSessionData{
|
||||
LogonId: data.LogonId,
|
||||
UserName: data.UserName.String(),
|
||||
LogonDomain: data.LogonDomain.String(),
|
||||
LogonType: data.LogonType,
|
||||
LogonTime: time.Unix(0, data.LogonTime.Nanoseconds()),
|
||||
}
|
||||
}
|
||||
16
internal/headers/secur32/secur32_test.go
Normal file
16
internal/headers/secur32/secur32_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package secur32_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/secur32"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetLogonSessions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sessionData, err := secur32.GetLogonSessions()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, sessionData)
|
||||
}
|
||||
112
internal/headers/secur32/types.go
Normal file
112
internal/headers/secur32/types.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package secur32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type LogonType uint32
|
||||
|
||||
type LSA_LAST_INTER_LOGON_INFO struct {
|
||||
LastSuccessfulLogon windows.Filetime
|
||||
LastFailedLogon windows.Filetime
|
||||
FailedAttemptCountSinceLastSuccessfulLogon uint32
|
||||
}
|
||||
|
||||
type SECURITY_LOGON_SESSION_DATA struct {
|
||||
Size uint32
|
||||
LogonId LUID
|
||||
UserName windows.NTUnicodeString
|
||||
LogonDomain windows.NTUnicodeString
|
||||
AuthenticationPackage windows.NTUnicodeString
|
||||
LogonType LogonType
|
||||
Session uint32
|
||||
Sid *windows.SID
|
||||
LogonTime windows.Filetime
|
||||
LogonServer windows.NTUnicodeString
|
||||
DnsDomainName windows.NTUnicodeString
|
||||
Upn windows.NTUnicodeString
|
||||
UserFlags uint32
|
||||
LastLogonInfo LSA_LAST_INTER_LOGON_INFO
|
||||
LogonScript windows.NTUnicodeString
|
||||
ProfilePath windows.NTUnicodeString
|
||||
HomeDirectory windows.NTUnicodeString
|
||||
HomeDirectoryDrive windows.NTUnicodeString
|
||||
LogoffTime windows.Filetime
|
||||
KickOffTime windows.Filetime
|
||||
PasswordLastSet windows.Filetime
|
||||
PasswordCanChange windows.Filetime
|
||||
PasswordMustChange windows.Filetime
|
||||
}
|
||||
|
||||
const (
|
||||
// LogonTypeSystem Not explicitly defined in LSA, but according to
|
||||
// https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-logonsession,
|
||||
// LogonType=0 is "Used only by the System account."
|
||||
LogonTypeSystem LogonType = iota
|
||||
_ // LogonType=1 is not used
|
||||
LogonTypeInteractive
|
||||
LogonTypeNetwork
|
||||
LogonTypeBatch
|
||||
LogonTypeService
|
||||
LogonTypeProxy
|
||||
LogonTypeUnlock
|
||||
LogonTypeNetworkCleartext
|
||||
LogonTypeNewCredentials
|
||||
LogonTypeRemoteInteractive
|
||||
LogonTypeCachedInteractive
|
||||
LogonTypeCachedRemoteInteractive
|
||||
LogonTypeCachedUnlock
|
||||
)
|
||||
|
||||
func (lt LogonType) String() string {
|
||||
switch lt {
|
||||
case LogonTypeSystem:
|
||||
return "System"
|
||||
case LogonTypeInteractive:
|
||||
return "Interactive"
|
||||
case LogonTypeNetwork:
|
||||
return "Network"
|
||||
case LogonTypeBatch:
|
||||
return "Batch"
|
||||
case LogonTypeService:
|
||||
return "Service"
|
||||
case LogonTypeProxy:
|
||||
return "Proxy"
|
||||
case LogonTypeUnlock:
|
||||
return "Unlock"
|
||||
case LogonTypeNetworkCleartext:
|
||||
return "NetworkCleartext"
|
||||
case LogonTypeNewCredentials:
|
||||
return "NewCredentials"
|
||||
case LogonTypeRemoteInteractive:
|
||||
return "RemoteInteractive"
|
||||
case LogonTypeCachedInteractive:
|
||||
return "CachedInteractive"
|
||||
case LogonTypeCachedRemoteInteractive:
|
||||
return "CachedRemoteInteractive"
|
||||
case LogonTypeCachedUnlock:
|
||||
return "CachedUnlock"
|
||||
default:
|
||||
return fmt.Sprintf("Undefined LogonType(%d)", lt)
|
||||
}
|
||||
}
|
||||
|
||||
type LogonSessionData struct {
|
||||
LogonId LUID
|
||||
UserName string
|
||||
LogonDomain string
|
||||
AuthenticationPackage string
|
||||
LogonType LogonType
|
||||
Session uint32
|
||||
Sid *windows.SID
|
||||
LogonTime time.Time
|
||||
}
|
||||
|
||||
type LUID windows.LUID
|
||||
|
||||
func (l LUID) String() string {
|
||||
return fmt.Sprintf("0x%x:0x%x", l.HighPart, l.LowPart)
|
||||
}
|
||||
Reference in New Issue
Block a user