diff --git a/Makefile b/Makefile index d4de7b13fd..faec1ed583 100644 --- a/Makefile +++ b/Makefile @@ -233,7 +233,7 @@ test: generate # -run=^$ will never match any of our regular non-benchmark tests, ensuring those don't run during benchmarking test-bench-tables: generate - go test ./ee/tables/... ./pkg/windows/windowsupdate/... ./pkg/osquery/table/... -bench=. -count=20 -run=^$ -benchmem + go test ./ee/tables/... ./ee/windowsupdate/... ./pkg/osquery/table/... -bench=. -count=20 -run=^$ -benchmem ## ## Lint diff --git a/cmd/launcher/query_windowsupdates_windows.go b/cmd/launcher/query_windowsupdates_windows.go index 0e822efb43..2959d18fa4 100644 --- a/cmd/launcher/query_windowsupdates_windows.go +++ b/cmd/launcher/query_windowsupdates_windows.go @@ -11,8 +11,8 @@ import ( comshim "github.com/NozomiNetworks/go-comshim" "github.com/kolide/launcher/v2/ee/tables/windowsupdatetable" + "github.com/kolide/launcher/v2/ee/windowsupdate" "github.com/kolide/launcher/v2/pkg/log/multislogger" - "github.com/kolide/launcher/v2/pkg/windows/windowsupdate" "github.com/peterbourgon/ff/v3" ) diff --git a/ee/tables/windowsupdatetable/windowsupdate.go b/ee/tables/windowsupdatetable/windowsupdate.go index 00b02e8b4e..06f900c7e2 100644 --- a/ee/tables/windowsupdatetable/windowsupdate.go +++ b/ee/tables/windowsupdatetable/windowsupdate.go @@ -18,7 +18,7 @@ import ( "github.com/kolide/launcher/v2/ee/tables/dataflattentable" "github.com/kolide/launcher/v2/ee/tables/tablehelpers" "github.com/kolide/launcher/v2/ee/tables/tablewrapper" - "github.com/kolide/launcher/v2/pkg/windows/windowsupdate" + "github.com/kolide/launcher/v2/ee/windowsupdate" "github.com/osquery/osquery-go/plugin/table" ) diff --git a/ee/windowsupdate/COM_LEAK_ANALYSIS-2026-04.md b/ee/windowsupdate/COM_LEAK_ANALYSIS-2026-04.md new file mode 100644 index 0000000000..c4330a2941 --- /dev/null +++ b/ee/windowsupdate/COM_LEAK_ANALYSIS-2026-04.md @@ -0,0 +1,105 @@ +# COM Memory Leak Analysis: windowsupdate package + +## Background + +The `pkg/windows/windowsupdate` package provides a Go binding for the Windows +Update Agent API using go-ole. It derives from +https://github.com/ceshihao/windowsupdate (which has the same bugs). + +A memory leak was observed when calling `IUpdateSearcher.Search` in-process. +The leak was severe enough that a workaround was added: run the query in a +subprocess (`launcher query-windowsupdates`) so the leaked memory is reclaimed +on process exit. See `ee/tables/windowsupdatetable/windowsupdate.go` and +`cmd/launcher/query_windowsupdates_windows.go`. + +## Root Cause + +The leak is caused by **missing COM reference cleanup** throughout the package. +There are two categories: + +### 1. VARIANTs never cleared + +Every call to `oleutil.GetProperty` or `oleutil.CallMethod` returns a +`*ole.VARIANT`. The VARIANT owns a reference to the underlying COM data (a +BSTR for strings, an IDispatch for objects, etc.). The caller must call +`result.Clear()` when done, which calls the Win32 `VariantClear` function to +free that reference. + +The package used an `oleconv` helper layer (`pkg/windows/oleconv`) that +consumed the VARIANT, extracted the Go value, and returned -- but **never +called `Clear()`**. Since every property access on every COM object went through +oleconv, this leaked on every single `GetProperty` call. For a search returning +200 updates with ~40 properties each, that's ~8000+ leaked VARIANTs per query. + +### 2. IDispatch pointers never Released + +Every struct (`IUpdateSession`, `IUpdateSearcher`, `ISearchResult`, `IUpdate`, +`ICategory`, `IUpdateHistoryEntry`, etc.) stored a `disp *ole.IDispatch` field +but never called `Release()` on it. Additionally, intermediate IDispatch +pointers from collection iteration (e.g. the IUpdateCollection dispatch from +`GetProperty(searchResult, "Updates")`) were used and then abandoned. + +### 3. IUnknown from CreateObject never Released + +`oleutil.CreateObject` returns an `*ole.IUnknown` with refcount 1. +`QueryInterface(IID_IDispatch)` adds another reference. The original IUnknown +was never Released, leaking one reference per session creation. (This was fixed +in the first pass of this investigation.) + +## What about STA vs MTA? + +The go-comshim library initializes COM in MTA (Multi-Threaded Apartment) mode. +A note from a Windows developer suggested the leak might be related to +STA/MTA threading and IUnknown reference count bugs (off-by-one, "0 vs 1"). + +Investigation found that `Microsoft.Update.Session` (CLSID +`{4CB43D7F-7EEE-4906-8698-60DA1C38F2FE}`) is registered with +`ThreadingModel = "Both"`, meaning it works in either STA or MTA. So apartment +model mismatch is **not** the root cause for this specific COM class. + +However, go-ole does have documented apartment-sensitive leak behavior +(https://github.com/go-ole/go-ole/issues/135), so apartment model can be a +contributing factor in general. + +## The oleconv problem + +`oleconv` (`pkg/windows/oleconv/oleconv.go`) was a thin wrapper that took a +`(*ole.VARIANT, error)` tuple from oleutil and returned a typed Go value. It +existed only to make property access a one-liner: + + oleconv.ToStringErr(oleutil.GetProperty(disp, "Title")) + +The problem: it consumed the VARIANT and discarded it, making cleanup +impossible at the call site. The VARIANT couldn't be `Clear()`'d because it +was already lost. oleconv had no other consumers outside the windowsupdate +package. + +## The fix: drop oleconv, follow wmi.go pattern + +The `ee/wmi/wmi.go` package in this same codebase does COM correctly: +- `defer unknown.Release()` after CreateObject +- `defer disp.Release()` after QueryInterface +- `defer raw.Clear()` after GetProperty/CallMethod +- Does NOT call Release on an IDispatch obtained via ToIDispatch(), because + ToIDispatch() is just a cast to the same memory; Clear() handles it + +The fix rewrites the windowsupdate package to follow this same pattern: +- Work with raw `*ole.VARIANT` directly +- `defer variant.Clear()` immediately after receiving it +- Extract the Go value from the variant after deferring the clear +- `defer disp.Release()` in each `toI*` function for the dispatch being consumed +- Remove the `disp` field from structs (it's never used after construction + in the query path) +- Delete oleconv entirely + +## Key references + +- COM refcount rules: https://learn.microsoft.com/en-us/windows/win32/com/rules-for-managing-reference-counts +- IUnknown: https://learn.microsoft.com/en-us/windows/win32/com/using-and-implementing-iunknown +- CoInitializeEx (STA/MTA): https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex +- go-ole issue #135 (apartment-sensitive leak): https://github.com/go-ole/go-ole/issues/135 +- go-ole IUnknown: https://github.com/go-ole/go-ole/blob/master/iunknown.go +- Upstream library (same bugs): https://github.com/ceshihao/windowsupdate +- wmi.go arm64 panic note: ToIDispatch() is a cast, not a new reference; + calling Release() on the IDispatch AND Clear() on the VARIANT double-frees + and panics on arm64. Clear() the VARIANT only. diff --git a/ee/windowsupdate/docs.go b/ee/windowsupdate/docs.go new file mode 100644 index 0000000000..13fc5dfffc --- /dev/null +++ b/ee/windowsupdate/docs.go @@ -0,0 +1,28 @@ +// Package windowsupdate provides a go-ole interface to the windows +// update agent. +// +// This code derives from https://github.com/ceshihao/windowsupdate +// +// # COM lifecycle management +// +// This package follows the same COM cleanup pattern as ee/wmi/wmi.go: +// +// - oleutil.GetProperty / CallMethod return a *ole.VARIANT. We call +// defer v.Clear() immediately to free the underlying COM reference +// (BSTR, IDispatch, etc.) via Win32 VariantClear. +// - For IDispatch results, we do NOT clear the VARIANT because +// ToIDispatch() is a pointer cast into the same memory. Instead, +// the caller calls Release() on the IDispatch when done. See the +// arm64 panic note in ee/wmi/wmi.go for why this matters. +// - CreateObject returns an IUnknown with refcount 1. QueryInterface +// adds another ref. We defer unknown.Release() immediately. +// - Each toI* function receives an IDispatch and (for types that +// are fully consumed during construction) calls defer disp.Release(). +// +// The helper functions in olehelpers.go centralize the VARIANT +// extraction + Clear() pattern so individual files don't need to +// repeat it. +// +// For background on the memory leak this addresses, see +// COM_LEAK_ANALYSIS.md in this directory. +package windowsupdate diff --git a/ee/windowsupdate/icategory.go b/ee/windowsupdate/icategory.go new file mode 100644 index 0000000000..fa2cde65ca --- /dev/null +++ b/ee/windowsupdate/icategory.go @@ -0,0 +1,97 @@ +package windowsupdate + +import ( + "fmt" + + "github.com/go-ole/go-ole" +) + +// ICategory represents the category to which an update belongs. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-icategory +type ICategory struct { + CategoryID string + Children []*ICategory + Description string + Image *IImageInformation + Name string + Order int32 + Parent *ICategory + Type string + Updates []*IUpdate +} + +func toICategories(categoriesDisp *ole.IDispatch) ([]*ICategory, error) { + count, err := getPropertyInt32(categoriesDisp, "Count") + if err != nil { + return nil, fmt.Errorf("Count: %w", err) + } + + categories := make([]*ICategory, 0, count) + for i := 0; i < int(count); i++ { + categoryDisp, err := getPropertyDispatch(categoriesDisp, "Item", i) + if err != nil { + return nil, fmt.Errorf("Item[%d/%d]: %w", i, count, err) + } + + category, err := toICategory(categoryDisp) + if err != nil { + return nil, fmt.Errorf("converting Item[%d/%d]: %w", i, count, err) + } + + categories = append(categories, category) + } + return categories, nil +} + +func toICategory(categoryDisp *ole.IDispatch) (*ICategory, error) { + defer categoryDisp.Release() + + var err error + iCategory := &ICategory{} + + if iCategory.CategoryID, err = getPropertyString(categoryDisp, "CategoryID"); err != nil { + return nil, fmt.Errorf("CategoryID: %w", err) + } + + if childrenDisp, err := getPropertyDispatch(categoryDisp, "Children"); err != nil { + return nil, fmt.Errorf("Children: %w", err) + } else if childrenDisp != nil { + defer childrenDisp.Release() + if iCategory.Children, err = toICategories(childrenDisp); err != nil { + return nil, fmt.Errorf("converting Children: %w", err) + } + } + + if iCategory.Description, err = getPropertyString(categoryDisp, "Description"); err != nil { + return nil, fmt.Errorf("Description: %w", err) + } + + if imageDisp, err := getPropertyDispatch(categoryDisp, "Image"); err != nil { + return nil, fmt.Errorf("Image: %w", err) + } else if imageDisp != nil { + defer imageDisp.Release() + if iCategory.Image, err = toIImageInformation(imageDisp); err != nil { + return nil, fmt.Errorf("converting Image: %w", err) + } + } + + if iCategory.Name, err = getPropertyString(categoryDisp, "Name"); err != nil { + return nil, fmt.Errorf("Name: %w", err) + } + + if iCategory.Order, err = getPropertyInt32(categoryDisp, "Order"); err != nil { + return nil, fmt.Errorf("Order: %w", err) + } + + // Parent is commented out to avoid infinite recursion (Parent -> Category -> Parent ...) + // See original code. + + if iCategory.Type, err = getPropertyString(categoryDisp, "Type"); err != nil { + return nil, fmt.Errorf("Type: %w", err) + } + + // Updates is commented out to avoid pulling the full update tree per category. + // See original code. + + return iCategory, nil +} diff --git a/pkg/windows/windowsupdate/iimageinformation.go b/ee/windowsupdate/iimageinformation.go similarity index 90% rename from pkg/windows/windowsupdate/iimageinformation.go rename to ee/windowsupdate/iimageinformation.go index 212fd43137..b281be5173 100644 --- a/pkg/windows/windowsupdate/iimageinformation.go +++ b/ee/windowsupdate/iimageinformation.go @@ -7,7 +7,6 @@ import ( // IImageInformation contains information about a localized image that is associated with an update or a category. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iimageinformation type IImageInformation struct { - disp *ole.IDispatch //nolint:unused AltText string Height int64 Source string @@ -15,6 +14,6 @@ type IImageInformation struct { } func toIImageInformation(imageInformationDisp *ole.IDispatch) (*IImageInformation, error) { - // TODO + // TODO: implement property extraction return nil, nil } diff --git a/pkg/windows/windowsupdate/iinstallationbehavior.go b/ee/windowsupdate/iinstallationbehavior.go similarity index 91% rename from pkg/windows/windowsupdate/iinstallationbehavior.go rename to ee/windowsupdate/iinstallationbehavior.go index 0aa631b0a0..5d2c930963 100644 --- a/pkg/windows/windowsupdate/iinstallationbehavior.go +++ b/ee/windowsupdate/iinstallationbehavior.go @@ -7,7 +7,6 @@ import ( // IInstallationBehavior represents the installation and uninstallation options of an update. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iinstallationbehavior type IInstallationBehavior struct { - disp *ole.IDispatch //nolint:unused CanRequestUserInput bool Impact int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-installationimpact RebootBehavior int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-installationrebootbehavior @@ -15,6 +14,6 @@ type IInstallationBehavior struct { } func toIInstallationBehavior(installationBehaviorDisp *ole.IDispatch) (*IInstallationBehavior, error) { - // TODO + // TODO: implement property extraction return nil, nil } diff --git a/ee/windowsupdate/isearchresult.go b/ee/windowsupdate/isearchresult.go new file mode 100644 index 0000000000..509ccec8e1 --- /dev/null +++ b/ee/windowsupdate/isearchresult.go @@ -0,0 +1,62 @@ +package windowsupdate + +import ( + "fmt" + + "github.com/go-ole/go-ole" +) + +// ISearchResult represents the result of a search. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-isearchresult +type ISearchResult struct { + ResultCode int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode + RootCategories []*ICategory + Updates []*IUpdate + Warnings []*IUpdateException +} + +func toISearchResult(searchResultDisp *ole.IDispatch) (*ISearchResult, error) { + defer searchResultDisp.Release() + + var err error + iSearchResult := &ISearchResult{} + + if iSearchResult.ResultCode, err = getPropertyInt32(searchResultDisp, "ResultCode"); err != nil { + return nil, fmt.Errorf("ResultCode: %w", err) + } + + rootCategoriesDisp, err := getPropertyDispatch(searchResultDisp, "RootCategories") + if err != nil { + return nil, fmt.Errorf("RootCategories: %w", err) + } + if rootCategoriesDisp != nil { + defer rootCategoriesDisp.Release() + if iSearchResult.RootCategories, err = toICategories(rootCategoriesDisp); err != nil { + return nil, fmt.Errorf("converting RootCategories: %w", err) + } + } + + updatesDisp, err := getPropertyDispatch(searchResultDisp, "Updates") + if err != nil { + return nil, fmt.Errorf("Updates: %w", err) + } + if updatesDisp != nil { + defer updatesDisp.Release() + if iSearchResult.Updates, err = toIUpdates(updatesDisp); err != nil { + return nil, fmt.Errorf("converting Updates: %w", err) + } + } + + warningsDisp, err := getPropertyDispatch(searchResultDisp, "Warnings") + if err != nil { + return nil, fmt.Errorf("Warnings: %w", err) + } + if warningsDisp != nil { + defer warningsDisp.Release() + if iSearchResult.Warnings, err = toIUpdateExceptions(warningsDisp); err != nil { + return nil, fmt.Errorf("converting Warnings: %w", err) + } + } + + return iSearchResult, nil +} diff --git a/ee/windowsupdate/istringcollection.go b/ee/windowsupdate/istringcollection.go new file mode 100644 index 0000000000..fdcce509d0 --- /dev/null +++ b/ee/windowsupdate/istringcollection.go @@ -0,0 +1,33 @@ +package windowsupdate + +import ( + "fmt" + + "github.com/go-ole/go-ole" +) + +// iStringCollectionToStringArray takes an IDispatch to a string collection +// and returns the array of strings. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-istringcollection +func iStringCollectionToStringArray(disp *ole.IDispatch) ([]string, error) { + if disp == nil { + return nil, nil + } + + count, err := getPropertyInt32(disp, "Count") + if err != nil { + return nil, fmt.Errorf("Count: %w", err) + } + + stringCollection := make([]string, count) + + for i := 0; i < int(count); i++ { + str, err := getPropertyString(disp, "Item", i) + if err != nil { + return nil, fmt.Errorf("Item[%d/%d]: %w", i, count, err) + } + + stringCollection[i] = str + } + return stringCollection, nil +} diff --git a/ee/windowsupdate/iupdate.go b/ee/windowsupdate/iupdate.go new file mode 100644 index 0000000000..45d592db78 --- /dev/null +++ b/ee/windowsupdate/iupdate.go @@ -0,0 +1,440 @@ +package windowsupdate + +import ( + "fmt" + "time" + + "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" +) + +// IUpdate contains the properties and methods that are available to an update. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdate +type IUpdate struct { + disp *ole.IDispatch + AutoDownload int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate5-get_autodownload + AutoSelection int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate5-get_autoselection + AutoSelectOnWebSites bool + BundledUpdates []*IUpdateIdentity // These are full IUpdate objects, but we truncate them + BrowseOnly bool // From IUpdate3 + CanRequireSource bool + Categories []*ICategory + CveIDs []string // From IUpdate2 + Deadline *time.Time + DeltaCompressedContentAvailable bool + DeltaCompressedContentPreferred bool + DeploymentAction int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-deploymentaction + Description string + DownloadContents []*IUpdateDownloadContent + DownloadPriority int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-downloadpriority + EulaAccepted bool + EulaText string + HandlerID string + Identity *IUpdateIdentity + Image *IImageInformation + InstallationBehavior *IInstallationBehavior + IsBeta bool + IsDownloaded bool + IsHidden bool + IsInstalled bool + IsMandatory bool + IsPresent bool // From IUpdate2 + IsUninstallable bool + KBArticleIDs []string + Languages []string + LastDeploymentChangeTime *time.Time + MaxDownloadSize int64 + MinDownloadSize int64 + MoreInfoUrls []string + MsrcSeverity string + PerUser bool // From IUpdate4 + RebootRequired bool // From IUpdate2 + RecommendedCpuSpeed int32 + RecommendedHardDiskSpace int32 + RecommendedMemory int32 + ReleaseNotes string + SecurityBulletinIDs []string + SupersededUpdateIDs []string + SupportUrl string + Title string + UninstallationBehavior *IInstallationBehavior + UninstallationNotes string + UninstallationSteps []string +} + +// toIUpdates takes a IUpdateCollection and returns a []*IUpdate +func toIUpdates(updatesDisp *ole.IDispatch) ([]*IUpdate, error) { + count, err := getPropertyInt32(updatesDisp, "Count") + if err != nil { + return nil, fmt.Errorf("Count: %w", err) + } + + updates := make([]*IUpdate, count) + for i := 0; i < int(count); i++ { + updateDisp, err := getPropertyDispatch(updatesDisp, "Item", i) + if err != nil { + return nil, fmt.Errorf("Item[%d/%d]: %w", i, count, err) + } + + update, err := toIUpdate(updateDisp) + if err != nil { + return nil, fmt.Errorf("converting Item[%d/%d]: %w", i, count, err) + } + + updates[i] = update + } + return updates, nil +} + +// toIUpdatesIdentities takes a IUpdateCollection and returns the +// []*IUpdateIdentity of the contained IUpdates. This is *not* recursive, though possibly should be. +func toIUpdatesIdentities(updatesDisp *ole.IDispatch) ([]*IUpdateIdentity, error) { + if updatesDisp == nil { + return nil, nil + } + + count, err := getPropertyInt32(updatesDisp, "Count") + if err != nil { + return nil, fmt.Errorf("Count: %w", err) + } + + identities := make([]*IUpdateIdentity, count) + for i := 0; i < int(count); i++ { + id, err := extractUpdateIdentity(updatesDisp, i, int(count)) + if err != nil { + return nil, err + } + identities[i] = id + } + return identities, nil +} + +func extractUpdateIdentity(updatesDisp *ole.IDispatch, i, count int) (*IUpdateIdentity, error) { + updateDisp, err := getPropertyDispatch(updatesDisp, "Item", i) + if err != nil { + return nil, fmt.Errorf("Item[%d/%d]: %w", i, count, err) + } + defer updateDisp.Release() + + identityDisp, err := getPropertyDispatch(updateDisp, "Identity") + if err != nil { + return nil, fmt.Errorf("Identity[%d/%d]: %w", i, count, err) + } + if identityDisp == nil { + return nil, nil + } + id, err := toIUpdateIdentity(identityDisp) + if err != nil { + return nil, fmt.Errorf("converting Identity[%d/%d]: %w", i, count, err) + } + return id, nil +} + +func toIUpdate(updateDisp *ole.IDispatch) (*IUpdate, error) { + // We keep the disp alive for AcceptEula() and other methods that need it. + // Callers that don't need it can call Release() when done. + var err error + iUpdate := &IUpdate{ + disp: updateDisp, + } + + if iUpdate.AutoDownload, err = getPropertyInt32(updateDisp, "AutoDownload"); err != nil { + return nil, err + } + + if iUpdate.AutoSelection, err = getPropertyInt32(updateDisp, "AutoSelection"); err != nil { + return nil, err + } + + if iUpdate.AutoSelectOnWebSites, err = getPropertyBool(updateDisp, "AutoSelectOnWebSites"); err != nil { + return nil, err + } + + if arrDisp, err := getPropertyDispatch(updateDisp, "BundledUpdates"); err != nil { + return nil, err + } else if arrDisp != nil { + defer arrDisp.Release() + if iUpdate.BundledUpdates, err = toIUpdatesIdentities(arrDisp); err != nil { + return nil, err + } + } + + if iUpdate.BrowseOnly, err = getPropertyBool(updateDisp, "BrowseOnly"); err != nil { + return nil, err + } + + if iUpdate.CanRequireSource, err = getPropertyBool(updateDisp, "CanRequireSource"); err != nil { + return nil, err + } + + if categoriesDisp, err := getPropertyDispatch(updateDisp, "Categories"); err != nil { + return nil, err + } else if categoriesDisp != nil { + defer categoriesDisp.Release() + if iUpdate.Categories, err = toICategories(categoriesDisp); err != nil { + return nil, err + } + } + + if cveDisp, err := getPropertyDispatch(updateDisp, "CveIDs"); err != nil { + return nil, err + } else if cveDisp != nil { + defer cveDisp.Release() + if iUpdate.CveIDs, err = iStringCollectionToStringArray(cveDisp); err != nil { + return nil, err + } + } + + if iUpdate.Deadline, err = getPropertyTime(updateDisp, "Deadline"); err != nil { + return nil, err + } + + if iUpdate.DeltaCompressedContentAvailable, err = getPropertyBool(updateDisp, "DeltaCompressedContentAvailable"); err != nil { + return nil, err + } + + if iUpdate.DeltaCompressedContentPreferred, err = getPropertyBool(updateDisp, "DeltaCompressedContentPreferred"); err != nil { + return nil, err + } + + if iUpdate.DeploymentAction, err = getPropertyInt32(updateDisp, "DeploymentAction"); err != nil { + return nil, err + } + + if iUpdate.Description, err = getPropertyString(updateDisp, "Description"); err != nil { + return nil, err + } + + if downloadContentsDisp, err := getPropertyDispatch(updateDisp, "DownloadContents"); err != nil { + return nil, err + } else if downloadContentsDisp != nil { + defer downloadContentsDisp.Release() + if iUpdate.DownloadContents, err = toIUpdateDownloadContents(downloadContentsDisp); err != nil { + return nil, err + } + } + + if iUpdate.DownloadPriority, err = getPropertyInt32(updateDisp, "DownloadPriority"); err != nil { + return nil, err + } + + if iUpdate.EulaAccepted, err = getPropertyBool(updateDisp, "EulaAccepted"); err != nil { + return nil, err + } + + if iUpdate.EulaText, err = getPropertyString(updateDisp, "EulaText"); err != nil { + return nil, err + } + + if iUpdate.HandlerID, err = getPropertyString(updateDisp, "HandlerID"); err != nil { + return nil, err + } + + if identityDisp, err := getPropertyDispatch(updateDisp, "Identity"); err != nil { + return nil, err + } else if identityDisp != nil { + // toIUpdateIdentity calls Release() on identityDisp internally + if iUpdate.Identity, err = toIUpdateIdentity(identityDisp); err != nil { + return nil, err + } + } + + if imageDisp, err := getPropertyDispatch(updateDisp, "Image"); err != nil { + return nil, err + } else if imageDisp != nil { + defer imageDisp.Release() + if iUpdate.Image, err = toIImageInformation(imageDisp); err != nil { + return nil, err + } + } + + if installBehaviorDisp, err := getPropertyDispatch(updateDisp, "InstallationBehavior"); err != nil { + return nil, err + } else if installBehaviorDisp != nil { + defer installBehaviorDisp.Release() + if iUpdate.InstallationBehavior, err = toIInstallationBehavior(installBehaviorDisp); err != nil { + return nil, err + } + } + + if iUpdate.IsBeta, err = getPropertyBool(updateDisp, "IsBeta"); err != nil { + return nil, err + } + + if iUpdate.IsDownloaded, err = getPropertyBool(updateDisp, "IsDownloaded"); err != nil { + return nil, err + } + + if iUpdate.IsHidden, err = getPropertyBool(updateDisp, "IsHidden"); err != nil { + return nil, err + } + + if iUpdate.IsInstalled, err = getPropertyBool(updateDisp, "IsInstalled"); err != nil { + return nil, err + } + + if iUpdate.IsMandatory, err = getPropertyBool(updateDisp, "IsMandatory"); err != nil { + return nil, err + } + + if iUpdate.IsPresent, err = getPropertyBool(updateDisp, "IsPresent"); err != nil { + return nil, err + } + + if iUpdate.IsUninstallable, err = getPropertyBool(updateDisp, "IsUninstallable"); err != nil { + return nil, err + } + + if kbDisp, err := getPropertyDispatch(updateDisp, "KBArticleIDs"); err != nil { + return nil, err + } else if kbDisp != nil { + defer kbDisp.Release() + if iUpdate.KBArticleIDs, err = iStringCollectionToStringArray(kbDisp); err != nil { + return nil, err + } + } + + if langDisp, err := getPropertyDispatch(updateDisp, "Languages"); err != nil { + return nil, err + } else if langDisp != nil { + defer langDisp.Release() + if iUpdate.Languages, err = iStringCollectionToStringArray(langDisp); err != nil { + return nil, err + } + } + + if iUpdate.LastDeploymentChangeTime, err = getPropertyTime(updateDisp, "LastDeploymentChangeTime"); err != nil { + return nil, err + } + + if iUpdate.MaxDownloadSize, err = getPropertyInt64(updateDisp, "MaxDownloadSize"); err != nil { + return nil, err + } + + if iUpdate.MinDownloadSize, err = getPropertyInt64(updateDisp, "MinDownloadSize"); err != nil { + return nil, err + } + + if moreInfoDisp, err := getPropertyDispatch(updateDisp, "MoreInfoUrls"); err != nil { + return nil, err + } else if moreInfoDisp != nil { + defer moreInfoDisp.Release() + if iUpdate.MoreInfoUrls, err = iStringCollectionToStringArray(moreInfoDisp); err != nil { + return nil, err + } + } + + if iUpdate.MsrcSeverity, err = getPropertyString(updateDisp, "MsrcSeverity"); err != nil { + return nil, err + } + + if iUpdate.PerUser, err = getPropertyBool(updateDisp, "PerUser"); err != nil { + return nil, err + } + + if iUpdate.RebootRequired, err = getPropertyBool(updateDisp, "RebootRequired"); err != nil { + return nil, err + } + + if iUpdate.RecommendedCpuSpeed, err = getPropertyInt32(updateDisp, "RecommendedCpuSpeed"); err != nil { + return nil, err + } + + if iUpdate.RecommendedHardDiskSpace, err = getPropertyInt32(updateDisp, "RecommendedHardDiskSpace"); err != nil { + return nil, err + } + + if iUpdate.RecommendedMemory, err = getPropertyInt32(updateDisp, "RecommendedMemory"); err != nil { + return nil, err + } + + if iUpdate.ReleaseNotes, err = getPropertyString(updateDisp, "ReleaseNotes"); err != nil { + return nil, err + } + + if secBulletinDisp, err := getPropertyDispatch(updateDisp, "SecurityBulletinIDs"); err != nil { + return nil, err + } else if secBulletinDisp != nil { + defer secBulletinDisp.Release() + if iUpdate.SecurityBulletinIDs, err = iStringCollectionToStringArray(secBulletinDisp); err != nil { + return nil, err + } + } + + if supersededDisp, err := getPropertyDispatch(updateDisp, "SupersededUpdateIDs"); err != nil { + return nil, err + } else if supersededDisp != nil { + defer supersededDisp.Release() + if iUpdate.SupersededUpdateIDs, err = iStringCollectionToStringArray(supersededDisp); err != nil { + return nil, err + } + } + + if iUpdate.SupportUrl, err = getPropertyString(updateDisp, "SupportUrl"); err != nil { + return nil, err + } + + if iUpdate.Title, err = getPropertyString(updateDisp, "Title"); err != nil { + return nil, err + } + + if uninstallBehaviorDisp, err := getPropertyDispatch(updateDisp, "UninstallationBehavior"); err != nil { + return nil, err + } else if uninstallBehaviorDisp != nil { + defer uninstallBehaviorDisp.Release() + if iUpdate.UninstallationBehavior, err = toIInstallationBehavior(uninstallBehaviorDisp); err != nil { + return nil, err + } + } + + if iUpdate.UninstallationNotes, err = getPropertyString(updateDisp, "UninstallationNotes"); err != nil { + return nil, err + } + + if uninstallStepsDisp, err := getPropertyDispatch(updateDisp, "UninstallationSteps"); err != nil { + return nil, err + } else if uninstallStepsDisp != nil { + defer uninstallStepsDisp.Release() + if iUpdate.UninstallationSteps, err = iStringCollectionToStringArray(uninstallStepsDisp); err != nil { + return nil, err + } + } + + return iUpdate, nil +} + +//nolint:unused +func toIUpdateCollection(updates []*IUpdate) (*ole.IDispatch, error) { + unknown, err := oleutil.CreateObject("Microsoft.Update.UpdateColl") + if err != nil { + return nil, err + } + defer unknown.Release() + + coll, err := unknown.QueryInterface(ole.IID_IDispatch) + if err != nil { + return nil, err + } + for _, update := range updates { + _, err := oleutil.CallMethod(coll, "Add", update.disp) + if err != nil { + return nil, err + } + } + return coll, nil +} + +// AcceptEula accepts the Microsoft Software License Terms that are associated with Windows Update. Administrators and power users can call this method. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate-accepteula +func (iUpdate *IUpdate) AcceptEula() error { + _, err := oleutil.CallMethod(iUpdate.disp, "AcceptEula") + return err +} + +// Release frees the underlying COM object. +func (iUpdate *IUpdate) Release() { + if iUpdate.disp != nil { + iUpdate.disp.Release() + iUpdate.disp = nil + } +} diff --git a/pkg/windows/windowsupdate/iupdatedownloadcontent.go b/ee/windowsupdate/iupdatedownloadcontent.go similarity index 88% rename from pkg/windows/windowsupdate/iupdatedownloadcontent.go rename to ee/windowsupdate/iupdatedownloadcontent.go index 8d9d44326d..12b031e5de 100644 --- a/pkg/windows/windowsupdate/iupdatedownloadcontent.go +++ b/ee/windowsupdate/iupdatedownloadcontent.go @@ -7,11 +7,10 @@ import ( // IUpdateDownloadContent represents the download content of an update. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdatedownloadcontent type IUpdateDownloadContent struct { - disp *ole.IDispatch //nolint:unused DownloadUrl string } func toIUpdateDownloadContents(updateDownloadContentsDisp *ole.IDispatch) ([]*IUpdateDownloadContent, error) { - // TODO + // TODO: implement property extraction return nil, nil } diff --git a/pkg/windows/windowsupdate/iupdateexception.go b/ee/windowsupdate/iupdateexception.go similarity index 74% rename from pkg/windows/windowsupdate/iupdateexception.go rename to ee/windowsupdate/iupdateexception.go index a6ea577a5d..e13331e6d5 100644 --- a/pkg/windows/windowsupdate/iupdateexception.go +++ b/ee/windowsupdate/iupdateexception.go @@ -7,13 +7,12 @@ import ( // IUpdateException represents info about the aspects of search results returned in the ISearchResult object that were incomplete. For more info, see Remarks. // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdateexception type IUpdateException struct { - disp *ole.IDispatch //nolint:unused - Context int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-updateexceptioncontext + Context int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-updateexceptioncontext HResult int64 Message string } func toIUpdateExceptions(updateExceptionsDisp *ole.IDispatch) ([]*IUpdateException, error) { - // TODO + // TODO: implement property extraction return nil, nil } diff --git a/ee/windowsupdate/iupdatehistoryentry.go b/ee/windowsupdate/iupdatehistoryentry.go new file mode 100644 index 0000000000..5baa3cc441 --- /dev/null +++ b/ee/windowsupdate/iupdatehistoryentry.go @@ -0,0 +1,126 @@ +package windowsupdate + +import ( + "fmt" + "time" + + "github.com/go-ole/go-ole" +) + +// IUpdateHistoryEntry represents the recorded history of an update. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdatehistoryentry +type IUpdateHistoryEntry struct { + ClientApplicationID string + Date *time.Time + Description string + HResult int32 + Operation int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-updateoperation + ResultCode int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode + ServerSelection int32 // enum + ServiceID string + SupportUrl string + Title string + UninstallationNotes string + UninstallationSteps []string + UnmappedResultCode int32 + UpdateIdentity *IUpdateIdentity +} + +func toIUpdateHistoryEntries(updateHistoryEntriesDisp *ole.IDispatch) ([]*IUpdateHistoryEntry, error) { + count, err := getPropertyInt32(updateHistoryEntriesDisp, "Count") + if err != nil { + return nil, fmt.Errorf("Count: %w", err) + } + + updateHistoryEntries := make([]*IUpdateHistoryEntry, count) + for i := 0; i < int(count); i++ { + entryDisp, err := getPropertyDispatch(updateHistoryEntriesDisp, "Item", i) + if err != nil { + return nil, fmt.Errorf("Item[%d/%d]: %w", i, count, err) + } + + entry, err := toIUpdateHistoryEntry(entryDisp) + if err != nil { + return nil, fmt.Errorf("converting Item[%d/%d]: %w", i, count, err) + } + + updateHistoryEntries[i] = entry + } + return updateHistoryEntries, nil +} + +func toIUpdateHistoryEntry(entryDisp *ole.IDispatch) (*IUpdateHistoryEntry, error) { + defer entryDisp.Release() + + var err error + entry := &IUpdateHistoryEntry{} + + if entry.ClientApplicationID, err = getPropertyString(entryDisp, "ClientApplicationID"); err != nil { + return nil, fmt.Errorf("ClientApplicationID: %w", err) + } + + if entry.Date, err = getPropertyTime(entryDisp, "Date"); err != nil { + return nil, fmt.Errorf("Date: %w", err) + } + + if entry.Description, err = getPropertyString(entryDisp, "Description"); err != nil { + return nil, fmt.Errorf("Description: %w", err) + } + + if entry.HResult, err = getPropertyInt32(entryDisp, "HResult"); err != nil { + return nil, fmt.Errorf("HResult: %w", err) + } + + if entry.Operation, err = getPropertyInt32(entryDisp, "Operation"); err != nil { + return nil, fmt.Errorf("Operation: %w", err) + } + + if entry.ResultCode, err = getPropertyInt32(entryDisp, "ResultCode"); err != nil { + return nil, fmt.Errorf("ResultCode: %w", err) + } + + if entry.ServerSelection, err = getPropertyInt32(entryDisp, "ServerSelection"); err != nil { + return nil, fmt.Errorf("ServerSelection: %w", err) + } + + if entry.ServiceID, err = getPropertyString(entryDisp, "ServiceID"); err != nil { + return nil, fmt.Errorf("ServiceID: %w", err) + } + + if entry.SupportUrl, err = getPropertyString(entryDisp, "SupportUrl"); err != nil { + return nil, fmt.Errorf("SupportUrl: %w", err) + } + + if entry.Title, err = getPropertyString(entryDisp, "Title"); err != nil { + return nil, fmt.Errorf("Title: %w", err) + } + + if entry.UninstallationNotes, err = getPropertyString(entryDisp, "UninstallationNotes"); err != nil { + return nil, fmt.Errorf("UninstallationNotes: %w", err) + } + + // UninstallationSteps is a string collection + if uninstallStepsDisp, err := getPropertyDispatch(entryDisp, "UninstallationSteps"); err != nil { + return nil, fmt.Errorf("UninstallationSteps: %w", err) + } else if uninstallStepsDisp != nil { + defer uninstallStepsDisp.Release() + if entry.UninstallationSteps, err = iStringCollectionToStringArray(uninstallStepsDisp); err != nil { + return nil, fmt.Errorf("converting UninstallationSteps: %w", err) + } + } + + if entry.UnmappedResultCode, err = getPropertyInt32(entryDisp, "UnmappedResultCode"); err != nil { + return nil, fmt.Errorf("UnmappedResultCode: %w", err) + } + + if identityDisp, err := getPropertyDispatch(entryDisp, "UpdateIdentity"); err != nil { + return nil, fmt.Errorf("UpdateIdentity: %w", err) + } else if identityDisp != nil { + // toIUpdateIdentity calls Release() on identityDisp internally + if entry.UpdateIdentity, err = toIUpdateIdentity(identityDisp); err != nil { + return nil, fmt.Errorf("converting UpdateIdentity: %w", err) + } + } + + return entry, nil +} diff --git a/ee/windowsupdate/iupdateidentity.go b/ee/windowsupdate/iupdateidentity.go new file mode 100644 index 0000000000..6a825565f4 --- /dev/null +++ b/ee/windowsupdate/iupdateidentity.go @@ -0,0 +1,31 @@ +package windowsupdate + +import ( + "fmt" + + "github.com/go-ole/go-ole" +) + +// IUpdateIdentity represents the unique identifier of an update. +// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdateidentity +type IUpdateIdentity struct { + RevisionNumber int32 + UpdateID string +} + +func toIUpdateIdentity(updateIdentityDisp *ole.IDispatch) (*IUpdateIdentity, error) { + defer updateIdentityDisp.Release() + + var err error + iUpdateIdentity := &IUpdateIdentity{} + + if iUpdateIdentity.RevisionNumber, err = getPropertyInt32(updateIdentityDisp, "RevisionNumber"); err != nil { + return nil, fmt.Errorf("RevisionNumber: %w", err) + } + + if iUpdateIdentity.UpdateID, err = getPropertyString(updateIdentityDisp, "UpdateID"); err != nil { + return nil, fmt.Errorf("UpdateID: %w", err) + } + + return iUpdateIdentity, nil +} diff --git a/pkg/windows/windowsupdate/iupdatesearcher.go b/ee/windowsupdate/iupdatesearcher.go similarity index 53% rename from pkg/windows/windowsupdate/iupdatesearcher.go rename to ee/windowsupdate/iupdatesearcher.go index 2d7cd76e79..e429885bc4 100644 --- a/pkg/windows/windowsupdate/iupdatesearcher.go +++ b/ee/windowsupdate/iupdatesearcher.go @@ -4,8 +4,6 @@ import ( "fmt" "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" ) // IUpdateSearcher searches for updates on a server. @@ -26,28 +24,28 @@ func toIUpdateSearcher(updateSearcherDisp *ole.IDispatch) (*IUpdateSearcher, err disp: updateSearcherDisp, } - if iUpdateSearcher.CanAutomaticallyUpgradeService, err = oleconv.ToBoolErr(oleutil.GetProperty(updateSearcherDisp, "CanAutomaticallyUpgradeService")); err != nil { - return nil, fmt.Errorf("getting property CanAutomaticallyUpgradeService as bool: %w", err) + if iUpdateSearcher.CanAutomaticallyUpgradeService, err = getPropertyBool(updateSearcherDisp, "CanAutomaticallyUpgradeService"); err != nil { + return nil, fmt.Errorf("CanAutomaticallyUpgradeService: %w", err) } - if iUpdateSearcher.ClientApplicationID, err = oleconv.ToStringErr(oleutil.GetProperty(updateSearcherDisp, "ClientApplicationID")); err != nil { - return nil, fmt.Errorf("getting property ClientApplicationID as string: %w", err) + if iUpdateSearcher.ClientApplicationID, err = getPropertyString(updateSearcherDisp, "ClientApplicationID"); err != nil { + return nil, fmt.Errorf("ClientApplicationID: %w", err) } - if iUpdateSearcher.IncludePotentiallySupersededUpdates, err = oleconv.ToBoolErr(oleutil.GetProperty(updateSearcherDisp, "IncludePotentiallySupersededUpdates")); err != nil { - return nil, fmt.Errorf("getting property IncludePotentiallySupersededUpdates as bool: %w", err) + if iUpdateSearcher.IncludePotentiallySupersededUpdates, err = getPropertyBool(updateSearcherDisp, "IncludePotentiallySupersededUpdates"); err != nil { + return nil, fmt.Errorf("IncludePotentiallySupersededUpdates: %w", err) } - if iUpdateSearcher.Online, err = oleconv.ToBoolErr(oleutil.GetProperty(updateSearcherDisp, "Online")); err != nil { - return nil, fmt.Errorf("getting property Online as bool: %w", err) + if iUpdateSearcher.Online, err = getPropertyBool(updateSearcherDisp, "Online"); err != nil { + return nil, fmt.Errorf("Online: %w", err) } - if iUpdateSearcher.ServerSelection, err = oleconv.ToInt32Err(oleutil.GetProperty(updateSearcherDisp, "ServerSelection")); err != nil { - return nil, fmt.Errorf("getting property ServerSelection as int32: %w", err) + if iUpdateSearcher.ServerSelection, err = getPropertyInt32(updateSearcherDisp, "ServerSelection"); err != nil { + return nil, fmt.Errorf("ServerSelection: %w", err) } - if iUpdateSearcher.ServiceID, err = oleconv.ToStringErr(oleutil.GetProperty(updateSearcherDisp, "ServiceID")); err != nil { - return nil, fmt.Errorf("getting property ServiceID as string: %w", err) + if iUpdateSearcher.ServiceID, err = getPropertyString(updateSearcherDisp, "ServiceID"); err != nil { + return nil, fmt.Errorf("ServiceID: %w", err) } return iUpdateSearcher, nil @@ -56,9 +54,9 @@ func toIUpdateSearcher(updateSearcherDisp *ole.IDispatch) (*IUpdateSearcher, err // Search performs a synchronous search for updates. The search uses the search options that are currently configured. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesearcher-search func (iUpdateSearcher *IUpdateSearcher) Search(criteria string) (*ISearchResult, error) { - searchResultDisp, err := oleconv.ToIDispatchErr(oleutil.CallMethod(iUpdateSearcher.disp, "Search", criteria)) + searchResultDisp, err := callMethodDispatch(iUpdateSearcher.disp, "Search", criteria) if err != nil { - return nil, fmt.Errorf("calling Search: %w", err) + return nil, fmt.Errorf("Search: %w", err) } return toISearchResult(searchResultDisp) } @@ -66,17 +64,18 @@ func (iUpdateSearcher *IUpdateSearcher) Search(criteria string) (*ISearchResult, // QueryHistory synchronously queries the computer for the history of the update events. // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesearcher-queryhistory func (iUpdateSearcher *IUpdateSearcher) QueryHistory(startIndex int32, count int32) ([]*IUpdateHistoryEntry, error) { - updateHistoryEntriesDisp, err := oleconv.ToIDispatchErr(oleutil.CallMethod(iUpdateSearcher.disp, "QueryHistory", startIndex, count)) + updateHistoryEntriesDisp, err := callMethodDispatch(iUpdateSearcher.disp, "QueryHistory", startIndex, count) if err != nil { - return nil, fmt.Errorf("calling QueryHistory: %w", err) + return nil, fmt.Errorf("QueryHistory: %w", err) } + defer updateHistoryEntriesDisp.Release() return toIUpdateHistoryEntries(updateHistoryEntriesDisp) } // GetTotalHistoryCount returns the number of update events on the computer. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesearcher-gettotalhistorycount func (iUpdateSearcher *IUpdateSearcher) GetTotalHistoryCount() (int32, error) { - return oleconv.ToInt32Err(oleutil.CallMethod(iUpdateSearcher.disp, "GetTotalHistoryCount")) + return callMethodInt32(iUpdateSearcher.disp, "GetTotalHistoryCount") } // QueryHistoryAll synchronously queries the computer for the history of all update events. diff --git a/ee/windowsupdate/olehelpers.go b/ee/windowsupdate/olehelpers.go new file mode 100644 index 0000000000..60be206a5e --- /dev/null +++ b/ee/windowsupdate/olehelpers.go @@ -0,0 +1,180 @@ +package windowsupdate + +import ( + "fmt" + "time" + + "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" +) + +// The helpers below replace the oleconv package. Each one calls +// oleutil.GetProperty (or CallMethod), extracts a typed Go value, +// and clears the VARIANT to release the underlying COM reference. +// +// For IDispatch results, the caller receives the raw *ole.IDispatch +// and is responsible for calling Release() when done. The VARIANT is +// NOT cleared in that case because ToIDispatch() is a pointer cast +// into the same memory -- clearing the VARIANT would release the +// IDispatch out from under the caller. See the arm64 panic note in +// ee/wmi/wmi.go for details. + +func getPropertyString(disp *ole.IDispatch, property string, params ...interface{}) (string, error) { + v, err := oleutil.GetProperty(disp, property, params...) + if err != nil { + return "", fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return "", nil + } + s, ok := raw.(string) + if !ok { + return "", fmt.Errorf("property %s: expected string, got %T", property, raw) + } + return s, nil +} + +func getPropertyBool(disp *ole.IDispatch, property string) (bool, error) { + v, err := oleutil.GetProperty(disp, property) + if err != nil { + return false, fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return false, nil + } + b, ok := raw.(bool) + if !ok { + return false, fmt.Errorf("property %s: expected bool, got %T", property, raw) + } + return b, nil +} + +func getPropertyInt32(disp *ole.IDispatch, property string) (int32, error) { + v, err := oleutil.GetProperty(disp, property) + if err != nil { + return 0, fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return 0, nil + } + i, ok := raw.(int32) + if !ok { + return 0, fmt.Errorf("property %s: expected int32, got %T", property, raw) + } + return i, nil +} + +func getPropertyInt64(disp *ole.IDispatch, property string) (int64, error) { + v, err := oleutil.GetProperty(disp, property) + if err != nil { + return 0, fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return 0, nil + } + i, ok := raw.(int64) + if !ok { + return 0, fmt.Errorf("property %s: expected int64, got %T", property, raw) + } + return i, nil +} + +func getPropertyUint32(disp *ole.IDispatch, property string) (uint32, error) { + v, err := oleutil.GetProperty(disp, property) + if err != nil { + return 0, fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return 0, nil + } + u, ok := raw.(uint32) + if !ok { + return 0, fmt.Errorf("property %s: expected uint32, got %T", property, raw) + } + return u, nil +} + +func getPropertyTime(disp *ole.IDispatch, property string) (*time.Time, error) { + v, err := oleutil.GetProperty(disp, property) + if err != nil { + return nil, fmt.Errorf("getting property %s: %w", property, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return nil, nil + } + t, ok := raw.(time.Time) + if !ok { + return nil, fmt.Errorf("property %s: expected time.Time, got %T", property, raw) + } + return &t, nil +} + +// getPropertyDispatch returns the IDispatch inside a VARIANT property. +// The caller is responsible for calling Release() on the returned IDispatch. +// The VARIANT is NOT cleared here -- see package comment above. +func getPropertyDispatch(disp *ole.IDispatch, property string, params ...interface{}) (*ole.IDispatch, error) { + v, err := oleutil.GetProperty(disp, property, params...) + if err != nil { + return nil, fmt.Errorf("getting property %s: %w", property, err) + } + + raw := v.Value() + if raw == nil { + return nil, nil + } + + return v.ToIDispatch(), nil +} + +// callMethodInt32 calls a method and returns the int32 result. +func callMethodInt32(disp *ole.IDispatch, method string, params ...interface{}) (int32, error) { + v, err := oleutil.CallMethod(disp, method, params...) + if err != nil { + return 0, fmt.Errorf("calling method %s: %w", method, err) + } + defer v.Clear() + + raw := v.Value() + if raw == nil { + return 0, nil + } + i, ok := raw.(int32) + if !ok { + return 0, fmt.Errorf("method %s: expected int32, got %T", method, raw) + } + return i, nil +} + +// callMethodDispatch calls a method and returns the IDispatch result. +// The caller is responsible for calling Release() on the returned IDispatch. +func callMethodDispatch(disp *ole.IDispatch, method string, params ...interface{}) (*ole.IDispatch, error) { + v, err := oleutil.CallMethod(disp, method, params...) + if err != nil { + return nil, fmt.Errorf("calling method %s: %w", method, err) + } + + raw := v.Value() + if raw == nil { + return nil, nil + } + + return v.ToIDispatch(), nil +} diff --git a/pkg/windows/windowsupdate/session.go b/ee/windowsupdate/session.go similarity index 67% rename from pkg/windows/windowsupdate/session.go rename to ee/windowsupdate/session.go index 7ceca00964..c9c3bd5c4c 100644 --- a/pkg/windows/windowsupdate/session.go +++ b/ee/windowsupdate/session.go @@ -5,7 +5,6 @@ import ( "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" ) // IUpdateSession represents a session in which the caller can perform @@ -25,6 +24,8 @@ func NewUpdateSession() (*IUpdateSession, error) { if err != nil { return nil, fmt.Errorf("creating Microsoft.Update.Session: %w", err) } + defer unknown.Release() + disp, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { return nil, fmt.Errorf("querying IID_IDispatch: %w", err) @@ -39,23 +40,24 @@ func toIUpdateSession(updateSessionDisp *ole.IDispatch) (*IUpdateSession, error) disp: updateSessionDisp, } - if iUpdateSession.ClientApplicationID, err = oleconv.ToStringErr(oleutil.GetProperty(updateSessionDisp, "ClientApplicationID")); err != nil { - return nil, fmt.Errorf("getting property ClientApplicationID as string: %w", err) + if iUpdateSession.ClientApplicationID, err = getPropertyString(updateSessionDisp, "ClientApplicationID"); err != nil { + return nil, fmt.Errorf("ClientApplicationID: %w", err) } - if iUpdateSession.ReadOnly, err = oleconv.ToBoolErr(oleutil.GetProperty(updateSessionDisp, "ReadOnly")); err != nil { - return nil, fmt.Errorf("getting property ReadOnly as bool: %w", err) + if iUpdateSession.ReadOnly, err = getPropertyBool(updateSessionDisp, "ReadOnly"); err != nil { + return nil, fmt.Errorf("ReadOnly: %w", err) } return iUpdateSession, nil } func (iUpdateSession *IUpdateSession) GetLocal() (uint32, error) { - return oleconv.ToUint32Err(oleutil.GetProperty(iUpdateSession.disp, "UserLocale")) + return getPropertyUint32(iUpdateSession.disp, "UserLocale") } func (iUpdateSession *IUpdateSession) SetLocal(locale uint32) error { - if _, err := oleconv.ToUint32Err(oleutil.PutProperty(iUpdateSession.disp, "UserLocale", locale)); err != nil { + _, err := oleutil.PutProperty(iUpdateSession.disp, "UserLocale", locale) + if err != nil { return fmt.Errorf("putproperty userlocale: %w", err) } return nil @@ -64,10 +66,18 @@ func (iUpdateSession *IUpdateSession) SetLocal(locale uint32) error { // CreateUpdateSearcher returns an IUpdateSearcher interface for this session. // https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdatesession-createupdatesearcher func (iUpdateSession *IUpdateSession) CreateUpdateSearcher() (*IUpdateSearcher, error) { - updateSearcherDisp, err := oleconv.ToIDispatchErr(oleutil.CallMethod(iUpdateSession.disp, "CreateUpdateSearcher")) + updateSearcherDisp, err := callMethodDispatch(iUpdateSession.disp, "CreateUpdateSearcher") if err != nil { - return nil, fmt.Errorf("calling CreateUpdateSearcher: %w", err) + return nil, fmt.Errorf("CreateUpdateSearcher: %w", err) } return toIUpdateSearcher(updateSearcherDisp) } + +// Release frees the underlying COM object. +func (iUpdateSession *IUpdateSession) Release() { + if iUpdateSession.disp != nil { + iUpdateSession.disp.Release() + iUpdateSession.disp = nil + } +} diff --git a/ee/windowsupdate/windowsupdate_test.go b/ee/windowsupdate/windowsupdate_test.go new file mode 100644 index 0000000000..3234b10407 --- /dev/null +++ b/ee/windowsupdate/windowsupdate_test.go @@ -0,0 +1,168 @@ +//go:build windows +// +build windows + +package windowsupdate + +import ( + "testing" + + comshim "github.com/NozomiNetworks/go-comshim" + "github.com/kolide/launcher/v2/ee/tables/ci" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func initCOM(t *testing.T) { + t.Helper() + require.NoError(t, comshim.TryAdd(1), "initializing COM") + t.Cleanup(comshim.Done) +} + +func initCOMBench(b *testing.B) { + b.Helper() + require.NoError(b, comshim.TryAdd(1), "initializing COM") + b.Cleanup(comshim.Done) +} + +func TestNewUpdateSession(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + // ClientApplicationID is a string (may be empty on a default session) + assert.IsType(t, "", session.ClientApplicationID) + // ReadOnly should be a bool; default sessions are not read-only + assert.False(t, session.ReadOnly) +} + +func TestCreateUpdateSearcher(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + // ServerSelection is an enum: ssDefault(0), ssManagedServer(1), ssWindowsUpdate(2), ssOthers(3) + assert.GreaterOrEqual(t, searcher.ServerSelection, int32(0)) + assert.LessOrEqual(t, searcher.ServerSelection, int32(3)) + + assert.IsType(t, "", searcher.ServiceID) +} + +func TestGetTotalHistoryCount(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + count, err := searcher.GetTotalHistoryCount() + require.NoError(t, err, "GetTotalHistoryCount") + assert.GreaterOrEqual(t, count, int32(0), "history count should be non-negative") +} + +func TestQueryHistorySmall(t *testing.T) { + t.Parallel() + initCOM(t) + + session, err := NewUpdateSession() + require.NoError(t, err, "NewUpdateSession") + defer session.Release() + + searcher, err := session.CreateUpdateSearcher() + require.NoError(t, err, "CreateUpdateSearcher") + + totalCount, err := searcher.GetTotalHistoryCount() + require.NoError(t, err, "GetTotalHistoryCount") + + if totalCount == 0 { + t.Skip("no update history entries on this machine, skipping") + } + + // Query a small number of entries to keep the test fast + queryCount := totalCount + if queryCount > 3 { + queryCount = 3 + } + + entries, err := searcher.QueryHistory(0, queryCount) + require.NoError(t, err, "QueryHistory") + require.Len(t, entries, int(queryCount)) + + for i, entry := range entries { + assert.NotEmpty(t, entry.Title, "entry[%d].Title should not be empty", i) + + // OperationResultCode enum: orcNotStarted(0), orcInProgress(1), orcSucceeded(2), + // orcSucceededWithErrors(3), orcFailed(4), orcAborted(5) + assert.GreaterOrEqual(t, entry.ResultCode, int32(0), "entry[%d].ResultCode", i) + assert.LessOrEqual(t, entry.ResultCode, int32(5), "entry[%d].ResultCode", i) + + // UpdateOperation enum: uoInstallation(1), uoUninstallation(2) + assert.GreaterOrEqual(t, entry.Operation, int32(1), "entry[%d].Operation", i) + assert.LessOrEqual(t, entry.Operation, int32(2), "entry[%d].Operation", i) + + // UpdateIdentity should be populated + if assert.NotNil(t, entry.UpdateIdentity, "entry[%d].UpdateIdentity", i) { + assert.NotEmpty(t, entry.UpdateIdentity.UpdateID, "entry[%d].UpdateIdentity.UpdateID", i) + } + } +} + +// BenchmarkQueryHistory exercises the full COM lifecycle path in a loop: +// session creation, searcher creation, history query with real VARIANT +// extraction and IDispatch Release. The non-golang-B/op metric captures +// native memory growth -- this is where COM leaks from missing +// Release()/Clear() calls would show up. The test fails if per-op +// native growth exceeds the threshold. +func BenchmarkQueryHistory(b *testing.B) { + initCOMBench(b) + + // Verify there's history to query; skip if not. + session, err := NewUpdateSession() + require.NoError(b, err) + searcher, err := session.CreateUpdateSearcher() + require.NoError(b, err) + totalCount, err := searcher.GetTotalHistoryCount() + require.NoError(b, err) + session.Release() + + if totalCount == 0 { + b.Skip("no update history entries on this machine") + } + + queryCount := totalCount + if queryCount > 5 { + queryCount = 5 + } + + baselineStats := ci.BaselineStats(b) + b.ReportAllocs() + b.ResetTimer() + + for range b.N { + session, err := NewUpdateSession() + require.NoError(b, err) + + searcher, err := session.CreateUpdateSearcher() + require.NoError(b, err) + + entries, err := searcher.QueryHistory(0, queryCount) + require.NoError(b, err) + require.NotEmpty(b, entries) + + session.Release() + } + + ci.ReportNonGolangMemoryUsage(b, baselineStats) +} diff --git a/pkg/windows/oleconv/oleconv.go b/pkg/windows/oleconv/oleconv.go deleted file mode 100644 index 639f76290f..0000000000 --- a/pkg/windows/oleconv/oleconv.go +++ /dev/null @@ -1,160 +0,0 @@ -// Package oleconv provides functions to convert from ole.VARIANT to -// expected types. -// -// It is originally from -// https://github.com/ceshihao/windowsupdate/blob/master/oleconv.go -package oleconv - -import ( - "fmt" - "time" - - "github.com/go-ole/go-ole" -) - -func okToErr(ok bool, t string) error { - if !ok { - return fmt.Errorf("not a %s", t) - } - return nil -} - -func ToIDispatchErr(result *ole.VARIANT, err error) (*ole.IDispatch, error) { - if err != nil { - return nil, err - } - return result.ToIDispatch(), nil -} - -func ToStringSliceErr(result *ole.VARIANT, err error) ([]string, error) { - // It's not clear anything uses this. The know use cases are - // better served by iStringCollectionToStringArrayErr - if err != nil { - return nil, err - } - array := result.ToArray() - if array == nil { - return nil, nil - } - return array.ToStringArray(), nil -} - -func ToInt64Err(result *ole.VARIANT, err error) (int64, error) { - if err != nil { - return 0, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return 0, nil - } - - value, ok := valueRaw.(int64) - return value, okToErr(ok, "int64") -} - -func ToInt32Err(result *ole.VARIANT, err error) (int32, error) { - if err != nil { - return 0, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return 0, nil - } - value, ok := valueRaw.(int32) - return value, okToErr(ok, "int32") -} - -func ToUint32Err(result *ole.VARIANT, err error) (uint32, error) { - if err != nil { - return 0, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return 0, nil - } - - value, ok := valueRaw.(uint32) - return value, okToErr(ok, "uint32") - -} - -func ToFloat64Err(result *ole.VARIANT, err error) (float64, error) { - if err != nil { - return 0, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return 0, nil - } - - value, ok := valueRaw.(float64) - return value, okToErr(ok, "float64") -} - -func ToFloat32Err(result *ole.VARIANT, err error) (float32, error) { - if err != nil { - return 0, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return 0, nil - } - - value, ok := valueRaw.(float32) - return value, okToErr(ok, "float32") -} - -func ToStringErr(result *ole.VARIANT, err error) (string, error) { - if err != nil { - return "", err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return "", nil - } - - value, ok := valueRaw.(string) - return value, okToErr(ok, "string") -} - -func ToBoolErr(result *ole.VARIANT, err error) (bool, error) { - if err != nil { - return false, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return false, nil - } - - value, ok := valueRaw.(bool) - return value, okToErr(ok, "bool") -} - -func ToTimeErr(result *ole.VARIANT, err error) (*time.Time, error) { - if err != nil { - return nil, err - } - - valueRaw := result.Value() - - if valueRaw == nil { - return nil, nil - } - - value, ok := valueRaw.(time.Time) - return &value, okToErr(ok, "time") -} diff --git a/pkg/windows/windowsupdate/docs.go b/pkg/windows/windowsupdate/docs.go deleted file mode 100644 index 5a4779ac4d..0000000000 --- a/pkg/windows/windowsupdate/docs.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package windowsupdate provides a go-ole interface to the windows -// update agent. -// -// This code derives from https://github.com/ceshihao/windowsupdate -package windowsupdate diff --git a/pkg/windows/windowsupdate/icategory.go b/pkg/windows/windowsupdate/icategory.go deleted file mode 100644 index 6794f06915..0000000000 --- a/pkg/windows/windowsupdate/icategory.go +++ /dev/null @@ -1,116 +0,0 @@ -package windowsupdate - -import ( - "fmt" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// ICategory represents the category to which an update belongs. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-icategory -type ICategory struct { - disp *ole.IDispatch - CategoryID string - Children []*ICategory - Description string - Image *IImageInformation - Name string - Order int32 - Parent *ICategory - Type string - Updates []*IUpdate -} - -func toICategories(categoriesDisp *ole.IDispatch) ([]*ICategory, error) { - count, err := oleconv.ToInt32Err(oleutil.GetProperty(categoriesDisp, "Count")) - if err != nil { - return nil, fmt.Errorf("getting property Count as int32: %w", err) - } - - categories := make([]*ICategory, 0, count) - for i := 0; i < int(count); i++ { - categoryDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(categoriesDisp, "Item", i)) - if err != nil { - return nil, fmt.Errorf("getting property Item at index %d of %d as IDispatch: %w", i, count, err) - } - - category, err := toICategory(categoryDisp) - if err != nil { - return nil, fmt.Errorf("converting Item IDispatch at index %d of %d to ICategory: %w", i, count, err) - } - - categories = append(categories, category) - } - return categories, nil -} - -func toICategory(categoryDisp *ole.IDispatch) (*ICategory, error) { - var err error - iCategory := &ICategory{ - disp: categoryDisp, - } - - if iCategory.CategoryID, err = oleconv.ToStringErr(oleutil.GetProperty(categoryDisp, "CategoryID")); err != nil { - return nil, fmt.Errorf("getting property CategoryID as string: %w", err) - } - - childrenDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(categoryDisp, "Children")) - if err != nil { - return nil, fmt.Errorf("getting property Children as IDispatch: %w", err) - } - if childrenDisp != nil { - if iCategory.Children, err = toICategories(childrenDisp); err != nil { - return nil, fmt.Errorf("converting Children IDispatch to ICategories: %w", err) - } - } - - if iCategory.Description, err = oleconv.ToStringErr(oleutil.GetProperty(categoryDisp, "Description")); err != nil { - return nil, fmt.Errorf("getting property Description as string: %w", err) - } - - imageDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(categoryDisp, "Image")) - if err != nil { - return nil, fmt.Errorf("getting property Image as IDispatch: %w", err) - } - if imageDisp != nil { - if iCategory.Image, err = toIImageInformation(imageDisp); err != nil { - return nil, fmt.Errorf("converting Image IDispatch to IImageInformation: %w", err) - } - } - - if iCategory.Name, err = oleconv.ToStringErr(oleutil.GetProperty(categoryDisp, "Name")); err != nil { - return nil, fmt.Errorf("getting property Name as string: %w", err) - } - - if iCategory.Order, err = oleconv.ToInt32Err(oleutil.GetProperty(categoryDisp, "Order")); err != nil { - return nil, fmt.Errorf("getting property Order as int32: %w", err) - } - - // parentDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(categoryDisp, "Parent")) - // if err != nil { - // return nil, err - // } - // if parentDisp != nil { - // if iCategory.Parent, err = toICategory(parentDisp); err != nil { - // return nil, err - // } - // } - - if iCategory.Type, err = oleconv.ToStringErr(oleutil.GetProperty(categoryDisp, "Type")); err != nil { - return nil, fmt.Errorf("getting property Type as string: %w", err) - } - - // updatesDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(categoryDisp, "Updates")) - // if err != nil { - // return nil, err - // } - // if updatesDisp != nil { - // if iCategory.Updates, err = toIUpdates(updatesDisp); err != nil { - // return nil, err - // } - // } - - return iCategory, nil -} diff --git a/pkg/windows/windowsupdate/isearchresult.go b/pkg/windows/windowsupdate/isearchresult.go deleted file mode 100644 index 6fcedfda3b..0000000000 --- a/pkg/windows/windowsupdate/isearchresult.go +++ /dev/null @@ -1,63 +0,0 @@ -package windowsupdate - -import ( - "fmt" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// ISearchResult represents the result of a search. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-isearchresult -type ISearchResult struct { - disp *ole.IDispatch - ResultCode int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode - RootCategories []*ICategory - Updates []*IUpdate - Warnings []*IUpdateException -} - -func toISearchResult(searchResultDisp *ole.IDispatch) (*ISearchResult, error) { - var err error - iSearchResult := &ISearchResult{ - disp: searchResultDisp, - } - - if iSearchResult.ResultCode, err = oleconv.ToInt32Err(oleutil.GetProperty(searchResultDisp, "ResultCode")); err != nil { - return nil, fmt.Errorf("getting property ResultCode as int32: %w", err) - } - - rootCategoriesDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(searchResultDisp, "RootCategories")) - if err != nil { - return nil, fmt.Errorf("getting property RootCategories as IDispatch: %w", err) - } - if rootCategoriesDisp != nil { - if iSearchResult.RootCategories, err = toICategories(rootCategoriesDisp); err != nil { - return nil, fmt.Errorf("converting RootCategories IDispatch to ICategories: %w", err) - } - } - - // Updates is a IUpdateCollection, and we want the full details. So cast it ia toIUpdates - updatesDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(searchResultDisp, "Updates")) - if err != nil { - return nil, fmt.Errorf("getting property Updates as IDispatch: %w", err) - } - if updatesDisp != nil { - if iSearchResult.Updates, err = toIUpdates(updatesDisp); err != nil { - return nil, fmt.Errorf("converting Updates IDispatch to IUpdates: %w", err) - } - } - - warningsDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(searchResultDisp, "Warnings")) - if err != nil { - return nil, fmt.Errorf("getting property Warnings as IDispatch: %w", err) - } - if warningsDisp != nil { - if iSearchResult.Warnings, err = toIUpdateExceptions(warningsDisp); err != nil { - return nil, fmt.Errorf("converting Warnings IDispatch to IUpdateExceptions: %w", err) - } - } - - return iSearchResult, nil -} diff --git a/pkg/windows/windowsupdate/istringcollection.go b/pkg/windows/windowsupdate/istringcollection.go deleted file mode 100644 index 7529561b72..0000000000 --- a/pkg/windows/windowsupdate/istringcollection.go +++ /dev/null @@ -1,39 +0,0 @@ -package windowsupdate - -import ( - "fmt" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// iStringCollectionToStringArrayErr takes a IDispatch to a -// stringcollection, and returns the array of strings -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-istringcollection -func iStringCollectionToStringArrayErr(disp *ole.IDispatch, err error) ([]string, error) { - if err != nil { - return nil, err - } - - if disp == nil { - return nil, nil - } - - count, err := oleconv.ToInt32Err(oleutil.GetProperty(disp, "Count")) - if err != nil { - return nil, fmt.Errorf("getting property Count as int32: %w", err) - } - - stringCollection := make([]string, count) - - for i := 0; i < int(count); i++ { - str, err := oleconv.ToStringErr(oleutil.GetProperty(disp, "Item", i)) - if err != nil { - return nil, fmt.Errorf("getting property Item at index %d of %d as string: %w", i, count, err) - } - - stringCollection[i] = str - } - return stringCollection, nil -} diff --git a/pkg/windows/windowsupdate/iupdate.go b/pkg/windows/windowsupdate/iupdate.go deleted file mode 100644 index 61fa8064e5..0000000000 --- a/pkg/windows/windowsupdate/iupdate.go +++ /dev/null @@ -1,386 +0,0 @@ -package windowsupdate - -import ( - "fmt" - "time" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// IUpdate contains the properties and methods that are available to an update. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdate -type IUpdate struct { - disp *ole.IDispatch - AutoDownload int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate5-get_autodownload - AutoSelection int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate5-get_autoselection - AutoSelectOnWebSites bool - BundledUpdates []*IUpdateIdentity // These are full IUpdate objects, but we truncate them - BrowseOnly bool // From IUpdate3 - CanRequireSource bool - Categories []*ICategory - CveIDs []string // From IUpdate2 - Deadline *time.Time - DeltaCompressedContentAvailable bool - DeltaCompressedContentPreferred bool - DeploymentAction int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-deploymentaction - Description string - DownloadContents []*IUpdateDownloadContent - DownloadPriority int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-downloadpriority - EulaAccepted bool - EulaText string - HandlerID string - Identity *IUpdateIdentity - Image *IImageInformation - InstallationBehavior *IInstallationBehavior - IsBeta bool - IsDownloaded bool - IsHidden bool - IsInstalled bool - IsMandatory bool - IsPresent bool // From IUpdate2 - IsUninstallable bool - KBArticleIDs []string - Languages []string - LastDeploymentChangeTime *time.Time - MaxDownloadSize int64 - MinDownloadSize int64 - MoreInfoUrls []string - MsrcSeverity string - PerUser bool // From IUpdate4 - RebootRequired bool // From IUpdate2 - RecommendedCpuSpeed int32 - RecommendedHardDiskSpace int32 - RecommendedMemory int32 - ReleaseNotes string - SecurityBulletinIDs []string - SupersededUpdateIDs []string - SupportUrl string - Title string - UninstallationBehavior *IInstallationBehavior - UninstallationNotes string - UninstallationSteps []string -} - -// toIUpdates takes a IUpdateCollection and returns a []*IUpdate -func toIUpdates(updatesDisp *ole.IDispatch) ([]*IUpdate, error) { - count, err := oleconv.ToInt32Err(oleutil.GetProperty(updatesDisp, "Count")) - if err != nil { - return nil, fmt.Errorf("getting property Count as int32: %w", err) - } - - updates := make([]*IUpdate, count) - for i := 0; i < int(count); i++ { - updateDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updatesDisp, "Item", i)) - if err != nil { - return nil, fmt.Errorf("getting property Item at index %d of %d as IDispatch: %w", i, count, err) - } - - update, err := toIUpdate(updateDisp) - if err != nil { - return nil, fmt.Errorf("converting Item IDispatch at index %d of %d to IUpdate: %w", i, count, err) - } - - updates[i] = update - } - return updates, nil -} - -// toIUpdates takes a IUpdateCollection and returns the a -// []*IUpdateIdentity of the contained IUpdates. This is *not* recursive, though possible is should be -func toIUpdatesIdentities(updatesDisp *ole.IDispatch) ([]*IUpdateIdentity, error) { - if updatesDisp == nil { - return nil, nil - } - - count, err := oleconv.ToInt32Err(oleutil.GetProperty(updatesDisp, "Count")) - if err != nil { - return nil, fmt.Errorf("getting property Count as int32: %w", err) - } - - identities := make([]*IUpdateIdentity, count) - for i := 0; i < int(count); i++ { - updateDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updatesDisp, "Item", i)) - if err != nil { - return nil, fmt.Errorf("getting property Item at index %d of %d as IDispatch: %w", i, count, err) - } - - identityDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "Identity")) - if err != nil { - return nil, fmt.Errorf("getting property Identity at index %d of %d as IDispatch: %w", i, count, err) - } - if identityDisp != nil { - if identities[i], err = toIUpdateIdentity(identityDisp); err != nil { - return nil, fmt.Errorf("converting Identity IDispatch at index %d of %d to IUpdateIdentity: %w", i, count, err) - } - } - } - return identities, nil -} - -func toIUpdate(updateDisp *ole.IDispatch) (*IUpdate, error) { - var err error - iUpdate := &IUpdate{ - disp: updateDisp, - } - - if iUpdate.AutoDownload, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "AutoDownload")); err != nil { - return nil, err - } - - if iUpdate.AutoSelection, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "AutoSelection")); err != nil { - return nil, err - } - - if iUpdate.AutoSelectOnWebSites, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "AutoSelectOnWebSites")); err != nil { - return nil, err - } - - if arrDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "BundledUpdates")); err != nil { - return nil, err - } else { - if iUpdate.BundledUpdates, err = toIUpdatesIdentities(arrDisp); err != nil { - return nil, err - - } - } - - if iUpdate.BrowseOnly, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "BrowseOnly")); err != nil { - return nil, err - } - - if iUpdate.CanRequireSource, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "CanRequireSource")); err != nil { - return nil, err - } - - if categoriesDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "Categories")); err != nil { - return nil, err - } else if categoriesDisp != nil { - if iUpdate.Categories, err = toICategories(categoriesDisp); err != nil { - return nil, err - } - } - - if iUpdate.CveIDs, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "CveIDs"))); err != nil { - return nil, err - } - - if iUpdate.Deadline, err = oleconv.ToTimeErr(oleutil.GetProperty(updateDisp, "Deadline")); err != nil { - return nil, err - } - - if iUpdate.DeltaCompressedContentAvailable, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "DeltaCompressedContentAvailable")); err != nil { - return nil, err - } - - if iUpdate.DeltaCompressedContentPreferred, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "DeltaCompressedContentPreferred")); err != nil { - return nil, err - } - - if iUpdate.DeploymentAction, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "DeploymentAction")); err != nil { - return nil, err - } - - if iUpdate.Description, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "Description")); err != nil { - return nil, err - } - - downloadContentsDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "DownloadContents")) - if err != nil { - return nil, err - } - if downloadContentsDisp != nil { - if iUpdate.DownloadContents, err = toIUpdateDownloadContents(downloadContentsDisp); err != nil { - return nil, err - } - } - - if iUpdate.DownloadPriority, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "DownloadPriority")); err != nil { - return nil, err - } - - if iUpdate.EulaAccepted, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "EulaAccepted")); err != nil { - return nil, err - } - - if iUpdate.EulaText, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "EulaText")); err != nil { - return nil, err - } - - if iUpdate.HandlerID, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "HandlerID")); err != nil { - return nil, err - } - - identityDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "Identity")) - if err != nil { - return nil, err - } - if identityDisp != nil { - if iUpdate.Identity, err = toIUpdateIdentity(identityDisp); err != nil { - return nil, err - } - } - - imageDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "Image")) - if err != nil { - return nil, err - } - if imageDisp != nil { - if iUpdate.Image, err = toIImageInformation(imageDisp); err != nil { - return nil, err - } - } - - installationBehaviorDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "InstallationBehavior")) - if err != nil { - return nil, err - } - if installationBehaviorDisp != nil { - if iUpdate.InstallationBehavior, err = toIInstallationBehavior(installationBehaviorDisp); err != nil { - return nil, err - } - } - - if iUpdate.IsBeta, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsBeta")); err != nil { - return nil, err - } - - if iUpdate.IsDownloaded, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsDownloaded")); err != nil { - return nil, err - } - - if iUpdate.IsHidden, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsHidden")); err != nil { - return nil, err - } - - if iUpdate.IsInstalled, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsInstalled")); err != nil { - return nil, err - } - - if iUpdate.IsMandatory, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsMandatory")); err != nil { - return nil, err - } - - if iUpdate.IsPresent, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsPresent")); err != nil { - return nil, err - } - - if iUpdate.IsUninstallable, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "IsUninstallable")); err != nil { - return nil, err - } - - if iUpdate.KBArticleIDs, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "KBArticleIDs"))); err != nil { - return nil, err - } - - if iUpdate.Languages, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "Languages"))); err != nil { - return nil, err - } - - if iUpdate.LastDeploymentChangeTime, err = oleconv.ToTimeErr(oleutil.GetProperty(updateDisp, "LastDeploymentChangeTime")); err != nil { - return nil, err - } - - if iUpdate.MaxDownloadSize, err = oleconv.ToInt64Err(oleutil.GetProperty(updateDisp, "MaxDownloadSize")); err != nil { - return nil, err - } - - if iUpdate.MinDownloadSize, err = oleconv.ToInt64Err(oleutil.GetProperty(updateDisp, "MinDownloadSize")); err != nil { - return nil, err - } - - if iUpdate.MoreInfoUrls, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "MoreInfoUrls"))); err != nil { - return nil, err - } - - if iUpdate.MsrcSeverity, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "MsrcSeverity")); err != nil { - return nil, err - } - - if iUpdate.PerUser, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "PerUser")); err != nil { - return nil, err - } - - if iUpdate.RebootRequired, err = oleconv.ToBoolErr(oleutil.GetProperty(updateDisp, "RebootRequired")); err != nil { - return nil, err - } - - if iUpdate.RecommendedCpuSpeed, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "RecommendedCpuSpeed")); err != nil { - return nil, err - } - - if iUpdate.RecommendedHardDiskSpace, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "RecommendedHardDiskSpace")); err != nil { - return nil, err - } - - if iUpdate.RecommendedMemory, err = oleconv.ToInt32Err(oleutil.GetProperty(updateDisp, "RecommendedMemory")); err != nil { - return nil, err - } - - if iUpdate.ReleaseNotes, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "ReleaseNotes")); err != nil { - return nil, err - } - - if iUpdate.SecurityBulletinIDs, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "SecurityBulletinIDs"))); err != nil { - return nil, err - } - - if iUpdate.SupersededUpdateIDs, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "SupersededUpdateIDs"))); err != nil { - return nil, err - } - - if iUpdate.SupportUrl, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "SupportUrl")); err != nil { - return nil, err - } - - if iUpdate.Title, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "Title")); err != nil { - return nil, err - } - - uninstallationBehaviorDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "UninstallationBehavior")) - if err != nil { - return nil, err - } - if uninstallationBehaviorDisp != nil { - if iUpdate.UninstallationBehavior, err = toIInstallationBehavior(uninstallationBehaviorDisp); err != nil { - return nil, err - } - } - - if iUpdate.UninstallationNotes, err = oleconv.ToStringErr(oleutil.GetProperty(updateDisp, "UninstallationNotes")); err != nil { - return nil, err - } - - if iUpdate.UninstallationSteps, err = iStringCollectionToStringArrayErr(oleconv.ToIDispatchErr(oleutil.GetProperty(updateDisp, "UninstallationSteps"))); err != nil { - return nil, err - } - - return iUpdate, nil -} - -//nolint:unused -func toIUpdateCollection(updates []*IUpdate) (*ole.IDispatch, error) { - unknown, err := oleutil.CreateObject("Microsoft.Update.UpdateColl") - if err != nil { - return nil, err - } - coll, err := unknown.QueryInterface(ole.IID_IDispatch) - if err != nil { - return nil, err - } - for _, update := range updates { - _, err := oleutil.CallMethod(coll, "Add", update.disp) - if err != nil { - return nil, err - } - } - return coll, nil -} - -// AcceptEula accepts the Microsoft Software License Terms that are associated with Windows Update. Administrators and power users can call this method. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nf-wuapi-iupdate-accepteula -func (iUpdate *IUpdate) AcceptEula() error { - _, err := oleutil.CallMethod(iUpdate.disp, "AcceptEula") - return err -} diff --git a/pkg/windows/windowsupdate/iupdatehistoryentry.go b/pkg/windows/windowsupdate/iupdatehistoryentry.go deleted file mode 100644 index a53b785533..0000000000 --- a/pkg/windows/windowsupdate/iupdatehistoryentry.go +++ /dev/null @@ -1,124 +0,0 @@ -package windowsupdate - -import ( - "fmt" - "time" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// IUpdateHistoryEntry represents the recorded history of an update. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdatehistoryentry -type IUpdateHistoryEntry struct { - disp *ole.IDispatch - ClientApplicationID string - Date *time.Time - Description string - HResult int32 - Operation int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-updateoperation - ResultCode int32 // enum https://docs.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode - ServerSelection int32 // enum - ServiceID string - SupportUrl string - Title string - UninstallationNotes string - UninstallationSteps []string - UnmappedResultCode int32 - UpdateIdentity *IUpdateIdentity -} - -func toIUpdateHistoryEntries(updateHistoryEntriesDisp *ole.IDispatch) ([]*IUpdateHistoryEntry, error) { - count, err := oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntriesDisp, "Count")) - if err != nil { - return nil, fmt.Errorf("getting property Count as int32: %w", err) - } - - updateHistoryEntries := make([]*IUpdateHistoryEntry, count) - for i := 0; i < int(count); i++ { - updateHistoryEntryDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateHistoryEntriesDisp, "Item", i)) - if err != nil { - return nil, fmt.Errorf("getting property Item at index %d of %d as IDispatch: %w", i, count, err) - } - - updateHistoryEntry, err := toIUpdateHistoryEntry(updateHistoryEntryDisp) - if err != nil { - return nil, fmt.Errorf("converting Item IDispatch at index %d of %d to IUpdateHistoryEntry: %w", i, count, err) - } - - updateHistoryEntries[i] = updateHistoryEntry - } - return updateHistoryEntries, nil -} - -func toIUpdateHistoryEntry(updateHistoryEntryDisp *ole.IDispatch) (*IUpdateHistoryEntry, error) { - var err error - iUpdateHistoryEntry := &IUpdateHistoryEntry{ - disp: updateHistoryEntryDisp, - } - - if iUpdateHistoryEntry.ClientApplicationID, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "ClientApplicationID")); err != nil { - return nil, fmt.Errorf("getting property ClientApplicationID as string: %w", err) - } - - if iUpdateHistoryEntry.Date, err = oleconv.ToTimeErr(oleutil.GetProperty(updateHistoryEntryDisp, "Date")); err != nil { - return nil, fmt.Errorf("getting property Date as time: %w", err) - } - - if iUpdateHistoryEntry.Description, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "Description")); err != nil { - return nil, fmt.Errorf("getting property Description as string: %w", err) - } - - if iUpdateHistoryEntry.HResult, err = oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntryDisp, "HResult")); err != nil { - return nil, fmt.Errorf("getting property HResult as int32: %w", err) - } - - if iUpdateHistoryEntry.Operation, err = oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntryDisp, "Operation")); err != nil { - return nil, fmt.Errorf("getting property Operation as int32: %w", err) - } - - if iUpdateHistoryEntry.ResultCode, err = oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntryDisp, "ResultCode")); err != nil { - return nil, fmt.Errorf("getting property ResultCode as int32: %w", err) - } - - if iUpdateHistoryEntry.ServerSelection, err = oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntryDisp, "ServerSelection")); err != nil { - return nil, fmt.Errorf("getting property ServerSelection as int32: %w", err) - } - - if iUpdateHistoryEntry.ServiceID, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "ServiceID")); err != nil { - return nil, fmt.Errorf("getting property ServiceID as string: %w", err) - } - - if iUpdateHistoryEntry.SupportUrl, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "SupportUrl")); err != nil { - return nil, fmt.Errorf("getting property SupportUrl as string: %w", err) - } - - if iUpdateHistoryEntry.Title, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "Title")); err != nil { - return nil, fmt.Errorf("getting property Title as string: %w", err) - } - - if iUpdateHistoryEntry.UninstallationNotes, err = oleconv.ToStringErr(oleutil.GetProperty(updateHistoryEntryDisp, "UninstallationNotes")); err != nil { - return nil, fmt.Errorf("getting property UninstallationNotes as string: %w", err) - } - - if iUpdateHistoryEntry.UninstallationSteps, err = oleconv.ToStringSliceErr(oleutil.GetProperty(updateHistoryEntryDisp, "UninstallationSteps")); err != nil { - return nil, fmt.Errorf("getting property UninstallationSteps as string slice: %w", err) - } - - if iUpdateHistoryEntry.UnmappedResultCode, err = oleconv.ToInt32Err(oleutil.GetProperty(updateHistoryEntryDisp, "UnmappedResultCode")); err != nil { - return nil, fmt.Errorf("getting property UnmappedResultCode as int32: %w", err) - } - - updateIdentityDisp, err := oleconv.ToIDispatchErr(oleutil.GetProperty(updateHistoryEntryDisp, "UpdateIdentity")) - if err != nil { - return nil, fmt.Errorf("getting property UpdateIdentity as IDispatch: %w", err) - } - if updateIdentityDisp != nil { - if iUpdateHistoryEntry.UpdateIdentity, err = toIUpdateIdentity(updateIdentityDisp); err != nil { - return nil, fmt.Errorf("converting UpdateIdentity IDispatch to IUpdateIdentity: %w", err) - } - } - - return iUpdateHistoryEntry, nil -} diff --git a/pkg/windows/windowsupdate/iupdateidentity.go b/pkg/windows/windowsupdate/iupdateidentity.go deleted file mode 100644 index 86dc8b20be..0000000000 --- a/pkg/windows/windowsupdate/iupdateidentity.go +++ /dev/null @@ -1,34 +0,0 @@ -package windowsupdate - -import ( - "fmt" - - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/kolide/launcher/v2/pkg/windows/oleconv" -) - -// IUpdateIdentity represents the unique identifier of an update. -// https://docs.microsoft.com/en-us/windows/win32/api/wuapi/nn-wuapi-iupdateidentity -type IUpdateIdentity struct { - disp *ole.IDispatch - RevisionNumber int32 - UpdateID string -} - -func toIUpdateIdentity(updateIdentityDisp *ole.IDispatch) (*IUpdateIdentity, error) { - var err error - iUpdateIdentity := &IUpdateIdentity{ - disp: updateIdentityDisp, - } - - if iUpdateIdentity.RevisionNumber, err = oleconv.ToInt32Err(oleutil.GetProperty(updateIdentityDisp, "RevisionNumber")); err != nil { - return nil, fmt.Errorf("getting property RevisionNumber as int32: %w", err) - } - - if iUpdateIdentity.UpdateID, err = oleconv.ToStringErr(oleutil.GetProperty(updateIdentityDisp, "UpdateID")); err != nil { - return nil, fmt.Errorf("getting property UpdateID as string: %w", err) - } - - return iUpdateIdentity, nil -} diff --git a/pkg/windows/windowsupdate/windowsupdate_test.go b/pkg/windows/windowsupdate/windowsupdate_test.go deleted file mode 100644 index 2864466140..0000000000 --- a/pkg/windows/windowsupdate/windowsupdate_test.go +++ /dev/null @@ -1,62 +0,0 @@ -//go:build windows -// +build windows - -package windowsupdate - -import ( - "testing" - - comshim "github.com/NozomiNetworks/go-comshim" - "github.com/kolide/launcher/v2/ee/tables/ci" - "github.com/stretchr/testify/require" -) - -func initCOMBench(b *testing.B) { - b.Helper() - require.NoError(b, comshim.TryAdd(1), "initializing COM") - b.Cleanup(comshim.Done) -} - -// BenchmarkQueryHistory exercises the full COM lifecycle path in a loop: -// session creation, searcher creation, history query with real VARIANT -// extraction and IDispatch Release. The non-golang-B/op metric captures -// native memory growth -- this is where COM leaks from missing -// Release()/Clear() calls would show up. -func BenchmarkQueryHistory(b *testing.B) { - initCOMBench(b) - - // Verify there's history to query; skip if not. - session, err := NewUpdateSession() - require.NoError(b, err) - searcher, err := session.CreateUpdateSearcher() - require.NoError(b, err) - totalCount, err := searcher.GetTotalHistoryCount() - require.NoError(b, err) - - if totalCount == 0 { - b.Skip("no update history entries on this machine") - } - - queryCount := totalCount - if queryCount > 5 { - queryCount = 5 - } - - baselineStats := ci.BaselineStats(b) - b.ReportAllocs() - b.ResetTimer() - - for range b.N { - session, err := NewUpdateSession() - require.NoError(b, err) - - searcher, err := session.CreateUpdateSearcher() - require.NoError(b, err) - - entries, err := searcher.QueryHistory(0, queryCount) - require.NoError(b, err) - require.NotEmpty(b, entries) - } - - ci.ReportNonGolangMemoryUsage(b, baselineStats) -}