wmi

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: MIT Imports: 33 Imported by: 0

README

wmi

CI PkgGoDev Go Report Card

Pure Go cross-platform WMI client. Query remote Windows hosts from Linux or Windows over DCOM/RPC using NTLM v2 or Kerberos, no CGo or COM dependencies

Supports:

  • NTLM v2 authentication (with ESS, key exchange, sealing/signing)
  • Kerberos authentication (TGT, TGS, AP-REQ, AES-128/256, RC4, GSS-API wrapping)
  • Kerberos ticket caching (file + memory)
  • WQL query execution (IWbemServices::ExecQuery)
  • Smart enumeration optimization (IWbemFetchSmartEnum)
  • Fallback to standard enumeration (IEnumWbemClassObject::Next)
  • CIM type decoding (scalars, arrays, references, datetime; object type not yet supported)
  • Property qualifier loading
  • Reference property resolution
  • RPC fault handling
  • DCOM/NDR encoding and object activation

Todo:

  • IWbemServices_ExecQueryAsync (async WMI queries)
  • WMI method invocation (ExecMethod)
  • ExecNotificationQuery (event subscriptions)
  • Kerberos implementation real world (e2e) testing

Usage

Below is a complete example of connecting to a remote host with NTLM authentication and querying with the high-level Client API. See the examples/ directory for more.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cemremengu/wmi"
)

// struct tags are optional; defaults to the field name
type OperatingSystem struct {
	Caption string // `wmi:"Caption"`
	Version string // `wmi:"Version"`
}

func main() {
	ctx := context.Background()

	client, err := wmi.DialNTLM(ctx, "10.0.0.1", "username", "password",
		wmi.WithDomain("CORP"),                  // optional
		wmi.WithConnectTimeout(15*time.Second), // optional
	)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	var systems []OperatingSystem
	if err := client.CollectDecoded(ctx, "SELECT Caption, Version FROM Win32_OperatingSystem", &systems); err != nil {
		log.Fatal(err)
	}

	for _, os := range systems {
		fmt.Printf("Caption=%s Version=%s\n", os.Caption, os.Version)
	}
}

Kerberos

For Kerberos authentication, use a FQDN as the hostname. Domain is required. The KDC defaults to the target host on port 88; use WithKDC when the Key Distribution Center is on a separate machine.

