From 8149360410f8297cdbae29077c9d837303398d64 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Thu, 18 Jun 2026 15:58:02 +0300 Subject: [PATCH] Fix SAA DMI parser to match real DMI.txt format Replace the guessed pipe/key=value parser with the correct format documented in SAA User Guide 4.8.1: [Section] Item Name {SHN} = "value" // comment Handles string values (strips surrounding quotes), non-string values (UUID, hex), section headers for display names, version line, and // comments. Verified against the SAA 1.5.0 User Guide sample. Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/webui/saa_dmi.go | 60 ++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/audit/internal/webui/saa_dmi.go b/audit/internal/webui/saa_dmi.go index bac3280..9976071 100644 --- a/audit/internal/webui/saa_dmi.go +++ b/audit/internal/webui/saa_dmi.go @@ -24,42 +24,56 @@ type saaChange struct { Value string `json:"value"` } -var shnRE = regexp.MustCompile(`^[A-Za-z0-9_]{1,16}$`) +var ( + shnRE = regexp.MustCompile(`^[A-Za-z0-9_]{1,16}$`) + dmiSectionRE = regexp.MustCompile(`^\[(.+?)\]$`) + // Item Name {SHN} = value // comment + dmiItemRE = regexp.MustCompile(`^(.+?)\s+\{([A-Za-z0-9]{1,16})\}\s*=\s*(.*)$`) + dmiVersionRE = regexp.MustCompile(`(?i)^version\s*=`) +) // parseDMIFile parses the DMI.txt produced by "saa GetDmiInfo". -// Supports two formats: -// - Name|Shn|Value (pipe-separated, primary) -// - Shn=Value (key=value fallback) +// Real format (from SAA User Guide 4.8.1): // -// Lines starting with '#', empty lines, "version=..." and section headers are skipped. +// [System] +// Version {SYVS} = "A Version" // string value +// Serial Number {SYSN} = $DEFAULT$ // string value +// UUID {SYUU} = 00112233-... // hex value func parseDMIFile(content string) []dmiField { var fields []dmiField + currentSection := "" for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { + if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") { continue } - lower := strings.ToLower(line) - if strings.HasPrefix(lower, "version=") || strings.HasPrefix(lower, "[") { + if dmiVersionRE.MatchString(line) { continue } - parts := strings.SplitN(line, "|", 3) - if len(parts) == 3 { - name := strings.TrimSpace(parts[0]) - shn := strings.TrimSpace(parts[1]) - value := strings.TrimSpace(parts[2]) - if shnRE.MatchString(shn) { - fields = append(fields, dmiField{Name: name, Shn: shn, Value: value}) - continue - } + if m := dmiSectionRE.FindStringSubmatch(line); m != nil { + currentSection = strings.TrimSpace(m[1]) + continue } - if idx := strings.IndexByte(line, '='); idx > 0 { - shn := strings.TrimSpace(line[:idx]) - value := strings.TrimSpace(line[idx+1:]) - if shnRE.MatchString(shn) { - fields = append(fields, dmiField{Name: shn, Shn: shn, Value: value}) - } + m := dmiItemRE.FindStringSubmatch(line) + if m == nil { + continue } + itemName := strings.TrimSpace(m[1]) + shn := m[2] + rawValue := strings.TrimSpace(m[3]) + // strip trailing comment (space + //) + if idx := strings.LastIndex(rawValue, " //"); idx >= 0 { + rawValue = strings.TrimSpace(rawValue[:idx]) + } + // strip surrounding double quotes from string values + if len(rawValue) >= 2 && rawValue[0] == '"' && rawValue[len(rawValue)-1] == '"' { + rawValue = rawValue[1 : len(rawValue)-1] + } + displayName := itemName + if currentSection != "" { + displayName = currentSection + " / " + itemName + } + fields = append(fields, dmiField{Name: displayName, Shn: shn, Value: rawValue}) } return fields }