diff --git a/providers/os/config/config.go b/providers/os/config/config.go index 1320a4b2a6..38b86e87fe 100644 --- a/providers/os/config/config.go +++ b/providers/os/config/config.go @@ -28,6 +28,7 @@ var Config = plugin.Provider{ shared.Type_RegistryImage.String(), shared.Type_FileSystem.String(), shared.Type_Winrm.String(), + shared.Type_Device.String(), }, Connectors: []plugin.Connector{ { @@ -261,6 +262,33 @@ var Config = plugin.Provider{ }, }, }, + { + Name: "device", + Use: "device", + Short: "a block device target", + MinArgs: 0, + MaxArgs: 0, + Flags: []plugin.Flag{ + { + 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.", + 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.", + Option: plugin.FlagOption_Hidden, + }, + { + Long: "platform-ids", + Type: plugin.FlagType_List, + Desc: "List of platform IDs to inject to the asset.", + Option: plugin.FlagOption_Hidden, + }, + }, + }, }, AssetUrlTrees: []*inventory.AssetUrlBranch{ { diff --git a/providers/os/connection/device/device_connection.go b/providers/os/connection/device/device_connection.go new file mode 100644 index 0000000000..787ebd8683 --- /dev/null +++ b/providers/os/connection/device/device_connection.go @@ -0,0 +1,151 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package device + +import ( + "errors" + "runtime" + "strings" + + "github.com/rs/zerolog/log" + "github.com/spf13/afero" + "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/fs" + "go.mondoo.com/cnquery/v11/providers/os/connection/shared" + "go.mondoo.com/cnquery/v11/providers/os/id" + "go.mondoo.com/cnquery/v11/providers/os/id/ids" +) + +const PlatformIdInject = "inject-platform-ids" + +type DeviceConnection struct { + *fs.FileSystemConnection + plugin.Connection + asset *inventory.Asset + deviceManager DeviceManager +} + +func getDeviceManager(conf *inventory.Config) (DeviceManager, error) { + shell := []string{"sh", "-c"} + if runtime.GOOS == "darwin" { + 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") + } + return linux.NewLinuxDeviceManager(shell, conf.Options) +} + +func NewDeviceConnection(connId uint32, conf *inventory.Config, asset *inventory.Asset) (*DeviceConnection, error) { + manager, err := getDeviceManager(conf) + if err != nil { + return nil, err + } + log.Debug().Str("manager", manager.Name()).Msg("device manager created") + + blocks, err := manager.IdentifyBlockDevice(conf.Options) + if err != nil { + return nil, err + } + if len(blocks) != 0 { + // FIXME: remove this when we start scanning multiple blocks + return nil, errors.New("internal>blocks size is not equal to 1.") + } + block := blocks[0] + log.Debug().Str("device", block.DeviceName).Msg("identified block for mounting") + + res := &DeviceConnection{ + Connection: plugin.NewConnection(connId, asset), + deviceManager: manager, + asset: asset, + } + + scanDir, err := manager.Mount(block) + if err != nil { + log.Error().Err(err).Msg("unable to complete mount step") + res.Close() + return nil, err + } + + conf.Options["path"] = scanDir + // create and initialize fs provider + fsConn, err := fs.NewConnection(connId, &inventory.Config{ + Path: scanDir, + PlatformId: conf.PlatformId, + Options: conf.Options, + Type: "fs", + Record: conf.Record, + }, asset) + if err != nil { + res.Close() + return nil, err + } + + res.FileSystemConnection = fsConn + + asset.IdDetector = []string{ids.IdDetector_Hostname} + fingerprint, p, err := id.IdentifyPlatform(res, asset.Platform, asset.IdDetector) + if err != nil { + res.Close() + return nil, err + } + asset.Name = fingerprint.Name + asset.PlatformIds = fingerprint.PlatformIDs + asset.IdDetector = fingerprint.ActiveIdDetectors + asset.Platform = p + asset.Id = conf.Type + + // allow injecting platform ids into the device connection. we cannot always know the asset that's being scanned, e.g. + // if we can scan an azure VM's disk we should be able to inject the platform ids of the VM + if platformIDs, ok := conf.Options[PlatformIdInject]; ok { + asset.PlatformIds = append(asset.PlatformIds, strings.Split(platformIDs, ",")...) + } + return res, nil +} + +func (c *DeviceConnection) Close() { + log.Debug().Msg("closing device connection") + if c == nil { + return + } + + if c.deviceManager != nil { + c.deviceManager.UnmountAndClose() + } +} + +func (p *DeviceConnection) Name() string { + return "device" +} + +func (p *DeviceConnection) Type() shared.ConnectionType { + return shared.Type_Device +} + +func (p *DeviceConnection) Asset() *inventory.Asset { + return p.asset +} + +func (p *DeviceConnection) UpdateAsset(asset *inventory.Asset) { + p.asset = asset +} + +func (p *DeviceConnection) Capabilities() shared.Capabilities { + return shared.Capability_File +} + +func (p *DeviceConnection) RunCommand(command string) (*shared.Command, error) { + return nil, plugin.ErrRunCommandNotImplemented +} + +func (p *DeviceConnection) FileSystem() afero.Fs { + return p.FileSystemConnection.FileSystem() +} + +func (p *DeviceConnection) FileInfo(path string) (shared.FileInfoDetails, error) { + return p.FileSystemConnection.FileInfo(path) +} diff --git a/providers/os/connection/device/device_manager.go b/providers/os/connection/device/device_manager.go new file mode 100644 index 0000000000..da8b214d79 --- /dev/null +++ b/providers/os/connection/device/device_manager.go @@ -0,0 +1,13 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package device + +import "go.mondoo.com/cnquery/v11/providers/os/connection/device/shared" + +type DeviceManager interface { + Name() string + IdentifyBlockDevice(opts map[string]string) ([]shared.MountInfo, error) + Mount(mi shared.MountInfo) (string, error) + UnmountAndClose() +} diff --git a/providers/os/connection/device/linux/device_manager.go b/providers/os/connection/device/linux/device_manager.go new file mode 100644 index 0000000000..a3f5b83c4a --- /dev/null +++ b/providers/os/connection/device/linux/device_manager.go @@ -0,0 +1,146 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package linux + +import ( + "strconv" + + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v11/providers/os/connection/device/shared" + "go.mondoo.com/cnquery/v11/providers/os/connection/snapshot" +) + +const ( + LunOption = "lun" + DeviceName = "device-name" +) + +type LinuxDeviceManager struct { + volumeMounter *snapshot.VolumeMounter + opts map[string]string +} + +func NewLinuxDeviceManager(shell []string, opts map[string]string) (*LinuxDeviceManager, error) { + if err := validateOpts(opts); err != nil { + return nil, err + } + + return &LinuxDeviceManager{ + volumeMounter: snapshot.NewVolumeMounter(shell), + opts: opts, + }, nil +} + +func (d *LinuxDeviceManager) Name() string { + return "linux" +} + +func (d *LinuxDeviceManager) IdentifyBlockDevice(opts map[string]string) ([]shared.MountInfo, error) { + if err := validateOpts(opts); err != nil { + return nil, err + } + if opts[LunOption] != "" { + lun, err := strconv.Atoi(opts[LunOption]) + if err != nil { + return nil, err + } + return d.identifyViaLun(lun) + } + + return d.identifyViaDeviceName(opts[DeviceName]) +} + +func (d *LinuxDeviceManager) Mount(mi shared.MountInfo) (string, error) { + // TODO: we should make the volume mounter return the scan dir from Mount() + // TODO: use the mount info to mount the volume + err := d.volumeMounter.Mount() + if err != nil { + return "", err + } + return d.volumeMounter.ScanDir, nil +} + +func (d *LinuxDeviceManager) UnmountAndClose() { + log.Debug().Msg("closing linux device manager") + if d == nil { + return + } + + if d.volumeMounter != nil { + err := d.volumeMounter.UnmountVolumeFromInstance() + if err != nil { + log.Error().Err(err).Msg("unable to unmount volume") + } + err = d.volumeMounter.RemoveTempScanDir() + if err != nil { + log.Error().Err(err).Msg("unable to remove dir") + } + } +} + +// validates the options provided to the device manager +// we cannot have both LUN and device name provided, those are mutually exclusive +func validateOpts(opts map[string]string) error { + lun := opts[LunOption] + deviceName := opts[DeviceName] + if lun != "" && deviceName != "" { + return errors.New("both lun and device name provided") + } + + return nil +} + +func (c *LinuxDeviceManager) identifyViaLun(lun int) ([]shared.MountInfo, error) { + scsiDevices, err := c.listScsiDevices() + if err != nil { + return nil, err + } + + // only interested in the scsi devices that match the provided LUN + filteredScsiDevices := filterScsiDevices(scsiDevices, lun) + if len(filteredScsiDevices) == 0 { + return nil, errors.New("no matching scsi devices found") + } + + // if we have exactly one device present at the LUN we can directly point the volume mounter towards it + if len(filteredScsiDevices) == 1 { + return []shared.MountInfo{{DeviceName: filteredScsiDevices[0].VolumePath}}, nil + } + + // we have multiple devices at the same LUN. we find the first non-mounted block devices in that list + blockDevices, err := c.volumeMounter.CmdRunner.GetBlockDevices() + if err != nil { + return nil, err + } + target, err := findMatchingDeviceByBlock(filteredScsiDevices, blockDevices) + if err != nil { + return nil, err + } + c.volumeMounter.VolumeAttachmentLoc = target + return []shared.MountInfo{{DeviceName: target}}, nil +} + +func (c *LinuxDeviceManager) identifyViaDeviceName(deviceName string) ([]shared.MountInfo, error) { + blockDevices, err := c.volumeMounter.CmdRunner.GetBlockDevices() + if err != nil { + return nil, err + } + // this is a best-effort approach, we try to find the first unmounted block device as we don't have the device name + if deviceName == "" { + fsInfo, err := blockDevices.GetUnmountedBlockEntry() + if err != nil { + return nil, err + } + c.volumeMounter.VolumeAttachmentLoc = deviceName + return []shared.MountInfo{{DeviceName: fsInfo.Name}}, nil + } + + fsInfo, err := blockDevices.GetBlockEntryByName(deviceName) + if err != nil { + return nil, err + } + c.volumeMounter.VolumeAttachmentLoc = deviceName + return []shared.MountInfo{{DeviceName: fsInfo.Name}}, nil +} diff --git a/providers/os/connection/device/linux/lun.go b/providers/os/connection/device/linux/lun.go new file mode 100644 index 0000000000..c77092c5a8 --- /dev/null +++ b/providers/os/connection/device/linux/lun.go @@ -0,0 +1,123 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package linux + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v11/providers/os/connection/snapshot" +) + +type scsiDeviceInfo struct { + // the LUN, e.g. 3 + Lun int + // where the disk is mounted, e.g. /dev/sda + VolumePath string +} + +type scsiDevices = []scsiDeviceInfo + +func (c *LinuxDeviceManager) listScsiDevices() ([]scsiDeviceInfo, error) { + cmd, err := c.volumeMounter.CmdRunner.RunCommand("lsscsi --brief") + 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 list logical unit numbers: %s", outErr) + } + data, err := io.ReadAll(cmd.Stdout) + if err != nil { + return nil, err + } + output := string(data) + return parseLsscsiOutput(output) +} + +func filterScsiDevices(scsiDevices scsiDevices, lun int) []scsiDeviceInfo { + matching := []scsiDeviceInfo{} + for _, d := range scsiDevices { + if d.Lun == lun { + matching = append(matching, d) + } + } + + return matching +} + +// there can be multiple devices mounted at the same LUN. +// the LUN so we need to find all the blocks, mounted at that LUN. then we find the first one +// that has no mounted partitions and use that as the target device. this is a best-effort approach +func findMatchingDeviceByBlock(scsiDevices scsiDevices, blockDevices *snapshot.BlockDevices) (string, error) { + matchingBlocks := []snapshot.BlockDevice{} + for _, device := range scsiDevices { + for _, block := range blockDevices.BlockDevices { + devName := "/dev/" + block.Name + if devName == device.VolumePath { + matchingBlocks = append(matchingBlocks, block) + } + } + } + + if len(matchingBlocks) == 0 { + return "", errors.New("no matching blocks found") + } + + var target string + for _, b := range matchingBlocks { + log.Debug().Str("name", b.Name).Msg("device connection> checking block") + mounted := false + for _, ch := range b.Children { + if len(ch.MountPoint) > 0 && ch.MountPoint != "" { + log.Debug().Str("name", ch.Name).Msg("device connection> has mounted partitons, skipping") + mounted = true + } + if !mounted { + target = "/dev/" + b.Name + } + } + } + + return target, nil +} + +// parses the output from running 'lsscsi --brief' and gets the device info, the output looks like this: +// [0:0:0:0] /dev/sda +// [1:0:0:0] /dev/sdb +func parseLsscsiOutput(output string) (scsiDevices, error) { + lines := strings.Split(strings.TrimSpace(output), "\n") + mountedDevices := []scsiDeviceInfo{} + for _, line := range lines { + log.Debug().Str("line", line).Msg("device connection> parsing lsscsi output") + if line == "" { + continue + } + parts := strings.Fields(strings.TrimSpace(line)) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid lsscsi output: %s", line) + } + lunInfo := parts[0] + path := parts[1] + // trim the [], turning [1:0:0:0] into 1:0:0:0 + trimLun := strings.Trim(lunInfo, "[]") + splitLun := strings.Split(trimLun, ":") + // the LUN is the last one + lun := splitLun[len(splitLun)-1] + lunInt, err := strconv.Atoi(lun) + if err != nil { + return nil, err + } + mountedDevices = append(mountedDevices, scsiDeviceInfo{Lun: lunInt, VolumePath: path}) + } + + return mountedDevices, nil +} diff --git a/providers/os/connection/device/linux/lun_test.go b/providers/os/connection/device/linux/lun_test.go new file mode 100644 index 0000000000..5e7f6cd564 --- /dev/null +++ b/providers/os/connection/device/linux/lun_test.go @@ -0,0 +1,139 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.mondoo.com/cnquery/v11/providers/os/connection/snapshot" +) + +func TestParseLsscsiOutput(t *testing.T) { + // different padding for the device names on purpose + an extra blank line + output := ` + [0:0:0:0] /dev/sda + [0:0:1:1] /dev/sdb + [0:0:1:2] /dev/sdc + [0:0:0:3] /dev/sdd + + ` + devices, err := parseLsscsiOutput(output) + assert.NoError(t, err) + assert.Len(t, devices, 4) + expected := scsiDevices{ + {Lun: 0, VolumePath: "/dev/sda"}, + {Lun: 1, VolumePath: "/dev/sdb"}, + {Lun: 2, VolumePath: "/dev/sdc"}, + {Lun: 3, VolumePath: "/dev/sdd"}, + } + assert.ElementsMatch(t, expected, devices) +} + +func TestFilterScsiDevices(t *testing.T) { + devices := scsiDevices{ + {Lun: 0, VolumePath: "/dev/sda"}, + {Lun: 1, VolumePath: "/dev/sdb"}, + {Lun: 2, VolumePath: "/dev/sdc"}, + {Lun: 3, VolumePath: "/dev/sdd"}, + } + + filtered := filterScsiDevices(devices, 1) + expected := scsiDevices{ + {Lun: 1, VolumePath: "/dev/sdb"}, + } + assert.ElementsMatch(t, expected, filtered) + + filtered = filterScsiDevices(devices, 4) + assert.Len(t, filtered, 0) +} + +func TestFindDeviceByBlock(t *testing.T) { + devices := scsiDevices{ + {Lun: 0, VolumePath: "/dev/sda"}, + {Lun: 0, VolumePath: "/dev/sdb"}, + } + t.Run("find device by block", func(t *testing.T) { + blockDevices := &snapshot.BlockDevices{ + BlockDevices: []snapshot.BlockDevice{ + { + Name: "sda", + Children: []snapshot.BlockDevice{ + { + Name: "sda1", + MountPoint: "/", + }, + }, + }, + { + Name: "sdb", + Children: []snapshot.BlockDevice{ + { + Name: "sdb1", + MountPoint: "", + }, + }, + }, + }, + } + target, err := findMatchingDeviceByBlock(devices, blockDevices) + assert.NoError(t, err) + expected := "/dev/sdb" + assert.Equal(t, expected, target) + }) + + t.Run("no matches", func(t *testing.T) { + blockDevices := &snapshot.BlockDevices{ + BlockDevices: []snapshot.BlockDevice{ + { + Name: "sdc", + Children: []snapshot.BlockDevice{ + { + Name: "sdc1", + MountPoint: "/", + }, + }, + }, + { + Name: "sdc", + Children: []snapshot.BlockDevice{ + { + Name: "sdc1", + MountPoint: "/tmp", + }, + }, + }, + }, + } + _, err := findMatchingDeviceByBlock(devices, blockDevices) + assert.Error(t, err) + }) + t.Run("empty target as all blocks are mounted", func(t *testing.T) { + blockDevices := &snapshot.BlockDevices{ + BlockDevices: []snapshot.BlockDevice{ + { + Name: "sda", + Children: []snapshot.BlockDevice{ + { + Name: "sda1", + MountPoint: "/", + }, + }, + }, + { + Name: "sdb", + Children: []snapshot.BlockDevice{ + { + Name: "sdb1", + MountPoint: "/tmp", + }, + }, + }, + }, + } + target, err := findMatchingDeviceByBlock(devices, blockDevices) + assert.NoError(t, err) + assert.Empty(t, target) + }) +} diff --git a/providers/os/connection/device/shared/mountinfo.go b/providers/os/connection/device/shared/mountinfo.go new file mode 100644 index 0000000000..2ade0714a2 --- /dev/null +++ b/providers/os/connection/device/shared/mountinfo.go @@ -0,0 +1,8 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package shared + +type MountInfo struct { + DeviceName string +} diff --git a/providers/os/connection/shared/shared.go b/providers/os/connection/shared/shared.go index d8f3f7aa08..7729cc4c6c 100644 --- a/providers/os/connection/shared/shared.go +++ b/providers/os/connection/shared/shared.go @@ -42,6 +42,7 @@ const ( Type_DockerSnapshot ConnectionType = "docker-snapshot" Type_ContainerRegistry ConnectionType = "container-registry" Type_RegistryImage ConnectionType = "registry-image" + Type_Device ConnectionType = "device" ContainerProxyOption string = "container-proxy" ) diff --git a/providers/os/connection/snapshot/blockdevices.go b/providers/os/connection/snapshot/blockdevices.go index 7fbdb0a56a..f7810d8511 100644 --- a/providers/os/connection/snapshot/blockdevices.go +++ b/providers/os/connection/snapshot/blockdevices.go @@ -4,32 +4,58 @@ package snapshot import ( + "encoding/json" "errors" + "fmt" + "io" "strings" "github.com/rs/zerolog/log" ) -type blockDevices struct { - BlockDevices []blockDevice `json:"blockDevices,omitempty"` +type BlockDevices struct { + BlockDevices []BlockDevice `json:"blockDevices,omitempty"` } -type blockDevice struct { +type BlockDevice struct { Name string `json:"name,omitempty"` - FsType string `json:"fstype,omitempty"` + FsType string `json:"FsType,omitempty"` Label string `json:"label,omitempty"` Uuid string `json:"uuid,omitempty"` MountPoint string `json:"mountpoint,omitempty"` - Children []blockDevice `json:"children,omitempty"` + Children []BlockDevice `json:"children,omitempty"` FsUse string `json:"fsuse%,omitempty"` } type fsInfo struct { - name string - fstype string + Name string + FsType string } -func (blockEntries blockDevices) GetRootBlockEntry() (*fsInfo, error) { +func (cmdRunner *LocalCommandRunner) GetBlockDevices() (*BlockDevices, error) { + cmd, err := cmdRunner.RunCommand("lsblk -f --json") + 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 lsblk: %s", outErr) + } + data, err := io.ReadAll(cmd.Stdout) + if err != nil { + return nil, err + } + blockEntries := &BlockDevices{} + if err := json.Unmarshal(data, blockEntries); err != nil { + return nil, err + } + return blockEntries, nil +} + +func (blockEntries BlockDevices) GetRootBlockEntry() (*fsInfo, error) { log.Debug().Msg("get root block entry") for i := range blockEntries.BlockDevices { d := blockEntries.BlockDevices[i] @@ -38,14 +64,14 @@ func (blockEntries blockDevices) GetRootBlockEntry() (*fsInfo, error) { entry := d.Children[i] if entry.IsNoBootVolume() { devFsName := "/dev/" + entry.Name - return &fsInfo{name: devFsName, fstype: entry.FsType}, nil + return &fsInfo{Name: devFsName, FsType: entry.FsType}, nil } } } return nil, errors.New("target volume not found on instance") } -func (blockEntries blockDevices) GetBlockEntryByName(name string) (*fsInfo, error) { +func (blockEntries BlockDevices) GetBlockEntryByName(name string) (*fsInfo, error) { log.Debug().Str("name", name).Msg("get matching block entry") var secondName string if strings.HasPrefix(name, "/dev/sd") { @@ -70,14 +96,14 @@ func (blockEntries blockDevices) GetBlockEntryByName(name string) (*fsInfo, erro entry := d.Children[i] if entry.IsNotBootOrRootVolumeAndUnmounted() { devFsName := "/dev/" + entry.Name - return &fsInfo{name: devFsName, fstype: entry.FsType}, nil + return &fsInfo{Name: devFsName, FsType: entry.FsType}, nil } } } return nil, errors.New("target volume not found on instance") } -func (blockEntries blockDevices) GetUnnamedBlockEntry() (*fsInfo, error) { +func (blockEntries BlockDevices) GetUnnamedBlockEntry() (*fsInfo, error) { fsInfo, err := blockEntries.GetUnmountedBlockEntry() if err == nil && fsInfo != nil { return fsInfo, nil @@ -93,7 +119,7 @@ func (blockEntries blockDevices) GetUnnamedBlockEntry() (*fsInfo, error) { return nil, errors.New("target volume not found on instance") } -func (blockEntries blockDevices) GetUnmountedBlockEntry() (*fsInfo, error) { +func (blockEntries BlockDevices) GetUnmountedBlockEntry() (*fsInfo, error) { log.Debug().Msg("get unmounted block entry") for i := range blockEntries.BlockDevices { d := blockEntries.BlockDevices[i] @@ -108,27 +134,27 @@ func (blockEntries blockDevices) GetUnmountedBlockEntry() (*fsInfo, error) { return nil, errors.New("target volume not found on instance") } -func findVolume(children []blockDevice) *fsInfo { +func findVolume(children []BlockDevice) *fsInfo { var fs *fsInfo for i := range children { entry := children[i] if entry.IsNotBootOrRootVolumeAndUnmounted() { // we are NOT searching for the root volume here, so we can exclude the "sda" and "xvda" volumes devFsName := "/dev/" + entry.Name - fs = &fsInfo{name: devFsName, fstype: entry.FsType} + fs = &fsInfo{Name: devFsName, FsType: entry.FsType} } } return fs } -func (entry blockDevice) IsNoBootVolume() bool { +func (entry BlockDevice) IsNoBootVolume() bool { return entry.Uuid != "" && entry.FsType != "" && entry.FsType != "vfat" && entry.Label != "EFI" && entry.Label != "boot" } -func (entry blockDevice) IsRootVolume() bool { +func (entry BlockDevice) IsRootVolume() bool { return strings.Contains(entry.Name, "sda") || strings.Contains(entry.Name, "xvda") || strings.Contains(entry.Name, "nvme0n1") } -func (entry blockDevice) IsNotBootOrRootVolumeAndUnmounted() bool { +func (entry BlockDevice) IsNotBootOrRootVolumeAndUnmounted() bool { return entry.IsNoBootVolume() && entry.MountPoint == "" && !entry.IsRootVolume() } diff --git a/providers/os/connection/snapshot/blockdevices_test.go b/providers/os/connection/snapshot/blockdevices_test.go index 6596569266..b649b1f487 100644 --- a/providers/os/connection/snapshot/blockdevices_test.go +++ b/providers/os/connection/snapshot/blockdevices_test.go @@ -12,197 +12,197 @@ import ( "github.com/stretchr/testify/require" ) -var RootDevice = blockDevice{Name: "sda", Children: []blockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}} +var RootDevice = BlockDevice{Name: "sda", Children: []BlockDevice{{Uuid: "1234", FsType: "xfs", Label: "ROOT", Name: "sda1", MountPoint: "/"}}} func TestGetMatchingBlockEntryByName(t *testing.T) { - blockEntries := blockDevices{BlockDevices: []blockDevice{RootDevice}} - blockEntries.BlockDevices = append(blockEntries.BlockDevices, []blockDevice{ - {Name: "nvme0n1", Children: []blockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, - {Name: "sdx", Children: []blockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + blockEntries := BlockDevices{BlockDevices: []BlockDevice{RootDevice}} + blockEntries.BlockDevices = append(blockEntries.BlockDevices, []BlockDevice{ + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "sdx", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "sdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, }...) realFsInfo, err := blockEntries.GetBlockEntryByName("/dev/sdx") require.Nil(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/sdh1"}, *realFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/sdh1"}, *realFsInfo) - blockEntries = blockDevices{BlockDevices: []blockDevice{RootDevice}} - blockEntries.BlockDevices = append(blockEntries.BlockDevices, []blockDevice{ - {Name: "nvme0n1", Children: []blockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, - {Name: "xvdx", Children: []blockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "xvdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + blockEntries = BlockDevices{BlockDevices: []BlockDevice{RootDevice}} + blockEntries.BlockDevices = append(blockEntries.BlockDevices, []BlockDevice{ + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "xvdx", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "xvdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, }...) realFsInfo, err = blockEntries.GetBlockEntryByName("/dev/sdx") require.Nil(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/xvdh1"}, *realFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/xvdh1"}, *realFsInfo) - blockEntries = blockDevices{BlockDevices: []blockDevice{RootDevice}} - blockEntries.BlockDevices = append(blockEntries.BlockDevices, []blockDevice{ - {Name: "nvme0n1", Children: []blockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, - {Name: "xvdh", Children: []blockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "xvdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + blockEntries = BlockDevices{BlockDevices: []BlockDevice{RootDevice}} + blockEntries.BlockDevices = append(blockEntries.BlockDevices, []BlockDevice{ + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + {Name: "xvdh", Children: []BlockDevice{{Uuid: "12346", FsType: "xfs", Label: "ROOT", Name: "xvdh1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, }...) realFsInfo, err = blockEntries.GetBlockEntryByName("/dev/xvdh") require.Nil(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/xvdh1"}, *realFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/xvdh1"}, *realFsInfo) - blockEntries = blockDevices{BlockDevices: []blockDevice{RootDevice}} - blockEntries.BlockDevices = append(blockEntries.BlockDevices, []blockDevice{ - {Name: "nvme0n1", Children: []blockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + blockEntries = BlockDevices{BlockDevices: []BlockDevice{RootDevice}} + blockEntries.BlockDevices = append(blockEntries.BlockDevices, []BlockDevice{ + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, }...) realFsInfo, err = blockEntries.GetBlockEntryByName("/dev/sdh") require.Error(t, err) - blockEntries = blockDevices{BlockDevices: []blockDevice{RootDevice}} + blockEntries = BlockDevices{BlockDevices: []BlockDevice{RootDevice}} realFsInfo, err = blockEntries.GetBlockEntryByName("/dev/sdh") require.Error(t, err) } func TestGetNonRootBlockEntry(t *testing.T) { - blockEntries := blockDevices{BlockDevices: []blockDevice{RootDevice}} - blockEntries.BlockDevices = append(blockEntries.BlockDevices, []blockDevice{ - {Name: "nvme0n1", Children: []blockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, + blockEntries := BlockDevices{BlockDevices: []BlockDevice{RootDevice}} + blockEntries.BlockDevices = append(blockEntries.BlockDevices, []BlockDevice{ + {Name: "nvme0n1", Children: []BlockDevice{{Uuid: "12345", FsType: "xfs", Label: "ROOT", Name: "nvmd1n1"}, {Uuid: "12345", FsType: "", Label: "EFI"}}}, }...) realFsInfo, err := blockEntries.GetUnmountedBlockEntry() require.Nil(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/nvmd1n1"}, *realFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/nvmd1n1"}, *realFsInfo) } func TestGetRootBlockEntry(t *testing.T) { - blockEntries := blockDevices{BlockDevices: []blockDevice{RootDevice}} + blockEntries := BlockDevices{BlockDevices: []BlockDevice{RootDevice}} realFsInfo, err := blockEntries.GetRootBlockEntry() require.Nil(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/sda1"}, *realFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/sda1"}, *realFsInfo) } func TestGetRootBlockEntryRhel8(t *testing.T) { data, err := os.ReadFile("./testdata/rhel8.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) rootFsInfo, err := blockEntries.GetRootBlockEntry() require.NoError(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/sda2"}, *rootFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/sda2"}, *rootFsInfo) rootFsInfo, err = blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/sdc2"}, *rootFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/sdc2"}, *rootFsInfo) } func TestGetRootBlockEntryRhelNoLabels(t *testing.T) { data, err := os.ReadFile("./testdata/rhel8_nolabels.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) rootFsInfo, err := blockEntries.GetRootBlockEntry() require.NoError(t, err) - require.Equal(t, fsInfo{fstype: "xfs", name: "/dev/sda2"}, *rootFsInfo) + require.Equal(t, fsInfo{FsType: "xfs", Name: "/dev/sda2"}, *rootFsInfo) rootFsInfo, err = blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, fsInfo{fstype: "ext4", name: "/dev/sdb1"}, *rootFsInfo) + require.Equal(t, fsInfo{FsType: "ext4", Name: "/dev/sdb1"}, *rootFsInfo) } func TestAttachedBlockEntry(t *testing.T) { data, err := os.ReadFile("./testdata/alma_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "xvdh")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "xvdh")) } func TestAttachedBlockEntryAWS(t *testing.T) { data, err := os.ReadFile("./testdata/aws_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "xvdh")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "xvdh")) } func TestAnotherAttachedBlockEntryAlma(t *testing.T) { data, err := os.ReadFile("./testdata/another_alma_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "nvme1n1")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "nvme1n1")) } func TestAttachedBlockEntryOracle(t *testing.T) { data, err := os.ReadFile("./testdata/oracle_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "ext4", info.fstype) - require.True(t, strings.Contains(info.name, "xvdb")) + require.Equal(t, "ext4", info.FsType) + require.True(t, strings.Contains(info.Name, "xvdb")) } func TestAttachedBlockEntryRhel(t *testing.T) { data, err := os.ReadFile("./testdata/rhel_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "nvme1n1")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "nvme1n1")) } func TestAttachedBlockEntryMultipleMatch(t *testing.T) { data, err := os.ReadFile("./testdata/alma9_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "xvdh4")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "xvdh4")) } func TestAttachedBlockEntryFedora(t *testing.T) { data, err := os.ReadFile("./testdata/fedora_attached.json") require.NoError(t, err) - blockEntries := blockDevices{} + blockEntries := BlockDevices{} err = json.Unmarshal(data, &blockEntries) require.NoError(t, err) info, err := blockEntries.GetUnnamedBlockEntry() require.NoError(t, err) - require.Equal(t, "xfs", info.fstype) - require.True(t, strings.Contains(info.name, "xvdh4")) + require.Equal(t, "xfs", info.FsType) + require.True(t, strings.Contains(info.Name, "xvdh4")) } diff --git a/providers/os/connection/snapshot/localcmd.go b/providers/os/connection/snapshot/localcmd.go index 06d277747c..11d64b4677 100644 --- a/providers/os/connection/snapshot/localcmd.go +++ b/providers/os/connection/snapshot/localcmd.go @@ -9,11 +9,11 @@ import ( ) type LocalCommandRunner struct { - shell []string + Shell []string } func (r *LocalCommandRunner) RunCommand(command string) (*shared.Command, error) { - c := local.CommandRunner{Shell: r.shell} + c := local.CommandRunner{Shell: r.Shell} args := []string{} res, err := c.Exec(command, args) diff --git a/providers/os/connection/snapshot/volumemounter.go b/providers/os/connection/snapshot/volumemounter.go index 0ab58a2582..8a3c82cb5d 100644 --- a/providers/os/connection/snapshot/volumemounter.go +++ b/providers/os/connection/snapshot/volumemounter.go @@ -26,12 +26,12 @@ type VolumeMounter struct { // where we tell AWS to attach the volume; it doesn't necessarily get attached there, but we have to reference this same location when detaching VolumeAttachmentLoc string opts map[string]string - cmdRunner *LocalCommandRunner + CmdRunner *LocalCommandRunner } func NewVolumeMounter(shell []string) *VolumeMounter { return &VolumeMounter{ - cmdRunner: &LocalCommandRunner{shell: shell}, + CmdRunner: &LocalCommandRunner{Shell: shell}, } } @@ -40,6 +40,8 @@ func (m *VolumeMounter) Mount() error { if err != nil { return err } + // we should consider dropping this if VolumeAttachmentLoc is set. we need to also add FsType but + // otherwise that means we're listing the devices twice fsInfo, err := m.getFsInfo() if err != nil { return err @@ -47,7 +49,7 @@ func (m *VolumeMounter) Mount() error { if fsInfo == nil { return errors.New("unable to find target volume on instance") } - log.Debug().Str("device name", fsInfo.name).Msg("found target volume") + log.Debug().Str("device name", fsInfo.Name).Msg("found target volume") return m.mountVolume(fsInfo) } @@ -63,11 +65,11 @@ func (m *VolumeMounter) createScanDir() error { } func (m *VolumeMounter) getFsInfo() (*fsInfo, error) { - log.Debug().Msg("search for target volume") + log.Debug().Str("volume attachment loc", m.VolumeAttachmentLoc).Msg("search for target volume") // TODO: replace with mql query once version with lsblk resource is released // TODO: only use sudo if we are not root - cmd, err := m.cmdRunner.RunCommand("sudo lsblk -f --json") + cmd, err := m.CmdRunner.RunCommand("sudo lsblk -f --json") if err != nil { return nil, err } @@ -75,7 +77,7 @@ func (m *VolumeMounter) getFsInfo() (*fsInfo, error) { if err != nil { return nil, err } - blockEntries := blockDevices{} + blockEntries := BlockDevices{} if err := json.Unmarshal(data, &blockEntries); err != nil { return nil, err } @@ -102,12 +104,12 @@ func (m *VolumeMounter) getFsInfo() (*fsInfo, error) { func (m *VolumeMounter) mountVolume(fsInfo *fsInfo) error { opts := []string{} - if fsInfo.fstype == "xfs" { + if fsInfo.FsType == "xfs" { opts = append(opts, "nouuid") } opts = stringx.DedupStringArray(opts) - log.Debug().Str("fstype", fsInfo.fstype).Str("device", fsInfo.name).Str("scandir", m.ScanDir).Str("opts", strings.Join(opts, ",")).Msg("mount volume to scan dir") - return Mount(fsInfo.name, m.ScanDir, fsInfo.fstype, opts) + log.Debug().Str("fstype", fsInfo.FsType).Str("device", fsInfo.Name).Str("scandir", m.ScanDir).Str("opts", strings.Join(opts, ",")).Msg("mount volume to scan dir") + return Mount(fsInfo.Name, m.ScanDir, fsInfo.FsType, opts) } func (m *VolumeMounter) UnmountVolumeFromInstance() error { diff --git a/providers/os/provider/provider.go b/providers/os/provider/provider.go index 986d49be08..ec965adf86 100644 --- a/providers/os/provider/provider.go +++ b/providers/os/provider/provider.go @@ -17,6 +17,7 @@ import ( "go.mondoo.com/cnquery/v11/providers-sdk/v1/upstream" "go.mondoo.com/cnquery/v11/providers-sdk/v1/vault" "go.mondoo.com/cnquery/v11/providers/os/connection/container" + "go.mondoo.com/cnquery/v11/providers/os/connection/device" "go.mondoo.com/cnquery/v11/providers/os/connection/docker" "go.mondoo.com/cnquery/v11/providers/os/connection/fs" "go.mondoo.com/cnquery/v11/providers/os/connection/local" @@ -71,6 +72,8 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) switch req.Connector { case "local": conf.Type = shared.Type_Local.String() + case "device": + conf.Type = shared.Type_Device.String() case "ssh": conf.Type = shared.Type_SSH.String() port = 22 @@ -216,6 +219,22 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) } } + if lun, ok := flags["lun"]; ok { + conf.Options["lun"] = lun.RawData().Value.(string) + } + if deviceName, ok := flags["device-name"]; ok { + conf.Options["device-name"] = deviceName.RawData().Value.(string) + } + if platformIDs, ok := flags["platform-ids"]; ok { + platformIDs := platformIDs.Array + strs := []string{} + for _, pID := range platformIDs { + strs = append(strs, pID.RawData().Value.(string)) + } + if len(strs) > 0 { + conf.Options["inject-platform-ids"] = strings.Join(strs, ",") + } + } res := plugin.ParseCLIRes{ Asset: asset, } @@ -329,7 +348,8 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba asset.Platform = p appendRelatedAssetsFromFingerprint(fingerprint, asset) } - + case shared.Type_Device.String(): + conn, err = device.NewDeviceConnection(connId, conf, asset) case shared.Type_SSH.String(): conn, err = ssh.NewConnection(connId, conf, asset) if err != nil {