client, err := wmi.DialKerberos(ctx, "host.example.com", username, password, "EXAMPLE.COM",
    wmi.WithConnectTimeout(15*time.Second),       // optional
    wmi.WithKDC("kdc.example.com", 88),           // optional: override KDC
    wmi.WithKerberosCache(myCache),               // optional: custom ticket cache
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Kerberos tickets (TGT + TGS) are cached inside each connection for its lifetime, so multiple queries reuse them automatically. Use WithKerberosCache with a file-backed cache (NewKerberosCache(path)) to persist tickets to disk across process restarts.

Querying

The query API exposes four entry points:

  • (*Client).Query(wql, opts...) creates a reusable query context bound to the client.
  • (*Client).Collect(ctx, wql, opts...) executes a query and returns all rows.
  • (*Client).CollectDecoded(ctx, wql, dest, opts...) executes a query and decodes all rows into dest.
  • (*Client).Each(ctx, wql, opts...) returns an iterator for streaming rows.

(*Client).Query(wql, opts...) acts as a query builder and returns a QContext that can be reused for multiple executions. The returned QContext has the same Collect, CollectDecoded, and Each methods, but without the WQL and options parameters since they are already set.

Query options

Query options can be passed to (*Client).Query, (*Client).Collect, (*Client).CollectDecoded, and (*Client).Each.

Available query options:

  • wmi.WithNamespace(...) overrides the default root/cimv2 namespace.
  • wmi.WithLanguage(...) overrides the query language. The default is WQL.
  • wmi.WithFlags(...) overrides query flags.
  • wmi.WithTimeout(...) sets the default per-row fetch timeout in milliseconds.
  • wmi.WithSkipOptimize(true) disables SmartEnum optimization.
  • wmi.WithResultOptions(...) configures property shaping for results.

Example with a reusable query context:

qc := client.Query(
    "SELECT Name, ProcessId FROM Win32_Process",
    wmi.WithNamespace("root/cimv2"),
    wmi.WithTimeout(120),
    wmi.WithSkipOptimize(true),
    wmi.WithResultOptions(wmi.ResultOptions{IgnoreDefaults: true}),
)

for props, err := range qc.Each(ctx) {
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("pid=%v name=%v\n", props["ProcessId"].Value, props["Name"].Value)
}

The same QContext can also decode directly into a slice destination:

var processes []struct {
    Name      string `wmi:"Name"`
    ProcessID uint32 `wmi:"ProcessId"`
}

if err := qc.CollectDecoded(ctx, &processes); err != nil {
    log.Fatal(err)
}

The same options can be passed directly to other query helpers:

rows, err := client.Collect(
    ctx,
    "SELECT Name FROM Win32_Service",
    wmi.WithNamespace("root/cimv2"),
    wmi.WithTimeout(120),
)
if err != nil {
    log.Fatal(err)
}

Querying a non-default namespace works the same way:

events := client.Query(
    "SELECT * FROM __EventFilter",
    wmi.WithNamespace("root/subscription"),
)

rows, err := events.Collect(ctx)
if err != nil {
    log.Fatal(err)
}

Query flags

DefaultQueryFlags() returns WBEMFlagReturnImmediately | WBEMFlagForwardOnly, which is the recommended setting for most queries. Additional flags from constants.go can be OR-ed in:

Constant Description
WBEMFlagReturnImmediately Return control immediately (async-style)
WBEMFlagForwardOnly Forward-only cursor (saves memory)
WBEMFlagDirectRead Bypass provider cache
WBEMFlagUseAmendedQualifiers Include localized qualifiers

Property types

Rows returned by Collect and Each methods use map[string]*Property. The Property struct exposes:

Field / Method Description
Value Decoded Go value (int8-uint64, float32/64, bool, string, time.Time, time.Duration, []any)
CIMTypeName() CIM type as a string ("uint32", "string", etc.)
IsArray() True when the property holds an array
IsReference() True when the value is a WMI object path
IsArrayReference() True when the property is an array of references
GetReference(...) Resolve a reference into its own property map
GetArrayReferences(...) Resolve an array of references into property maps
NullDefault True when the value is the null/default for the class
InheritedDefault True when the value is inherited from a parent class

Error handling

All errors satisfy the standard error interface. WMI-specific errors are returned as *wmi.Error, which includes the HRESULT Code, an optional operation name Op, and a human-readable Msg:

var wmiErr *wmi.Error
if errors.As(err, &wmiErr) {
    fmt.Printf("WMI error 0x%08x: %s\n", wmiErr.Code, wmiErr.Msg)
}

WBEM and RPC status names use a broad built-in taxonomy. Unknown WBEM codes fall back to WBEM_E_UNKNOWN; unknown RPC codes fall back to unknown rpc exception.

Sentinel errors:

Variable Meaning
wmi.ErrServerNotOptimized Server does not support SmartEnum; library falls back to standard enumeration
wmi.ErrLegacyEncoding Legacy object encoding encountered (unsupported)
wmi.ErrNotImplemented Feature not yet implemented

Debug logging

The package exposes opt-in debug logging for the DCOM/RPC authentication and request path. Logging is disabled by default.

Enable the built-in logger:

wmi.EnableDebug()

Or attach your own slog logger:

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
	Level: slog.LevelDebug,
}))

wmi.SetLogger(logger.With("component", "wmi"))
wmi.EnableDebug()

You can also enable debugging with the WMI_DEBUG=1 environment variable if you do not call wmi.SetDebug, wmi.EnableDebug, or wmi.DisableDebug.

Debug configuration is intended to be set during process startup, before you start using the package from multiple goroutines.

Disclaimer

This library was built with significant help from Claude and Codex and may include inaccuracies, inefficient code patterns, or potential security vulnerabilities. Please use it with caution. If you encounter any issues, feel free to open an issue or submit a pull request.

Documentation

Overview

