diff --git a/audit/internal/app/app.go b/audit/internal/app/app.go index 99656c0..f8b7578 100644 --- a/audit/internal/app/app.go +++ b/audit/internal/app/app.go @@ -80,6 +80,7 @@ type installer interface { ListInstallDisks() ([]platform.InstallDisk, error) InstallToDisk(ctx context.Context, device string, logFile string) error IsLiveMediaInRAM() bool + LiveBootSource() platform.LiveBootSource RunInstallToRAM(ctx context.Context, logFunc func(string)) error } @@ -100,6 +101,10 @@ func (a *App) IsLiveMediaInRAM() bool { return a.installer.IsLiveMediaInRAM() } +func (a *App) LiveBootSource() platform.LiveBootSource { + return a.installer.LiveBootSource() +} + func (a *App) RunInstallToRAM(ctx context.Context, logFunc func(string)) error { return a.installer.RunInstallToRAM(ctx, logFunc) } diff --git a/audit/internal/platform/install.go b/audit/internal/platform/install.go index 598c914..9b77a35 100644 --- a/audit/internal/platform/install.go +++ b/audit/internal/platform/install.go @@ -11,10 +11,10 @@ import ( // InstallDisk describes a candidate disk for installation. type InstallDisk struct { - Device string // e.g. /dev/sda - Model string - Size string // human-readable, e.g. "500G" - SizeBytes int64 // raw byte count from lsblk + Device string // e.g. /dev/sda + Model string + Size string // human-readable, e.g. "500G" + SizeBytes int64 // raw byte count from lsblk MountedParts []string // partition mount points currently active } @@ -117,6 +117,61 @@ func findLiveBootDevice() string { return "/dev/" + strings.TrimSpace(string(out2)) } +func mountSource(target string) string { + out, err := exec.Command("findmnt", "-n", "-o", "SOURCE", target).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func mountFSType(target string) string { + out, err := exec.Command("findmnt", "-n", "-o", "FSTYPE", target).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func blockDeviceType(device string) string { + if strings.TrimSpace(device) == "" { + return "" + } + out, err := exec.Command("lsblk", "-dn", "-o", "TYPE", device).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func blockDeviceTransport(device string) string { + if strings.TrimSpace(device) == "" { + return "" + } + out, err := exec.Command("lsblk", "-dn", "-o", "TRAN", device).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func inferLiveBootKind(fsType, source, deviceType, transport string) string { + switch { + case strings.EqualFold(strings.TrimSpace(fsType), "tmpfs"): + return "ram" + case strings.EqualFold(strings.TrimSpace(deviceType), "rom"): + return "cdrom" + case strings.EqualFold(strings.TrimSpace(transport), "usb"): + return "usb" + case strings.HasPrefix(strings.TrimSpace(source), "/dev/sr"): + return "cdrom" + case strings.HasPrefix(strings.TrimSpace(source), "/dev/"): + return "disk" + default: + return "unknown" + } +} + // MinInstallBytes returns the minimum recommended disk size for installation: // squashfs size × 1.5 to allow for extracted filesystem and bootloader. // Returns 0 if the squashfs is not available (non-live environment). diff --git a/audit/internal/platform/install_to_ram.go b/audit/internal/platform/install_to_ram.go index 5404f77..7b41846 100644 --- a/audit/internal/platform/install_to_ram.go +++ b/audit/internal/platform/install_to_ram.go @@ -12,11 +12,40 @@ import ( ) func (s *System) IsLiveMediaInRAM() bool { - out, err := exec.Command("findmnt", "-n", "-o", "FSTYPE", "/run/live/medium").Output() - if err != nil { + fsType := mountFSType("/run/live/medium") + if fsType == "" { return toramActive() } - return strings.TrimSpace(string(out)) == "tmpfs" + return strings.EqualFold(fsType, "tmpfs") +} + +func (s *System) LiveBootSource() LiveBootSource { + fsType := mountFSType("/run/live/medium") + source := mountSource("/run/live/medium") + device := findLiveBootDevice() + status := LiveBootSource{ + InRAM: strings.EqualFold(fsType, "tmpfs"), + Source: source, + Device: device, + } + if fsType == "" && source == "" && device == "" { + if toramActive() { + status.InRAM = true + status.Kind = "ram" + status.Source = "tmpfs" + return status + } + status.Kind = "unknown" + return status + } + status.Kind = inferLiveBootKind(fsType, source, blockDeviceType(device), blockDeviceTransport(device)) + if status.Kind == "" { + status.Kind = "unknown" + } + if status.InRAM && strings.TrimSpace(status.Source) == "" { + status.Source = "tmpfs" + } + return status } func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) error { diff --git a/audit/internal/platform/install_to_ram_test.go b/audit/internal/platform/install_to_ram_test.go new file mode 100644 index 0000000..18be1a3 --- /dev/null +++ b/audit/internal/platform/install_to_ram_test.go @@ -0,0 +1,28 @@ +package platform + +import "testing" + +func TestInferLiveBootKind(t *testing.T) { + tests := []struct { + name string + fsType string + source string + deviceType string + transport string + want string + }{ + {name: "ram tmpfs", fsType: "tmpfs", source: "/dev/shm/bee-live", want: "ram"}, + {name: "usb disk", source: "/dev/sdb1", deviceType: "disk", transport: "usb", want: "usb"}, + {name: "cdrom rom", source: "/dev/sr0", deviceType: "rom", want: "cdrom"}, + {name: "disk sata", source: "/dev/nvme0n1p1", deviceType: "disk", transport: "nvme", want: "disk"}, + {name: "unknown", source: "overlay", want: "unknown"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := inferLiveBootKind(tc.fsType, tc.source, tc.deviceType, tc.transport) + if got != tc.want { + t.Fatalf("inferLiveBootKind(%q,%q,%q,%q)=%q want %q", tc.fsType, tc.source, tc.deviceType, tc.transport, got, tc.want) + } + }) + } +} diff --git a/audit/internal/platform/types.go b/audit/internal/platform/types.go index 58ec647..aec7b30 100644 --- a/audit/internal/platform/types.go +++ b/audit/internal/platform/types.go @@ -2,6 +2,13 @@ package platform type System struct{} +type LiveBootSource struct { + InRAM bool `json:"in_ram"` + Kind string `json:"kind"` + Source string `json:"source,omitempty"` + Device string `json:"device,omitempty"` +} + type InterfaceInfo struct { Name string State string diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go index 24d0823..f2f8bb1 100644 --- a/audit/internal/webui/api.go +++ b/audit/internal/webui/api.go @@ -526,9 +526,9 @@ func (h *handler) handleAPIRAMStatus(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusServiceUnavailable, "app not configured") return } - inRAM := h.opts.App.IsLiveMediaInRAM() + status := h.opts.App.LiveBootSource() w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]bool{"in_ram": inRAM}) + _ = json.NewEncoder(w).Encode(status) } func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request) { diff --git a/audit/internal/webui/pages.go b/audit/internal/webui/pages.go index 09b0979..ce74467 100644 --- a/audit/internal/webui/pages.go +++ b/audit/internal/webui/pages.go @@ -1282,6 +1282,7 @@ func renderTools() string {
Install to RAM
+

Detecting boot source...

Checking...

@@ -1293,8 +1294,18 @@ func renderTools() string {