package collector import ( "bee/audit/internal/schema" "bufio" "log/slog" "os/exec" "strings" ) // collectBoard runs dmidecode for types 0, 1, 2 and returns the board record // plus the BIOS firmware entry. Any failure is logged and returns zero values. func collectBoard() (schema.HardwareBoard, []schema.HardwareFirmwareRecord) { type1, err := runDmidecode("1") if err != nil { slog.Warn("board: dmidecode type 1 failed", "err", err) return schema.HardwareBoard{}, nil } type2, err := runDmidecode("2") if err != nil { slog.Warn("board: dmidecode type 2 failed", "err", err) } type0, err := runDmidecode("0") if err != nil { slog.Warn("board: dmidecode type 0 failed", "err", err) } board := parseBoard(type1, type2) firmware := parseBIOSFirmware(type0) slog.Info("board: collected", "serial", board.SerialNumber) return board, firmware } // parseBoard extracts HardwareBoard from dmidecode type 1 (System) and type 2 (Baseboard) output. func parseBoard(type1, type2 string) schema.HardwareBoard { sys := parseDMIFields(type1, "System Information") base := parseDMIFields(type2, "Base Board Information") board := schema.HardwareBoard{} if v := cleanDMIValue(sys["Manufacturer"]); v != "" { board.Manufacturer = &v } if v := cleanDMIValue(sys["Product Name"]); v != "" { board.ProductName = &v } if v := cleanDMIValue(sys["Serial Number"]); v != "" { board.SerialNumber = v } if v := cleanDMIValue(sys["UUID"]); v != "" { board.UUID = &v } // part number comes from baseboard Product Name if v := cleanDMIValue(base["Product Name"]); v != "" { board.PartNumber = &v } return board } // parseBIOSFirmware extracts BIOS version from dmidecode type 0 output. func parseBIOSFirmware(type0 string) []schema.HardwareFirmwareRecord { fields := parseDMIFields(type0, "BIOS Information") version := cleanDMIValue(fields["Version"]) if version == "" { return nil } return []schema.HardwareFirmwareRecord{ {DeviceName: "BIOS", Version: version}, } } // parseDMIFields parses the key-value pairs from a dmidecode section. // sectionTitle is the section header line to find (e.g. "System Information"). // Returns a map of trimmed field names to raw values. func parseDMIFields(output, sectionTitle string) map[string]string { fields := make(map[string]string) inSection := false scanner := bufio.NewScanner(strings.NewReader(output)) for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == sectionTitle { inSection = true continue } if inSection { // blank line or new Handle line = end of section if line == "" || strings.HasPrefix(line, "Handle ") { break } // skip sub-list items (double tab indent) if strings.HasPrefix(line, "\t\t") { continue } // key: value line (single tab indent) trimmed := strings.TrimPrefix(line, "\t") if idx := strings.Index(trimmed, ": "); idx >= 0 { key := trimmed[:idx] val := trimmed[idx+2:] fields[key] = val } } } return fields } // cleanDMIValue returns empty string for known placeholder values that vendors // use when a field is unpopulated. func cleanDMIValue(v string) string { v = strings.TrimSpace(v) if v == "" { return "" } upper := strings.ToUpper(v) placeholders := []string{ "TO BE FILLED BY O.E.M.", "NOT SPECIFIED", "NOT SETTABLE", "NOT PRESENT", "UNKNOWN", "N/A", "NONE", "NULL", "DEFAULT STRING", "0", } for _, p := range placeholders { if upper == p { return "" } } return v } // runDmidecode executes dmidecode -t and returns its stdout. func runDmidecode(typeNum string) (string, error) { out, err := exec.Command("dmidecode", "-t", typeNum).Output() if err != nil { return "", err } return string(out), nil }