Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform presence detection on Windows using UserConsentVerifier interop #1890

Merged
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
31506a0
Proof-of-concept for Windows Hello
RebeccaMahany Sep 12, 2024
c62471b
Merge remote-tracking branch 'upstream/main' into becca/windows-hello
RebeccaMahany Sep 13, 2024
c9e9fe9
Remove debug call
RebeccaMahany Sep 23, 2024
4a8e67b
Rename files and function signature to match interface
RebeccaMahany Sep 23, 2024
c6ad847
Merge remote-tracking branch 'upstream/main' into becca/windows-hello
RebeccaMahany Sep 23, 2024
109d1ed
Add documentation, comment out unused func
RebeccaMahany Sep 23, 2024
9a3b793
Refactor and split up into Register + Detect
RebeccaMahany Sep 23, 2024
1fa02bf
Simplify
RebeccaMahany Sep 23, 2024
262fce2
Call initialize only once, set up manager reference fresh each time
RebeccaMahany Sep 23, 2024
64b0d22
Strip down to just the working parts
RebeccaMahany Sep 23, 2024
00c8c6d
Try RequestVerificationAsync
RebeccaMahany Sep 23, 2024
356e03a
Create window via win32 api
RebeccaMahany Sep 23, 2024
459d98b
Fix call to GetModuleHandleW
RebeccaMahany Sep 23, 2024
86d7c4b
Call RequestVerificationForWindowAsync
RebeccaMahany Oct 14, 2024
d54a300
Tidy, document
RebeccaMahany Oct 14, 2024
7d9d2cd
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 14, 2024
6623056
Get REFIID correct, finally
RebeccaMahany Oct 14, 2024
dcb933c
Call ShowWindow
RebeccaMahany Oct 14, 2024
378e5d0
Get window handle from systray
RebeccaMahany Oct 15, 2024
aba1700
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 15, 2024
bf6c76e
Rename file
RebeccaMahany Oct 16, 2024
e0bf952
Improve documentation
RebeccaMahany Oct 16, 2024
d9360b2
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 16, 2024
4d4064e
Another docs update
RebeccaMahany Oct 16, 2024
058dd0a
Fix encoding for message
RebeccaMahany Oct 16, 2024
1b31d53
Add error messages
RebeccaMahany Oct 16, 2024
1d47791
Rename variable for clarity
RebeccaMahany Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ee/presencedetection/presencedetection_other.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build !darwin
// +build !darwin
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
//go:build linux
// +build linux

package presencedetection

Expand Down
305 changes: 305 additions & 0 deletions ee/presencedetection/presencedetection_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
//go:build windows
// +build windows

package presencedetection

import (
"errors"
"fmt"
"sync"
"syscall"
"time"
"unsafe"

ole "github.com/go-ole/go-ole"
directionless marked this conversation as resolved.
Show resolved Hide resolved
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/foundation"
)

// GUIDs retrieved from:
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/um/UserConsentVerifierInterop.idl
var (
iUserConsentVerifierStaticsGuid = ole.NewGUID("AF4F3F91-564C-4DDC-B8B5-973447627C65")
iUserConsentVerifierInteropGuid = ole.NewGUID("39E050C3-4E74-441A-8DC0-B81104DF949C")
)

// Signatures were generated following the guidance in
// https://learn.microsoft.com/en-us/uwp/winrt-cref/winrt-type-system#guid-generation-for-parameterized-types.
// The GUIDs themselves came from the same source as above (windows.security.credentials.idl).
// The GUIDs must be lowercase in the parameterized types.
const (
userConsentVerificationResultSignature = "enum(Windows.Security.Credentials.UI.UserConsentVerificationResult;i4)" // i4 is underlying type of int32
)

// UserConsentVerifier is defined here, with references to IUserConsentVerifierInterop below:
// https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverifier?view=winrt-26100#desktop-apps-using-cwinrt
type IUserConsentVerifierInterop struct {
ole.IInspectable
}

func (v *IUserConsentVerifierInterop) VTable() *IUserConsentVerifierInteropVTable {
return (*IUserConsentVerifierInteropVTable)(unsafe.Pointer(v.RawVTable))
}

