168 lines
4.2 KiB
Go
168 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"bee/audit/internal/collector"
|
|
)
|
|
|
|
// Version is the audit binary version.
|
|
// Injected at release build time via:
|
|
//
|
|
// -ldflags "-X main.Version=1.2"
|
|
//
|
|
// Defaults to "dev" in local builds.
|
|
var Version = "dev"
|
|
|
|
func main() {
|
|
output := flag.String("output", "stdout", `output destination:
|
|
stdout — print JSON to stdout (default)
|
|
file:<path> — write JSON to file
|
|
usb — auto-detect removable media, write JSON there`)
|
|
showVersion := flag.Bool("version", false, "print version and exit")
|
|
flag.Parse()
|
|
|
|
if *showVersion {
|
|
fmt.Println(Version)
|
|
return
|
|
}
|
|
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
})))
|
|
|
|
result := collector.Run()
|
|
|
|
data, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
slog.Error("marshal result", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err := writeOutput(*output, data); err != nil {
|
|
slog.Error("write output", "destination", *output, "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func writeOutput(dest string, data []byte) error {
|
|
switch {
|
|
case dest == "stdout":
|
|
_, err := os.Stdout.Write(append(data, '\n'))
|
|
return err
|
|
|
|
case strings.HasPrefix(dest, "file:"):
|
|
path := strings.TrimPrefix(dest, "file:")
|
|
return os.WriteFile(path, append(data, '\n'), 0644)
|
|
|
|
case dest == "usb":
|
|
return writeToUSB(data)
|
|
|
|
default:
|
|
return fmt.Errorf("unknown output destination %q — use stdout, file:<path>, or usb", dest)
|
|
}
|
|
}
|
|
|
|
// writeToUSB auto-detects the first removable block device, mounts it,
|
|
// and writes the audit JSON. Falls back to /tmp on any failure.
|
|
func writeToUSB(data []byte) error {
|
|
boardSerial := extractBoardSerial(data)
|
|
filename := auditFilename(boardSerial, time.Now().UTC())
|
|
|
|
device, err := firstRemovableDevice()
|
|
if err != nil {
|
|
slog.Warn("usb output: no removable device, writing to /tmp", "err", err)
|
|
return writeAuditToPath(filepath.Join("/tmp", filename), data)
|
|
}
|
|
|
|
mountpoint := "/tmp/bee-usb"
|
|
if err := os.MkdirAll(mountpoint, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := exec.Command("mount", device, mountpoint).Run(); err != nil {
|
|
slog.Warn("usb output: mount failed, writing to /tmp", "device", device, "err", err)
|
|
return writeAuditToPath(filepath.Join("/tmp", filename), data)
|
|
}
|
|
defer func() {
|
|
if err := exec.Command("umount", mountpoint).Run(); err != nil {
|
|
slog.Warn("usb output: umount failed", "mountpoint", mountpoint, "err", err)
|
|
}
|
|
}()
|
|
|
|
path := filepath.Join(mountpoint, filename)
|
|
if err := writeAuditToPath(path, data); err != nil {
|
|
slog.Warn("usb output: write failed, falling back to /tmp", "path", path, "err", err)
|
|
return writeAuditToPath(filepath.Join("/tmp", filename), data)
|
|
}
|
|
|
|
slog.Info("usb output: written", "path", path)
|
|
return nil
|
|
}
|
|
|
|
func writeAuditToPath(path string, data []byte) error {
|
|
if err := os.WriteFile(path, append(data, '\n'), 0644); err != nil {
|
|
return err
|
|
}
|
|
slog.Info("audit output written", "path", path)
|
|
return nil
|
|
}
|
|
|
|
func extractBoardSerial(data []byte) string {
|
|
var doc struct {
|
|
Hardware struct {
|
|
Board struct {
|
|
SerialNumber string `json:"serial_number"`
|
|
} `json:"board"`
|
|
} `json:"hardware"`
|
|
}
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return "unknown"
|
|
}
|
|
serial := strings.TrimSpace(doc.Hardware.Board.SerialNumber)
|
|
if serial == "" {
|
|
return "unknown"
|
|
}
|
|
return serial
|
|
}
|
|
|
|
func auditFilename(boardSerial string, now time.Time) string {
|
|
boardSerial = strings.TrimSpace(boardSerial)
|
|
if boardSerial == "" {
|
|
boardSerial = "unknown"
|
|
}
|
|
return fmt.Sprintf("audit-%s-%s.json", boardSerial, now.Format("20060102-150405"))
|
|
}
|
|
|
|
func firstRemovableDevice() (string, error) {
|
|
entries, err := os.ReadDir("/sys/block")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
|
|
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") {
|
|
continue
|
|
}
|
|
removableFlag, err := os.ReadFile(filepath.Join("/sys/block", name, "removable"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(string(removableFlag)) == "1" {
|
|
return filepath.Join("/dev", name), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no removable block device found")
|
|
}
|