Package wmi provides a pure-Go WMI client that communicates with Windows hosts over DCOM/RPC using either NTLM or Kerberos authentication.

Connect with NTLM and query using the convenience API:

client, err := wmi.DialNTLM(ctx, "10.0.0.1", "username", "password")

if err != nil {
	log.Fatal(err)
}
defer client.Close()

var systems []OperatingSystem
if err := client.CollectDecoded(ctx, "SELECT Caption, Version FROM Win32_OperatingSystem", &systems); err != nil {
	log.Fatal(err)
}

Index

Constants

View Source
const (
	WBEMInfinite = 0xffffffff
	WBEMNoWait   = 0x0
)

WBEM timeout constants.

View Source
const (
	WBEMFlagUseAmendedQualifiers = 0x00020000
	WBEMFlagReturnImmediately    = 0x00000010
	WBEMFlagDirectRead           = 0x00000200
	WBEMFlagPrototype            = 0x00000002
	WBEMFlagForwardOnly          = 0x00000020
	WBEMFlagSendStatus           = 0x00000080
)

WBEM query flag constants.

Variables

View Source
var (
	ErrServerNotOptimized     = &Error{Msg: "server does not support smart enumeration"}
	ErrLegacyEncoding         = &Error{Msg: "legacy object encoding is not supported"}
	ErrNotImplemented         = &Error{Msg: "not implemented"}
	ErrKerberosPacketTooLarge = errors.New("kerberos KDC response too large")
)

Sentinel errors.

Functions

func Decode

func Decode(props map[string]*Property, dest any) error

Decode maps WMI properties into a struct. Fields are matched by the "wmi" struct tag, or by exact field name when no tag is present. Use "-" to skip a field.

type OS struct {
    Caption string `wmi:"Caption"`
    Version string `wmi:"Version"`
}
var os OS
wmi.Decode(props, &os)

func DecodeAll

func DecodeAll(rows []map[string]*Property, dest any) error

DecodeAll decodes a slice of property maps into a slice of structs.

var procs []Process
wmi.DecodeAll(rows, &procs)

func DefaultQueryFlags

func DefaultQueryFlags() uint32

DefaultQueryFlags returns the default WBEM query flags.

func DisableDebug

func DisableDebug()

DisableDebug is a shorthand for SetDebug(false).

func EnableDebug

func EnableDebug()

EnableDebug is a shorthand for SetDebug(true).

func FormatWMIDateTime

func FormatWMIDateTime(t time.Time) string

FormatWMIDateTime formats a time.Time as a WMI datetime string.

func ParseWMIDateTime

func ParseWMIDateTime(s string) any

ParseWMIDateTime parses a WMI datetime string into a time.Time or time.Duration.

func SetDebug

func SetDebug(enabled bool)

SetDebug enables or disables debug logging.

func SetLogger

func SetLogger(logger *slog.Logger)

SetLogger replaces the debug logger; pass nil to reset to the default.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a high-level WMI client that owns both the connection and the authenticated service. Create one with DialNTLM or DialKerberos:

client, err := wmi.DialNTLM(ctx, "10.0.0.1", "admin", "secret")
if err != nil { log.Fatal(err) }
defer client.Close()

func DialKerberos

func DialKerberos(
	ctx context.Context,
	host, username, password, domain string,
	opts ...KerberosOption,
) (*Client, error)

DialKerberos connects to host and authenticates using Kerberos, returning a ready-to-use Client. Domain is required. Call Client.Close when done.

client, err := wmi.DialKerberos(ctx, "host.example.com", "admin", "secret", "EXAMPLE.COM")
client, err := wmi.DialKerberos(ctx, host, user, pass, domain, wmi.WithKDC("kdc.host", 88))

func DialNTLM

func DialNTLM(ctx context.Context, host, username, password string, opts ...NTLMOption) (*Client, error)

DialNTLM connects to host and authenticates using NTLM, returning a ready-to-use Client. Call Client.Close when done.

client, err := wmi.DialNTLM(ctx, "10.0.0.1", "admin", "secret")
client, err := wmi.DialNTLM(ctx, host, user, pass, wmi.WithDomain("CORP"))