type IUserConsentVerifierInteropVTable struct {
ole.IInspectableVtbl
RequestVerificationForWindowAsync uintptr
}

// See: https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
type WNDCLASSEX struct {
cbSize uint32 // Size of struct
style uint32
lpfnWndProc uintptr // Pointer to the Windows procedure
cbClsExtra int32 // The number of extra bytes to allocate following the window-class structure
cbWndExtra int32 // The number of extra bytes to allocate following the window instance
hInstance syscall.Handle // Handle for the process that will create the window (i.e. launcher.exe)
hIcon syscall.Handle // Null for default icon
hCursor syscall.Handle // Handle for cursor resource
hbrBackground syscall.Handle // Color value must be one of the standard system colors with 1 added
lpszMenuName *uint16 // Null if no menu
lpszClassName *uint16 // Identifies this class
hIconSm syscall.Handle // Handle to small icon, can also be null
}

const (
COLOR_WINDOW = 5

// https://learn.microsoft.com/en-us/windows/win32/winmsg/window-class-styles
CS_SAVEBITS = 0x0800
CS_DROPSHADOW = 0x00020000

// https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
WS_EX_WINDOWEDGE = 0x00000100
WS_EX_CLIENTEDGE = 0x00000200
WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE

// overlappedWindow := 0 | 0x800000 | 0x400000 | 0x80000 | 0x40000 | 0x20000 | 0x10000
CW_USEDEFAULT = 0x80000000
WS_OVERLAPPED = 0x00000000
WS_CAPTION = 0x00C00000
WS_SYSMENU = 0x00080000
WS_THICKFRAME = 0x00040000
WS_MINIMIZEBOX = 0x20000000
WS_MAXIMIZEBOX = 0x01000000
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX

cWM_DESTROY = 0x0002
cWM_CLOSE = 0x0010
)

var roInitialize = sync.OnceFunc(func() {
ole.RoInitialize(1)
})

// Detect prompts the user via Hello.
func Detect(reason string) (bool, error) {
roInitialize()

if err := requestVerification(reason); err != nil {
return false, fmt.Errorf("requesting verification: %w", err)
}

return true, nil
}

// requestVerification calls Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync.
// See: https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverifier.requestverificationasync?view=winrt-26100
func requestVerification(reason string) error {
// Get access to UserConsentVerifier via factory
factory, err := ole.RoGetActivationFactory("Windows.Security.Credentials.UI.UserConsentVerifier", iUserConsentVerifierStaticsGuid)
if err != nil {
return fmt.Errorf("getting activation factory for UserConsentVerifier: %w", err)
}
defer factory.Release()

// Query for the interop interface, which we need to actually interact with this method
verifierObj, err := factory.QueryInterface(iUserConsentVerifierInteropGuid)
if err != nil {
return fmt.Errorf("getting UserConsentVerifier from factory: %w", err)
}
defer verifierObj.Release()
verifier := (*IUserConsentVerifierInterop)(unsafe.Pointer(verifierObj))

// Create a window
windowHwnd, err := createWindow()
if err != nil {
return fmt.Errorf("creating window: %w", err)
}

// Create hstring for "reason" message
reasonHString, err := ole.NewHString(reason)
if err != nil {
return fmt.Errorf("creating reason hstring: %w", err)
}
defer ole.DeleteHString(reasonHString)

// https://learn.microsoft.com/en-us/windows/win32/api/userconsentverifierinterop/nf-userconsentverifierinterop-iuserconsentverifierinterop-requestverificationforwindowasync
// RequestVerificationForWindowAsync returns Windows.Foundation.IAsyncOperation<UserConsentVerificationResult>
refiid := winrt.ParameterizedInstanceGUID(foundation.GUIDIAsyncOperation, userConsentVerificationResultSignature)
var requestVerificationAsyncOperation *foundation.IAsyncOperation
requestVerificationReturn, _, _ := syscall.SyscallN(
verifier.VTable().RequestVerificationForWindowAsync,
uintptr(unsafe.Pointer(verifier)), // Reference to our interop
uintptr(windowHwnd), // HWND to our window
uintptr(unsafe.Pointer(&reasonHString)), // The message to include in the verification request
uintptr(unsafe.Pointer(ole.NewGUID(refiid))), // REFIID -- reference to the interface identifier for the return value (below)
uintptr(unsafe.Pointer(&requestVerificationAsyncOperation)), // Return value -- Windows.Foundation.IAsyncOperation<UserConsentVerificationResult>
)
if requestVerificationReturn != 0 {
return fmt.Errorf("calling RequestVerificationForWindowAsync: %w", ole.NewError(requestVerificationReturn))
}

// Wait for async operation to complete
iid := winrt.ParameterizedInstanceGUID(foundation.GUIDAsyncOperationCompletedHandler, userConsentVerificationResultSignature)
statusChan := make(chan foundation.AsyncStatus)
handler := foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
statusChan <- asyncStatus
})
defer handler.Release()
requestVerificationAsyncOperation.SetCompleted(handler)

