diff --git a/providers/os/config/config.go b/providers/os/config/config.go index 1b4cfbd53c..c55072b7ea 100644 --- a/providers/os/config/config.go +++ b/providers/os/config/config.go @@ -273,13 +273,19 @@ var Config = plugin.Provider{ { Long: "lun", Type: plugin.FlagType_String, - Desc: "The logical unit number of the block device that should be scanned. Do not use together with --device-name", + Desc: "The logical unit number of the block device that should be scanned. Do not use together with --device-name or --serial-number", Option: plugin.FlagOption_Hidden, }, { Long: "device-name", Type: plugin.FlagType_String, - Desc: "The target device to scan, e.g. /dev/sda. Do not use together with --lun", + Desc: "The target device to scan, e.g. /dev/sda. Supported only for Linux scanning. Do not use together with --lun or --serial-number", + Option: plugin.FlagOption_Hidden, + }, + { + Long: "serial-number", + Type: plugin.FlagType_String, + Desc: "The serial number of the block device that should be scanned. Supported only for Windows scanning. Do not use together with --device-name or --lun", Option: plugin.FlagOption_Hidden, }, { diff --git a/providers/os/connection/device/device_connection.go b/providers/os/connection/device/device_connection.go index 5f35e3a671..89c6fd9a3a 100644 --- a/providers/os/connection/device/device_connection.go +++ b/providers/os/connection/device/device_connection.go @@ -13,6 +13,8 @@ import ( "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v11/providers/os/connection/device/linux" + "go.mondoo.com/cnquery/v11/providers/os/connection/device/windows" + "go.mondoo.com/cnquery/v11/providers/os/connection/fs" "go.mondoo.com/cnquery/v11/providers/os/connection/shared" "go.mondoo.com/cnquery/v11/providers/os/detector" @@ -35,8 +37,8 @@ func getDeviceManager(conf *inventory.Config) (DeviceManager, error) { return nil, errors.New("device manager not implemented for darwin") } if runtime.GOOS == "windows" { - // shell = []string{"powershell", "-c"} - return nil, errors.New("device manager not implemented for windows") + shell = []string{"powershell", "-c"} + return windows.NewWindowsDeviceManager(shell, conf.Options) } return linux.NewLinuxDeviceManager(shell, conf.Options) } @@ -148,7 +150,7 @@ func (p *DeviceConnection) UpdateAsset(asset *inventory.Asset) { } func (p *DeviceConnection) Capabilities() shared.Capabilities { - return shared.Capability_File + return p.FileSystemConnection.Capabilities() } func (p *DeviceConnection) RunCommand(command string) (*shared.Command, error) { diff --git a/providers/os/connection/device/windows/device_manager.go b/providers/os/connection/device/windows/device_manager.go new file mode 100644 index 0000000000..f32a4261c9 --- /dev/null +++ b/providers/os/connection/device/windows/device_manager.go @@ -0,0 +1,114 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package windows + +import ( + "errors" + "strings" + + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v11/providers/os/connection/snapshot" +) + +const ( + LunOption = "lun" + SerialNumberOption = "serial-number" +) + +type WindowsDeviceManager struct { + cmdRunner *snapshot.LocalCommandRunner + opts map[string]string + // indicates if the disk we've targeted has been set to online. we use this to know if we need to put it back offline once we're done + diskSetToOnline bool + // if we've set the disk online, we need to know the index to set it back offline + diskIndex int +} + +func NewWindowsDeviceManager(shell []string, opts map[string]string) (*WindowsDeviceManager, error) { + if err := validateOpts(opts); err != nil { + return nil, err + } + return &WindowsDeviceManager{ + cmdRunner: &snapshot.LocalCommandRunner{Shell: shell}, + opts: opts, + }, nil +} + +func (d *WindowsDeviceManager) Name() string { + return "windows" +} + +func (d *WindowsDeviceManager) IdentifyMountTargets(opts map[string]string) ([]*snapshot.PartitionInfo, error) { + log.Debug().Msg("device connection> identifying mount targets") + diskDrives, err := d.IdentifyDiskDrives() + if err != nil { + return nil, err + } + + targetDrive, err := filterDiskDrives(diskDrives, opts) + if err != nil { + return nil, err + } + + diskOnline, err := d.identifyDiskOnline(targetDrive.Index) + if err != nil { + return nil, err + } + if diskOnline.IsOffline { + err = d.setDiskOnlineState(targetDrive.Index, true) + if err != nil { + return nil, err + } + d.diskSetToOnline = true + d.diskIndex = targetDrive.Index + } + partitions, err := d.identifyPartitions(targetDrive.Index) + if err != nil { + return nil, err + } + partition, err := filterPartitions(partitions) + if err != nil { + return nil, err + } + partitionInfo := &snapshot.PartitionInfo{ + Name: partition.DriveLetter, + FsType: "Windows", + } + return []*snapshot.PartitionInfo{partitionInfo}, nil +} + +// validates the options provided to the device manager +func validateOpts(opts map[string]string) error { + lun := opts[LunOption] + serialNumber := opts[SerialNumberOption] + + if lun != "" && serialNumber != "" { + return errors.New("lun and serial-number are mutually exclusive options") + } + + if lun == "" && serialNumber == "" { + return errors.New("either lun or serial-number must be provided") + } + + return nil +} + +func (d *WindowsDeviceManager) Mount(pi *snapshot.PartitionInfo) (string, error) { + // note: we do not (yet) do the mounting in windows. for now, we simply return the drive letter + // as that means the drive is already mounted + if strings.HasSuffix(pi.Name, ":") { + return pi.Name, nil + } + return pi.Name + ":", nil +} + +func (d *WindowsDeviceManager) UnmountAndClose() { + log.Debug().Msg("closing windows device manager") + if d.diskSetToOnline { + err := d.setDiskOnlineState(d.diskIndex, false) + if err != nil { + log.Debug().Err(err).Msg("could not set disk offline") + } + } +} diff --git a/providers/os/connection/device/windows/device_manager_test.go b/providers/os/connection/device/windows/device_manager_test.go new file mode 100644 index 0000000000..e61a151276 --- /dev/null +++ b/providers/os/connection/device/windows/device_manager_test.go @@ -0,0 +1,43 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package windows + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateOpts(t *testing.T) { + t.Run("valid (only LUN)", func(t *testing.T) { + opts := map[string]string{ + LunOption: "0", + } + err := validateOpts(opts) + require.NoError(t, err) + }) + + t.Run("valid (only serial number)", func(t *testing.T) { + opts := map[string]string{ + SerialNumberOption: "0", + } + err := validateOpts(opts) + require.NoError(t, err) + }) + + t.Run("invalid (both LUN and serial number are provided", func(t *testing.T) { + opts := map[string]string{ + SerialNumberOption: "1234", + LunOption: "1", + } + err := validateOpts(opts) + require.Error(t, err) + }) + + t.Run("invalid (neither LUN nor serial number are provided", func(t *testing.T) { + opts := map[string]string{} + err := validateOpts(opts) + require.Error(t, err) + }) +} diff --git a/providers/os/connection/device/windows/disk_drive.go b/providers/os/connection/device/windows/disk_drive.go new file mode 100644 index 0000000000..aa2b1a48f2 --- /dev/null +++ b/providers/os/connection/device/windows/disk_drive.go @@ -0,0 +1,198 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package windows + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "slices" + "strconv" + + "github.com/rs/zerolog/log" + + "go.mondoo.com/cnquery/v11/providers/os/resources/powershell" +) + +const ( + identifyDiskDrivesPwshScript = `Get-WmiObject -Class Win32_DiskDrive | Select-Object Name, SCSILogicalUnit, Index, SerialNumber | ConvertTo-Json` + identifyDiskOnlinePwshScript = `Get-Disk -Number %d | Select-Object Number, IsOffline | ConvertTo-Json` + identifyPartitionPwshScript = `Get-Disk -Number %d | Get-Partition | Select DriveLetter, Size, Type | ConvertTo-Json` + setDiskOnlinePwshScript = `Set-Disk -Number %d -IsOffline %s` +) + +type diskDrive struct { + Name string `json:"Name"` + SCSILogicalUnit int `json:"SCSILogicalUnit"` + Index int `json:"Index"` + SerialNumber string `json:"SerialNumber"` +} + +type diskOnlineStatus struct { + Number int `json:"Number"` + IsOffline bool `json:"IsOffline"` +} + +type diskPartition struct { + DriveLetter string `json:"DriveLetter"` + Size uint64 `json:"Size"` + Type string `json:"Type"` +} + +func (d *WindowsDeviceManager) setDiskOnlineState(diskNumber int, online bool) error { + str := "$true" + if online { + str = "$false" + } + log.Debug().Int("diskNumber", diskNumber).Bool("online", online).Msg("setting disk online state") + script := fmt.Sprintf(setDiskOnlinePwshScript, diskNumber, str) + cmd, err := d.cmdRunner.RunCommand(powershell.Encode(script)) + if err != nil { + return err + } + if cmd.ExitStatus != 0 { + outErr, err := io.ReadAll(cmd.Stderr) + if err != nil { + return err + } + return fmt.Errorf("failed to run powershell script: %s", outErr) + } + + return nil +} + +func (d *WindowsDeviceManager) identifyDiskOnline(diskNumber int) (*diskOnlineStatus, error) { + cmd, err := d.cmdRunner.RunCommand(powershell.Encode(fmt.Sprintf(identifyDiskOnlinePwshScript, diskNumber))) + if err != nil { + return nil, err + } + if cmd.ExitStatus != 0 { + outErr, err := io.ReadAll(cmd.Stderr) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("failed to run powershell script: %s", outErr) + } + + stdout, err := io.ReadAll(cmd.Stdout) + if err != nil { + return nil, err + } + + var status *diskOnlineStatus + err = json.Unmarshal(stdout, &status) + if err != nil { + return nil, err + } + + return status, nil +} + +func (d *WindowsDeviceManager) IdentifyDiskDrives() ([]*diskDrive, error) { + cmd, err := d.cmdRunner.RunCommand(powershell.Encode(identifyDiskDrivesPwshScript)) + if err != nil { + return nil, err + } + + if cmd.ExitStatus != 0 { + outErr, err := io.ReadAll(cmd.Stderr) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("failed to run powershell script: %s", outErr) + } + + stdout, err := io.ReadAll(cmd.Stdout) + if err != nil { + return nil, err + } + + var drives []*diskDrive + err = json.Unmarshal(stdout, &drives) + if err != nil { + return nil, err + } + + return drives, nil +} + +func (d *WindowsDeviceManager) identifyPartitions(diskNumber int) ([]*diskPartition, error) { + script := fmt.Sprintf(identifyPartitionPwshScript, diskNumber) + cmd, err := d.cmdRunner.RunCommand(powershell.Encode(script)) + if err != nil { + return nil, err + } + + if cmd.ExitStatus != 0 { + outErr, err := io.ReadAll(cmd.Stderr) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("failed to run powershell script: %s", outErr) + } + + stdout, err := io.ReadAll(cmd.Stdout) + if err != nil { + return nil, err + } + + var partitions []*diskPartition + err = json.Unmarshal(stdout, &partitions) + if err != nil { + // fallback, if only one partition is found, the output is not an array + var partition *diskPartition + err = json.Unmarshal(stdout, &partition) + if err != nil { + return nil, err + } + return []*diskPartition{partition}, nil + } + + return partitions, nil +} + +func filterDiskDrives(drives []*diskDrive, opts map[string]string) (*diskDrive, error) { + serialNumber := opts[SerialNumberOption] + lun := opts[LunOption] + if serialNumber != "" { + return filterDiskDrivesBySerialNumber(drives, serialNumber) + } + + lunInt, err := strconv.Atoi(lun) + if err != nil { + return nil, err + } + return filterDiskDrivesByLun(drives, lunInt) +} + +func filterDiskDrivesBySerialNumber(drives []*diskDrive, serialNumber string) (*diskDrive, error) { + for _, d := range drives { + if serialNumber == d.SerialNumber { + log.Debug().Str("serialNumber", serialNumber).Str("name", d.Name).Int("index", d.Index).Msg("found disk drive with matching serial number") + return d, nil + } + } + return nil, errors.New("no disk drive with matching serial number found") +} + +func filterDiskDrivesByLun(drives []*diskDrive, lun int) (*diskDrive, error) { + for _, d := range drives { + if lun == d.SCSILogicalUnit { + log.Debug().Int("lun", lun).Str("name", d.Name).Int("index", d.Index).Msg("found disk drive with matching LUN") + return d, nil + } + } + return nil, errors.New("no disk drive with matching LUN found") +} + +func filterPartitions(partitions []*diskPartition) (*diskPartition, error) { + allowed := []string{"Basic", "Windows", "IFS"} + for _, p := range partitions { + if slices.Contains(allowed, p.Type) && p.DriveLetter != "" { + return p, nil + } + } + return nil, errors.New("no basic partition with assigned drive letter found") +} diff --git a/providers/os/connection/device/windows/disk_drive_test.go b/providers/os/connection/device/windows/disk_drive_test.go new file mode 100644 index 0000000000..9cf3d9418b --- /dev/null +++ b/providers/os/connection/device/windows/disk_drive_test.go @@ -0,0 +1,152 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package windows + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFilterDrives(t *testing.T) { + t.Run("filter by serial number", func(t *testing.T) { + opts := map[string]string{ + SerialNumberOption: "1234", + } + drives := []*diskDrive{ + { + SerialNumber: "1234", + SCSILogicalUnit: 0, + Index: 0, + Name: "a", + }, + { + SerialNumber: "5678", + SCSILogicalUnit: 1, + Index: 1, + Name: "b", + }, + } + filtered, err := filterDiskDrives(drives, opts) + require.NoError(t, err) + expected := &diskDrive{ + SerialNumber: "1234", + SCSILogicalUnit: 0, + Index: 0, + Name: "a", + } + require.Equal(t, expected, filtered) + }) + + t.Run("filter by LUN", func(t *testing.T) { + opts := map[string]string{ + LunOption: "1", + } + drives := []*diskDrive{ + { + SerialNumber: "1234", + SCSILogicalUnit: 0, + Index: 0, + Name: "a", + }, + { + SerialNumber: "5678", + SCSILogicalUnit: 1, + Index: 1, + Name: "b", + }, + } + filtered, err := filterDiskDrives(drives, opts) + require.NoError(t, err) + expected := &diskDrive{ + SerialNumber: "5678", + SCSILogicalUnit: 1, + Index: 1, + Name: "b", + } + require.Equal(t, expected, filtered) + }) + t.Run("filter by invalid LUN", func(t *testing.T) { + opts := map[string]string{ + LunOption: "a", + } + drives := []*diskDrive{ + { + SerialNumber: "1234", + SCSILogicalUnit: 0, + Index: 0, + Name: "a", + }, + { + SerialNumber: "5678", + SCSILogicalUnit: 1, + Index: 1, + Name: "b", + }, + } + _, err := filterDiskDrives(drives, opts) + require.Error(t, err) + }) +} + +func TestFilterPartitions(t *testing.T) { + t.Run("find a partition (Basic type)", func(t *testing.T) { + parts := []*diskPartition{ + { + DriveLetter: "A", + Size: 123, + Type: "Basic", + }, + { + Size: 123, + Type: "Basic", + }, + } + part, err := filterPartitions(parts) + require.NoError(t, err) + expected := &diskPartition{ + DriveLetter: "A", + Size: 123, + Type: "Basic", + } + require.Equal(t, expected, part) + }) + + t.Run("find a partition (IFS type)", func(t *testing.T) { + parts := []*diskPartition{ + { + DriveLetter: "A", + Size: 123, + Type: "IFS", + }, + { + Size: 123, + Type: "Basic", + }, + } + part, err := filterPartitions(parts) + require.NoError(t, err) + expected := &diskPartition{ + DriveLetter: "A", + Size: 123, + Type: "IFS", + } + require.Equal(t, expected, part) + }) + + t.Run("no applicable partition", func(t *testing.T) { + parts := []*diskPartition{ + { + Size: 123, + Type: "IFS", + }, + { + Size: 123, + Type: "Basic", + }, + } + _, err := filterPartitions(parts) + require.Error(t, err) + }) +} diff --git a/providers/os/provider/provider.go b/providers/os/provider/provider.go index dfc51ba43a..694f5d0556 100644 --- a/providers/os/provider/provider.go +++ b/providers/os/provider/provider.go @@ -230,6 +230,10 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) if deviceName, ok := flags["device-name"]; ok { conf.Options["device-name"] = deviceName.RawData().Value.(string) } + if serialNumber, ok := flags["serial-number"]; ok { + conf.Options["serial-number"] = serialNumber.RawData().Value.(string) + } + if platformIDs, ok := flags["platform-ids"]; ok { platformIDs := platformIDs.Array strs := []string{} diff --git a/providers/os/registry/registryhandler_windows.go b/providers/os/registry/registryhandler_windows.go index b52a03e902..9713af1e0d 100644 --- a/providers/os/registry/registryhandler_windows.go +++ b/providers/os/registry/registryhandler_windows.go @@ -7,44 +7,14 @@ package registry import ( - "syscall" - "unsafe" -) - -var ( - advapi32 = syscall.NewLazyDLL("advapi32.dll") - // note: we're using the W (RegLoadKeyW and NOT RegLoadKeyA) versions of these functions to work with UTF16 strings - regLoadKey = advapi32.NewProc("RegLoadKeyW") - regUnloadKey = advapi32.NewProc("RegUnLoadKeyW") + "fmt" + "os/exec" ) func LoadRegistrySubkey(key, path string) error { - keyPtr, err := syscall.UTF16PtrFromString(key) - if err != nil { - return err - } - pathPtr, err := syscall.UTF16PtrFromString(path) - if err != nil { - return err - } - ret, _, err := regLoadKey.Call(syscall.HKEY_LOCAL_MACHINE, uintptr(unsafe.Pointer(keyPtr)), uintptr(unsafe.Pointer(pathPtr))) - // the Microsoft docs indicate that the return value is 0 on success - if ret != 0 { - return err - } - return nil + return exec.Command("cmd", "/C", "reg", "load", fmt.Sprintf(`HKEY_LOCAL_MACHINE\%s`, key), path).Run() } func UnloadRegistrySubkey(key string) error { - keyPtr, err := syscall.UTF16PtrFromString(key) - if err != nil { - return err - } - - ret, _, err := regUnloadKey.Call(syscall.HKEY_LOCAL_MACHINE, uintptr(unsafe.Pointer(keyPtr))) - // the Microsoft docs indicate that the return value is 0 on success - if ret != 0 { - return err - } - return nil + return exec.Command("cmd", "/C", "reg", "unload", fmt.Sprintf(`HKEY_LOCAL_MACHINE\%s`, key)).Run() } diff --git a/providers/os/resources/packages/windows_packages.go b/providers/os/resources/packages/windows_packages.go index 75b5de7098..ca5223bfa7 100644 --- a/providers/os/resources/packages/windows_packages.go +++ b/providers/os/resources/packages/windows_packages.go @@ -247,7 +247,7 @@ func (w *WinPkgManager) getInstalledApps() ([]Package, error) { return w.getLocalInstalledApps() } - if w.conn.Type() == shared.Type_FileSystem { + if w.conn.Type() == shared.Type_FileSystem || w.conn.Type() == shared.Type_Device { return w.getFsInstalledApps() }