func (*Client) Close

func (c *Client) Close() error

Close releases the service binding and the underlying connection.

func (*Client) Collect

func (c *Client) Collect(ctx context.Context, wql string, opts ...QueryOption) ([]map[string]*Property, error)

Collect executes wql and returns all result rows in a slice.

rows, err := client.Collect(ctx, "SELECT Name FROM Win32_Process", WithTimeout(120))

func (*Client) CollectDecoded

func (c *Client) CollectDecoded(ctx context.Context, wql string, dest any, opts ...QueryOption) error

CollectDecoded executes wql and decodes all result rows into dest, which must be a non-nil pointer to a slice of structs (or pointers to structs).

type Proc struct{ Name string `wmi:"Name"` }
var procs []Proc
err := client.CollectDecoded(ctx, "SELECT Name FROM Win32_Process", &procs, WithTimeout(120))

func (*Client) Each

func (c *Client) Each(ctx context.Context, wql string, opts ...QueryOption) iter.Seq2[map[string]*Property, error]

Each executes wql and returns an iterator over the result rows. Iteration stops on the first error; break out of the range loop to release resources early.

for props, err := range client.Each(ctx, "SELECT * FROM Win32_Process", WithTimeout(120)) {
    if err != nil { ... }
    fmt.Println(props["Name"].Value)
}

func (*Client) Query

func (c *Client) Query(wql string, opts ...QueryOption) *QContext

Query creates a QContext bound to this client's connection and service.

qc := client.Query("SELECT Name FROM Win32_Process", WithTimeout(120))

type DialOption

type DialOption interface {
	NTLMOption
	KerberosOption
}

DialOption configures connection setup shared by DialNTLM and DialKerberos.

func WithConnectTimeout

func WithConnectTimeout(timeout time.Duration) DialOption

WithConnectTimeout sets a timeout for the overall dial and authentication handshake. The caller's context is still honored and can cancel earlier.

type Error

type Error struct {
	Code uint32
	Op   string
	Msg  string
}

Error represents a WMI or RPC error with an associated status code.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether target matches this error by code.

type KerberosCache

type KerberosCache struct {
	// contains filtered or unexported fields
}

KerberosCache holds cached Kerberos TGT and TGS ticket material.

func NewKerberosCache

func NewKerberosCache(filePath string) *KerberosCache

NewKerberosCache creates a new KerberosCache backed by the given file path.

func (*KerberosCache) FilePath

func (k *KerberosCache) FilePath() string

FilePath returns the file path of the cache, or empty if in-memory only.

func (*KerberosCache) HasValidKeys

func (k *KerberosCache) HasValidKeys(offset ...time.Duration) bool

HasValidKeys reports whether the cached keys are present and not yet expired.

func (*KerberosCache) Load

func (k *KerberosCache) Load() error

Load reads cached ticket data from the file on disk.

func (*KerberosCache) Save

func (k *KerberosCache) Save() error

Save persists the current ticket data to disk.

type KerberosOption

type KerberosOption interface {
	// contains filtered or unexported methods
}

KerberosOption configures DialKerberos.

func WithKDC

func WithKDC(host string, port int) KerberosOption

WithKDC overrides the Kerberos KDC host and port. By default DialKerberos uses the target host on port 88.

func WithKerberosCache

func WithKerberosCache(cache *KerberosCache) KerberosOption

WithKerberosCache sets a custom Kerberos ticket cache.

type NTLMOption

type NTLMOption interface {
	// contains filtered or unexported methods
}

NTLMOption configures DialNTLM.

func WithDomain

func WithDomain(domain string) NTLMOption

WithDomain sets the domain for NTLM authentication.

type Property

type Property struct {
	Name             string
	Type             uint16
	Order            uint16
	Value            any
	Qualifiers       []Qualifier
	NullDefault      bool
	InheritedDefault bool
}

Property represents a single WMI property with its name, type, and decoded value.

func (*Property) CIMTypeName

func (p *Property) CIMTypeName() string

CIMTypeName returns the human-readable CIM type name for the property.

