feat(audit): 1.2 — board collector (dmidecode types 0, 1, 2)

- board.go: collectBoard(), parseBoard(), parseBIOSFirmware(), parseDMIFields(), cleanDMIValue()
- Reads System Information (type 1): serial, manufacturer, product_name, uuid
- Reads Base Board Information (type 2): part_number
- Reads BIOS Information (type 0): firmware version record
- cleanDMIValue strips vendor placeholders (O.E.M., Not Specified, Unknown, etc.)
- board_test.go: 6 table/case tests with dmidecode fixtures in testdata/
- collector.go: wired board + BIOS firmware into snapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 10:35:14 +03:00
parent a4f70b17f0
commit f1e392a7fe
7 changed files with 341 additions and 1 deletions

View File

@@ -0,0 +1,149 @@
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 <typeNum> 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
}

View File

@@ -0,0 +1,119 @@
package collector
import (
"os"
"testing"
)
func TestParseBoard(t *testing.T) {
type1 := mustReadFile(t, "testdata/dmidecode_type1.txt")
type2 := mustReadFile(t, "testdata/dmidecode_type2.txt")
board := parseBoard(type1, type2)
if board.SerialNumber != "CAR315KA0803B90" {
t.Errorf("serial_number: got %q, want %q", board.SerialNumber, "CAR315KA0803B90")
}
if board.Manufacturer == nil || *board.Manufacturer != "Inspur" {
t.Errorf("manufacturer: got %v, want Inspur", board.Manufacturer)
}
if board.ProductName == nil || *board.ProductName != "NF5468M7" {
t.Errorf("product_name: got %v, want NF5468M7", board.ProductName)
}
if board.PartNumber == nil || *board.PartNumber != "YZCA-02758-105" {
t.Errorf("part_number: got %v, want YZCA-02758-105", board.PartNumber)
}
if board.UUID == nil || *board.UUID != "a1b2c3d4-e5f6-7890-abcd-ef1234567890" {
t.Errorf("uuid: got %v, want a1b2c3d4-e5f6-7890-abcd-ef1234567890", board.UUID)
}
}
func TestParseBoard_emptySerial(t *testing.T) {
type1 := mustReadFile(t, "testdata/dmidecode_type1_empty_serial.txt")
board := parseBoard(type1, "")
if board.SerialNumber != "" {
t.Errorf("expected empty serial for placeholder value, got %q", board.SerialNumber)
}
if board.Manufacturer != nil {
t.Errorf("expected nil manufacturer for placeholder, got %q", *board.Manufacturer)
}
if board.UUID != nil {
t.Errorf("expected nil UUID for 'Not Settable', got %q", *board.UUID)
}
}
func TestParseBIOSFirmware(t *testing.T) {
type0 := mustReadFile(t, "testdata/dmidecode_type0.txt")
fw := parseBIOSFirmware(type0)
if len(fw) != 1 {
t.Fatalf("expected 1 firmware record, got %d", len(fw))
}
if fw[0].DeviceName != "BIOS" {
t.Errorf("device_name: got %q, want BIOS", fw[0].DeviceName)
}
if fw[0].Version != "06.08.05" {
t.Errorf("version: got %q, want 06.08.05", fw[0].Version)
}
}
func TestParseBIOSFirmware_empty(t *testing.T) {
fw := parseBIOSFirmware("")
if len(fw) != 0 {
t.Errorf("expected no firmware records for empty input, got %d", len(fw))
}
}
func TestCleanDMIValue(t *testing.T) {
tests := []struct {
input string
want string
}{
{"CAR315KA0803B90", "CAR315KA0803B90"},
{"To Be Filled By O.E.M.", ""},
{"to be filled by o.e.m.", ""},
{"Not Specified", ""},
{"Not Settable", ""},
{"Unknown", ""},
{"N/A", ""},
{"None", ""},
{"NULL", ""},
{"Default String", ""},
{" Inspur ", "Inspur"},
{"", ""},
{"0", ""},
}
for _, tt := range tests {
got := cleanDMIValue(tt.input)
if got != tt.want {
t.Errorf("cleanDMIValue(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestParseDMIFields(t *testing.T) {
type1 := mustReadFile(t, "testdata/dmidecode_type1.txt")
fields := parseDMIFields(type1, "System Information")
if fields["Manufacturer"] != "Inspur" {
t.Errorf("Manufacturer: got %q", fields["Manufacturer"])
}
if fields["Serial Number"] != "CAR315KA0803B90" {
t.Errorf("Serial Number: got %q", fields["Serial Number"])
}
// sub-list items must not be included
if _, ok := fields["PCI is supported"]; ok {
t.Error("sub-list item should not appear in fields")
}
}
func mustReadFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read fixture %s: %v", path, err)
}
return string(b)
}

View File

@@ -17,7 +17,11 @@ func Run() schema.HardwareIngestRequest {
snap := schema.HardwareSnapshot{}
// collectors are added here in subsequent steps (1.2 1.10)
board, biosFW := collectBoard()
snap.Board = board
snap.Firmware = append(snap.Firmware, biosFW...)
// remaining collectors added in steps 1.3 1.10
slog.Info("audit completed", "duration", time.Since(start).Round(time.Millisecond))

View File

@@ -0,0 +1,25 @@
# dmidecode 3.5
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0000, DMI type 0, 26 bytes
BIOS Information
Vendor: American Megatrends Inc.
Version: 06.08.05
Release Date: 12/20/2023
Address: 0xF0000
Runtime Size: 64 kB
ROM Size: 32 MB
Characteristics:
PCI is supported
PCI Express is supported
BIOS is upgradeable
BIOS shadowing is allowed
Selectable boot is supported
ACPI is supported
USB legacy is supported
BIOS boot specification is supported
Targeted content distribution is supported
UEFI is supported
BIOS Revision: 5.22
Firmware Revision: 4.1

View File

@@ -0,0 +1,14 @@
# dmidecode 3.5
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0001, DMI type 1, 27 bytes
System Information
Manufacturer: Inspur
Product Name: NF5468M7
Version: To Be Filled By O.E.M.
Serial Number: CAR315KA0803B90
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Wake-up Type: Power Switch
SKU Number: To Be Filled By O.E.M.
Family: To Be Filled By O.E.M.

View File

@@ -0,0 +1,14 @@
# dmidecode 3.5
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0001, DMI type 1, 27 bytes
System Information
Manufacturer: To Be Filled By O.E.M.
Product Name: To Be Filled By O.E.M.
Version: To Be Filled By O.E.M.
Serial Number: To Be Filled By O.E.M.
UUID: Not Settable
Wake-up Type: Power Switch
SKU Number: To Be Filled By O.E.M.
Family: To Be Filled By O.E.M.

View File

@@ -0,0 +1,15 @@
# dmidecode 3.5
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
Manufacturer: Inspur
Product Name: YZCA-02758-105
Version: To Be Filled By O.E.M.
Serial Number: CAR315KA0803B90
Asset Tag: To Be Filled By O.E.M.
Features:
Board is a hosting board
Location In Chassis: To Be Filled By O.E.M.
Type: Motherboard