Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Mount attached volume from CLI #90

Merged
merged 10 commits into from
Mar 5, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
2 changes: 2 additions & 0 deletions Dockerfile.cli
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ RUN --mount=type=cache,target=/go/pkg/mod \

FROM ${VMCLARITY_TOOLS_BASE}

RUN apk add util-linux

WORKDIR /app

COPY --from=builder /build/cli/cli ./vmclarity-cli
Expand Down
79 changes: 79 additions & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ import (
"os"

"github.com/ghodss/yaml"
"github.com/openclarity/kubeclarity/shared/pkg/utils"
uuid "github.com/satori/go.uuid"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/openclarity/vmclarity/cli/pkg"
"github.com/openclarity/vmclarity/cli/pkg/mount"
"github.com/openclarity/vmclarity/shared/pkg/families"
"github.com/openclarity/vmclarity/shared/pkg/families/exploits"
"github.com/openclarity/vmclarity/shared/pkg/families/results"
Expand All @@ -42,6 +45,12 @@ var (

server string
scanResultID string
mountVolume bool
)

const (
fsTypeExt4 = "ext4"
fsTypeXFS = "xfs"
)

// rootCmd represents the base command when called without any subcommands.
Expand All @@ -53,6 +62,15 @@ var rootCmd = &cobra.Command{
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
logger.Infof("Running...")

if mountVolume {
mountPoints, err := mountAttachedVolume()
if err != nil {
return fmt.Errorf("failed to mount attached volume: %v", err)
}
setMountPointsForFamiliesInput(mountPoints, config)
}

var exporter *Exporter
if server != "" {
exp, err := CreateExporter()
Expand Down Expand Up @@ -193,6 +211,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&output, "output", "", "set file path output (default: stdout)")
rootCmd.PersistentFlags().StringVar(&server, "server", "", "VMClarity server to export scan results to, for example: http://localhost:9999/api")
rootCmd.PersistentFlags().StringVar(&scanResultID, "scan-result-id", "", "the ScanResult ID to export the scan results to")
rootCmd.PersistentFlags().BoolVar(&mountVolume, "mount-attached-volume", false, "discover for an attached volume and mount it before the scan")

// TODO(sambetts) we may have to change this to our own validation when
// we add the CI/CD scenario and there isn't an existing scan-result-id
Expand Down Expand Up @@ -266,3 +285,63 @@ func Output(bytes []byte, outputPrefix string) error {

return nil
}

func isSupportedFS(fs string) bool {
akpsgit marked this conversation as resolved.
Show resolved Hide resolved
switch fs {
case fsTypeExt4, fsTypeXFS:
return true
}
return false
}

func setMountPointsForFamiliesInput(mountPoints []string, familiesConfig *families.Config) *families.Config {
// update families inputs with the mount point as rootfs
for _, mountDir := range mountPoints {
if familiesConfig.SBOM.Enabled {
familiesConfig.SBOM.Inputs = append(familiesConfig.SBOM.Inputs, sbom.Input{
Input: mountDir,
InputType: string(utils.ROOTFS),
})
}
if familiesConfig.Vulnerabilities.Enabled {
if familiesConfig.SBOM.Enabled {
familiesConfig.Vulnerabilities.InputFromSbom = true
} else {
familiesConfig.Vulnerabilities.Inputs = append(familiesConfig.Vulnerabilities.Inputs, vulnerabilities.Input{
Input: mountDir,
InputType: string(utils.ROOTFS),
})
}
}
if familiesConfig.Secrets.Enabled {
familiesConfig.Secrets.Inputs = append(familiesConfig.Secrets.Inputs, secrets.Input{
Input: mountDir,
InputType: string(utils.ROOTFS),
})
}
}
return familiesConfig
}

func mountAttachedVolume() ([]string, error) {
var mountPoints []string

devices, err := mount.ListBlockDevices()
if err != nil {
return nil, fmt.Errorf("failed to list block devices: %v", err)
}
for _, device := range devices {
// if the device is not mounted and of a supported filesystem type,
// we assume it belongs to the attached volume, so we mount it.
if device.MountPoint == "" && isSupportedFS(device.FilesystemType) {
akpsgit marked this conversation as resolved.
Show resolved Hide resolved
mountDir := "/mnt/snapshot" + uuid.NewV4().String()

if err := device.Mount(mountDir); err != nil {
return nil, fmt.Errorf("failed to mount device: %v", err)
}
logger.Infof("Mounted device %v on %v", device.DeviceName, mountDir)
mountPoints = append(mountPoints, mountDir)
}
}
return mountPoints, nil
}
143 changes: 143 additions & 0 deletions cli/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package cmd

import (
"reflect"
"testing"

"github.com/openclarity/kubeclarity/shared/pkg/utils"

"github.com/openclarity/vmclarity/shared/pkg/families"
"github.com/openclarity/vmclarity/shared/pkg/families/sbom"
"github.com/openclarity/vmclarity/shared/pkg/families/secrets"
"github.com/openclarity/vmclarity/shared/pkg/families/vulnerabilities"
)

func Test_isSupportedFS(t *testing.T) {
type args struct {
fs string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "supported ext4",
args: args{
fs: fsTypeExt4,
},
want: true,
},
{
name: "supported xfs",
args: args{
fs: fsTypeXFS,
},
want: true,
},
{
name: "not supported btrfs",
args: args{
fs: "btrfs",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isSupportedFS(tt.args.fs); got != tt.want {
t.Errorf("isSupportedFS() = %v, want %v", got, tt.want)
}
})
}
}

func Test_setMountPointsForFamiliesInput(t *testing.T) {
type args struct {
mountPoints []string
familiesConfig *families.Config
}
tests := []struct {
name string
args args
want *families.Config
}{
{
name: "sbom, vuls and secrets are enabled",
args: args{
mountPoints: []string{"/mnt/snapshot1"},
familiesConfig: &families.Config{
SBOM: sbom.Config{
Enabled: true,
Inputs: nil,
},
Vulnerabilities: vulnerabilities.Config{
Enabled: true,
Inputs: nil,
InputFromSbom: false,
},
Secrets: secrets.Config{
Enabled: true,
Inputs: nil,
},
},
},
want: &families.Config{
SBOM: sbom.Config{
Enabled: true,
Inputs: []sbom.Input{
{
Input: "/mnt/snapshot1",
InputType: string(utils.ROOTFS),
},
},
},
Vulnerabilities: vulnerabilities.Config{
Enabled: true,
InputFromSbom: true,
},
Secrets: secrets.Config{
Enabled: true,
Inputs: []secrets.Input{
{
Input: "/mnt/snapshot1",
InputType: string(utils.ROOTFS),
},
},
},
},
},
{
name: "only vuls enabled",
args: args{
mountPoints: []string{"/mnt/snapshot1"},
familiesConfig: &families.Config{
Vulnerabilities: vulnerabilities.Config{
Enabled: true,
Inputs: nil,
InputFromSbom: false,
},
},
},
want: &families.Config{
Vulnerabilities: vulnerabilities.Config{
Enabled: true,
Inputs: []vulnerabilities.Input{
{
Input: "/mnt/snapshot1",
InputType: string(utils.ROOTFS),
},
},
InputFromSbom: false,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := setMountPointsForFamiliesInput(tt.args.mountPoints, tt.args.familiesConfig); !reflect.DeepEqual(got, tt.want) {
t.Errorf("setMountPointsForFamiliesInput() = %v, want %v", got, tt.want)
}
})
}
}
119 changes: 119 additions & 0 deletions cli/pkg/mount/mount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package mount

import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"

log "github.com/sirupsen/logrus"
"k8s.io/mount-utils"
)

var pairsRE = regexp.MustCompile(`([A-Z]+)=(?:"(.*?)")`)

type BlockDevice struct {
DeviceName string
Size uint64
Label string
UUID string
FilesystemType string
MountPoint string
}

// ListBlockDevices Taken from https://github.com/BishopFox/dufflebag
func ListBlockDevices() ([]BlockDevice, error) {
log.Info("Listing block devices...")
columns := []string{
"NAME", // name
"SIZE", // size
"LABEL", // filesystem label
"UUID", // filesystem UUID
"FSTYPE", // filesystem type
"TYPE", // device type
"MOUNTPOINT", // device mountpoint
}

log.Info("executing lsblk...")
output, err := exec.Command(
"lsblk",
"-b", // output size in bytes
"-P", // output fields as key=value pairs
"-o", strings.Join(columns, ","),
).Output()
if err != nil {
return nil, fmt.Errorf("cannot list block devices: %v", err)
}

blockDeviceMap := make(map[string]BlockDevice)
s := bufio.NewScanner(bytes.NewReader(output))
for s.Scan() {
pairs := pairsRE.FindAllStringSubmatch(s.Text(), -1)
var dev BlockDevice
var deviceType string
for _, pair := range pairs {
switch pair[1] {
case "NAME":
dev.DeviceName = pair[2]
case "SIZE":
size, err := strconv.ParseUint(pair[2], 10, 64)
if err != nil {
log.Warnf(
"Invalid size %q from lsblk: %v", pair[2], err,
)
} else {
// the number of bytes in a MiB.
dev.Size = size / 1024 * 1024
}
case "LABEL":
dev.Label = pair[2]
case "UUID":
dev.UUID = pair[2]
case "FSTYPE":
dev.FilesystemType = pair[2]
case "TYPE":
deviceType = pair[2]
case "MOUNTPOINT":
dev.MountPoint = pair[2]
default:
log.Warnf("unexpected field from lsblk: %q", pair[1])
}
}

if deviceType == "loop" {
continue
}

blockDeviceMap[dev.DeviceName] = dev
}
if err := s.Err(); err != nil {
return nil, fmt.Errorf("cannot parse lsblk output: %v", err)
}

blockDevices := make([]BlockDevice, 0, len(blockDeviceMap))
for _, dev := range blockDeviceMap {
blockDevices = append(blockDevices, dev)
}
return blockDevices, nil
}

func (b BlockDevice) Mount(mountPoint string) error {
// Make a directory for the device to mount to
if err := os.MkdirAll(mountPoint, os.ModePerm); err != nil {
return fmt.Errorf("failed to run mkdir comand: %v", err)
}

// Do the mount

mounter := mount.New(mountPoint)
if err := mounter.Mount("/dev/"+b.DeviceName, mountPoint, b.FilesystemType, nil); err != nil {
return fmt.Errorf("failed to run mount command: %v", err)
}
b.MountPoint = mountPoint

return nil
}
Loading