select {
case operationStatus := <-statusChan:
if operationStatus != foundation.AsyncStatusCompleted {
return fmt.Errorf("RequestVerificationForWindowAsync operation did not complete: status %d", operationStatus)
}
case <-time.After(1 * time.Minute):
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
return errors.New("timed out waiting for RequestVerificationForWindowAsync operation to complete")
}

// Retrieve the results from the async operation
resPtr, err := requestVerificationAsyncOperation.GetResults()
if err != nil {
return fmt.Errorf("getting results of RequestVerificationForWindowAsync: %w", err)
}
if uintptr(resPtr) == 0x0 {
return errors.New("no response to RequestVerificationForWindowAsync")
}

// TODO RM -- switch for enum response
return fmt.Errorf("response to RequestVerificationForWindowAsync: %+v", resPtr)
}

// createWindow calls CreateWindowExW to create a basic window; it returns the HWND to the window.
// See: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
func createWindow() (syscall.Handle, error) {
instance, err := getInstance()
if err != nil {
return syscall.InvalidHandle, fmt.Errorf("getting instance: %w", err)
}

// TODO RM: need to close handles?

className := "launcher"
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
classHandle, err := registerClass(className, instance)
if err != nil {
return syscall.InvalidHandle, fmt.Errorf("registering class: %w", err)
}

user32 := syscall.NewLazyDLL("user32.dll")
procCreateWindowExW := user32.NewProc("CreateWindowExW")

title, err := syscall.UTF16PtrFromString("Kolide")
if err != nil {
return syscall.InvalidHandle, fmt.Errorf("creating title string: %w", err)
}

r0, _, e0 := procCreateWindowExW.Call(
uintptr(0), // DWORD dwExStyle
uintptr(classHandle), // LPCWSTR lpClassName (allegedly optional)
uintptr(unsafe.Pointer(title)), // LPCWSTR lpWindowName (optional)
uintptr(WS_OVERLAPPEDWINDOW), // DWORD dwStyle
uintptr(CW_USEDEFAULT), // int x
uintptr(CW_USEDEFAULT), // int y
uintptr(CW_USEDEFAULT), // int nWidth
uintptr(CW_USEDEFAULT), // int nHeight
uintptr(0), // HWND hWndParent (optional)
uintptr(0), // HMENU hMenu (optional)
uintptr(instance), // HINSTANCE hInstance
uintptr(0)) // LPVOID lpParam (optional)
handle := syscall.Handle(r0)
if handle == 0 {
return syscall.InvalidHandle, fmt.Errorf("calling CreateWindowExW: %v", e0)
}

return syscall.Handle(r0), nil
}

// getInstance calls GetModuleHandleW to get a HINSTANCE reference to the current launcher.exe process.
// See: https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew
func getInstance() (syscall.Handle, error) {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
procGetModuleHandleW := kernel32.NewProc("GetModuleHandleW")

r0, _, e0 := syscall.SyscallN(
procGetModuleHandleW.Addr(),
0,
)
instanceHandle := syscall.Handle(r0)
if instanceHandle == 0 {
return syscall.InvalidHandle, fmt.Errorf("could not get module handle: %v", e0)
}

return instanceHandle, nil
}