func (*Property) GetArrayReferences

func (p *Property) GetArrayReferences(
	ctx context.Context,
	client *Client,
	filterProps []string,
	missingAsNil bool,
) ([]map[string]*Property, error)

GetArrayReferences resolves each element of an array-of-references property.

func (*Property) GetReference

func (p *Property) GetReference(
	ctx context.Context,
	client *Client,
	filterProps []string,
) (map[string]*Property, error)

GetReference resolves a single reference property and returns the referenced object's properties.

func (*Property) IsArray

func (p *Property) IsArray() bool

IsArray reports whether the property holds an array value.

func (*Property) IsArrayReference

func (p *Property) IsArrayReference() bool

IsArrayReference reports whether the property is an array of CIM references.

func (*Property) IsReference

func (p *Property) IsReference() bool

IsReference reports whether the property is a CIM reference.

type QContext

type QContext struct {
	// contains filtered or unexported fields
}

QContext holds the state for an in-progress WMI query.

func (*QContext) Collect

func (q *QContext) Collect(ctx context.Context) ([]map[string]*Property, error)

Collect executes the query and returns all result rows in a slice.

func (*QContext) CollectDecoded

func (q *QContext) CollectDecoded(ctx context.Context, dest any) error

CollectDecoded executes the query and decodes all result rows into dest, which must be a non-nil pointer to a slice of structs (or pointers to structs).

func (*QContext) Each

func (q *QContext) Each(ctx context.Context) iter.Seq2[map[string]*Property, error]

Each returns an iterator over the query result rows. The query is started lazily on the first call to the iterator and closed when iteration ends.

for props, err := range qc.Each(ctx) {
    if err != nil { break }
    fmt.Println(props["Name"].Value)
}

type Qualifier

type Qualifier struct {
	Name   string
	Flavor byte
	Type   uint32
	Value  any
}

Qualifier represents a CIM qualifier attached to a property or class.

type Query

type Query struct {
	Query     string
	Namespace string
	Language  string
	Flags     uint32
	Timeout   uint32

	SkipOptimize  bool
	ResultOptions ResultOptions
	// contains filtered or unexported fields
}

Query describes a WQL query to execute against a WMI namespace.

func NewQuery

func NewQuery(query string, opts ...QueryOption) Query

NewQuery creates a Query with default namespace and language.

type QueryOption

type QueryOption func(*Query)

QueryOption configures a Query.

func WithFlags

func WithFlags(flags uint32) QueryOption

WithFlags overrides the WBEM query flags.

func WithLanguage

func WithLanguage(language string) QueryOption

WithLanguage overrides the query language. The default is WQL.

func WithNamespace

func WithNamespace(namespace string) QueryOption

WithNamespace overrides the default WMI namespace for a query.

func WithResultOptions

func WithResultOptions(options ResultOptions) QueryOption

WithResultOptions configures property shaping for query results.

func WithSkipOptimize

func WithSkipOptimize(skip bool) QueryOption

WithSkipOptimize disables the smart-enum optimization when set to true.

func WithTimeout

func WithTimeout(timeout uint32) QueryOption

WithTimeout sets the default per-row fetch timeout in milliseconds.

type ResultOptions

type ResultOptions struct {
	// IgnoreDefaults skips loading class-level default values.
	// Properties whose values are missing in the instance data
	// will have their Value set to nil instead of the class default.
	// Automatically treated as true when IgnoreMissing is enabled.
	IgnoreDefaults bool

	// IgnoreMissing removes properties from the result map entirely
	// when their values are not present in the instance data.
	// This implies IgnoreDefaults.
	IgnoreMissing bool

	// LoadQualifiers controls whether property qualifiers (e.g. key,
	// read, write) are populated on each Property. When false (the
	// default), the Qualifiers slice is left empty.
	LoadQualifiers bool
}

ResultOptions controls which properties are included in query results.

func DefaultResultOptions

func DefaultResultOptions() ResultOptions

DefaultResultOptions returns the default ResultOptions.

Directories

Path Synopsis
examples
kerberos command
ntlm command
sysinfo command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL