package collector import ( "bee/audit/internal/schema" "log/slog" "os/exec" "strconv" "strings" ) func collectPCIe() []schema.HardwarePCIeDevice { out, err := exec.Command("lspci", "-vmm", "-D").Output() if err != nil { slog.Warn("pcie: lspci failed", "err", err) return nil } devs := parseLspci(string(out)) slog.Info("pcie: collected", "count", len(devs)) return devs } func parseLspci(output string) []schema.HardwarePCIeDevice { // lspci -vmm -D outputs blank-line separated records, each field is "Key:\tValue" var devs []schema.HardwarePCIeDevice for _, block := range strings.Split(output, "\n\n") { block = strings.TrimSpace(block) if block == "" { continue } fields := map[string]string{} for _, line := range strings.Split(block, "\n") { idx := strings.Index(line, ":\t") if idx < 0 { continue } key := strings.TrimSpace(line[:idx]) val := strings.TrimSpace(line[idx+2:]) fields[key] = val } if !shouldIncludePCIeDevice(fields["Class"]) { continue } dev := parseLspciDevice(fields) devs = append(devs, dev) } return devs } func shouldIncludePCIeDevice(class string) bool { c := strings.ToLower(strings.TrimSpace(class)) if c == "" { return true } // Keep inventory focused on useful replaceable components, not chipset/virtual noise. excluded := []string{ "host bridge", "isa bridge", "pci bridge", "ram memory", "system peripheral", "communication controller", "signal processing controller", "usb controller", "smbus", "audio device", "serial bus controller", "unassigned class", } for _, bad := range excluded { if strings.Contains(c, bad) { return false } } return true } func parseLspciDevice(fields map[string]string) schema.HardwarePCIeDevice { dev := schema.HardwarePCIeDevice{} present := true dev.Present = &present status := "OK" dev.Status = &status // Slot is the BDF: "0000:00:02.0" if bdf := fields["Slot"]; bdf != "" { dev.BDF = &bdf // parse vendor_id and device_id from sysfs vendorID, deviceID := readPCIIDs(bdf) if vendorID != 0 { dev.VendorID = &vendorID } if deviceID != 0 { dev.DeviceID = &deviceID } } if v := fields["Class"]; v != "" { dev.DeviceClass = &v } if v := fields["Vendor"]; v != "" { dev.Manufacturer = &v } if v := fields["Device"]; v != "" { dev.Model = &v } // SVendor/SDevice available but not in schema — skip return dev } // readPCIIDs reads vendor and device IDs from sysfs for a given BDF. func readPCIIDs(bdf string) (vendorID, deviceID int) { base := "/sys/bus/pci/devices/" + bdf if v, err := readHexFile(base + "/vendor"); err == nil { vendorID = v } if v, err := readHexFile(base + "/device"); err == nil { deviceID = v } return } func readHexFile(path string) (int, error) { out, err := exec.Command("cat", path).Output() if err != nil { return 0, err } s := strings.TrimSpace(strings.TrimPrefix(string(out), "0x")) n, err := strconv.ParseInt(s, 16, 64) return int(n), err }