// registerClass calls RegisterClassExW to register a class with name `className` that can be used
// to create windows.
// See: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw
// Also see:
// - https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow
// - https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
// - https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw
func registerClass(className string, instance syscall.Handle) (syscall.Handle, error) {
user32 := syscall.NewLazyDLL("user32.dll")
procRegisterClassExW := user32.NewProc("RegisterClassExW")
procDestroyWindow := user32.NewProc("DestroyWindow")
procPostQuitMessage := user32.NewProc("PostQuitMessage")
procDefWindowProcW := user32.NewProc("DefWindowProcW")

classNamePtr, err := syscall.UTF16PtrFromString(className)
if err != nil {
return syscall.InvalidHandle, fmt.Errorf("creating pointer to class name: %w", err)
}

fn := func(hWnd syscall.Handle, uMsg uint32, wParam, lParam uintptr) uintptr {
switch uMsg {
case cWM_CLOSE:
procDestroyWindow.Call(uintptr(hWnd))
return 0
case cWM_DESTROY:
procPostQuitMessage.Call(uintptr(0))
return 0
default:
r0, _, _ := procDefWindowProcW.Call(
uintptr(hWnd),
uintptr(uMsg),
uintptr(wParam),

Check failure on line 278 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)

Check failure on line 278 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)
uintptr(lParam),

Check failure on line 279 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)

Check failure on line 279 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)
)
return uintptr(r0)

Check failure on line 281 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)

Check failure on line 281 in ee/presencedetection/presencedetection_windows.go

View workflow job for this annotation

GitHub Actions / lint (windows-latest)

unnecessary conversion (unconvert)
}
}

class := WNDCLASSEX{
// style: CS_SAVEBITS | CS_DROPSHADOW,
lpfnWndProc: syscall.NewCallback(fn),
hInstance: instance,
// hbrBackground: COLOR_WINDOW + 1,
lpszClassName: classNamePtr,
}
class.cbSize = uint32(unsafe.Sizeof(class))

r0, _, e0 := syscall.SyscallN(
procRegisterClassExW.Addr(),
uintptr(unsafe.Pointer(&class)),
)

classHandle := syscall.Handle(r0)
if classHandle == 0 {
return syscall.InvalidHandle, fmt.Errorf("could not get module handle: %v", e0)
}

return classHandle, nil
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/mixer/clock v0.0.0-20170901150240-b08e6b4da7ea
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa
github.com/peterbourgon/ff/v3 v3.0.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/pkg/errors v0.9.1
github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516
Expand Down Expand Up @@ -53,6 +53,7 @@ require (
github.com/kolide/goleveldb v0.0.0-20240514204455-8d30cd4d31c6
github.com/kolide/systray v1.10.5-0.20241011144003-35bc09a9664f
github.com/kolide/toast v1.0.2
github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877
github.com/shirou/gopsutil/v3 v3.23.3
github.com/spf13/pflag v1.0.5
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
Expand Down Expand Up @@ -92,13 +93,12 @@ require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logfmt/logfmt v0.4.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down Expand Up @@ -182,7 +183,6 @@ github.com/kolide/systray v1.10.5-0.20241011144003-35bc09a9664f/go.mod h1:FwK9yU
github.com/kolide/toast v1.0.2 h1:BQlIfO3wbKIEWfF0c8v4UkdhSIZYnSWaKkZl+Yarptk=
github.com/kolide/toast v1.0.2/go.mod h1:OguLiOUf57YSEuZqjfk4uP4KdT0QOblGoySOI8F1I0Y=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -231,8 +231,8 @@ github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa h1:bDsjvyU27AQG
github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa/go.mod h1:mLJRc1Go8uP32LRALGvWj2lVJ+hDYyIfxDzVa+C5Yo8=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.0.0 h1:eQzEmNahuOjQXfuegsKQTSTDbf4dNvr/eNLrmJhiH7M=
github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0=
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand All @@ -258,6 +258,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877 h1:h+mGFGCgqpe2xqFpYtXSqDg3uJ1nYugFb5VQhTHvyL4=
github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=
Expand Down
Loading