diff --git a/audit/internal/collector/storage.go b/audit/internal/collector/storage.go index 0d93b39..2f69983 100644 --- a/audit/internal/collector/storage.go +++ b/audit/internal/collector/storage.go @@ -270,9 +270,16 @@ func mergeStorageDevice(existing, incoming lsblkDevice) lsblkDevice { // smartctlInfo is the subset of smartctl -j -a output we care about. type smartctlInfo struct { - ModelFamily string `json:"model_family"` - ModelName string `json:"model_name"` - SerialNumber string `json:"serial_number"` + ModelFamily string `json:"model_family"` + ModelName string `json:"model_name"` + // ScsiModelName is set instead of ModelName when smartctl reports over the + // SCSI protocol (device.protocol == "SCSI") — this includes NVMe drives + // exposed through a RAID/HBA passthrough as /dev/sdX rather than + // /dev/nvmeX, which report no ModelName at all. + ScsiModelName string `json:"scsi_model_name"` + ScsiVendor string `json:"scsi_vendor"` + ScsiProduct string `json:"scsi_product"` + SerialNumber string `json:"serial_number"` FirmwareVer string `json:"firmware_version"` RotationRate int `json:"rotation_rate"` Temperature struct { @@ -299,6 +306,21 @@ type smartctlInfo struct { PowerCycleCount int `json:"power_cycle_count"` } +// smartctlModelName picks the drive model out of a parsed smartctl -j -a +// document. ATA/NVMe devices report it as model_name; devices smartctl talks +// to over the SCSI protocol (including NVMe drives exposed through a +// RAID/HBA passthrough as /dev/sdX) leave model_name empty and report +// scsi_model_name, or failing that scsi_vendor/scsi_product, instead. +func smartctlModelName(info smartctlInfo) string { + if v := cleanDMIValue(info.ModelName); v != "" { + return v + } + if v := cleanDMIValue(info.ScsiModelName); v != "" { + return v + } + return cleanDMIValue(strings.TrimSpace(info.ScsiVendor + " " + info.ScsiProduct)) +} + func enrichWithSmartctl(dev lsblkDevice) schema.HardwareStorage { present := true s := schema.HardwareStorage{Present: &present} @@ -349,7 +371,7 @@ func enrichWithSmartctl(dev lsblkDevice) schema.HardwareStorage { var raw map[string]any _ = json.Unmarshal(out, &raw) if err := json.Unmarshal(out, &info); err == nil { - if v := cleanDMIValue(info.ModelName); v != "" { + if v := smartctlModelName(info); v != "" { s.Model = &v } if v := cleanDMIValue(info.SerialNumber); v != "" { diff --git a/audit/internal/collector/storage_scsi_test.go b/audit/internal/collector/storage_scsi_test.go index 2bca785..4257dd3 100644 --- a/audit/internal/collector/storage_scsi_test.go +++ b/audit/internal/collector/storage_scsi_test.go @@ -99,3 +99,51 @@ func TestApplySCSISmartctlTelemetryDoesNotOverwriteExistingValues(t *testing.T) t.Fatalf("life_used_pct=%v want 50", disk.LifeUsedPct) } } + +func TestSmartctlModelNamePrefersATAModelName(t *testing.T) { + t.Parallel() + + info := smartctlInfo{ + ModelName: "Samsung SSD 970 EVO", + ScsiModelName: "should not be used", + } + if got := smartctlModelName(info); got != "Samsung SSD 970 EVO" { + t.Fatalf("model=%q want %q", got, "Samsung SSD 970 EVO") + } +} + +func TestSmartctlModelNameFallsBackToScsiModelName(t *testing.T) { + t.Parallel() + + // NVMe drive exposed through a RAID/HBA passthrough as /dev/sdX: + // smartctl reports it over the SCSI protocol, so model_name is empty + // and the model only shows up as scsi_model_name. + info := smartctlInfo{ + ScsiModelName: "NVMe SSSTC CA6-8D1024", + ScsiVendor: "NVMe", + ScsiProduct: "SSSTC CA6-8D1024", + } + if got := smartctlModelName(info); got != "NVMe SSSTC CA6-8D1024" { + t.Fatalf("model=%q want %q", got, "NVMe SSSTC CA6-8D1024") + } +} + +func TestSmartctlModelNameFallsBackToScsiVendorProduct(t *testing.T) { + t.Parallel() + + info := smartctlInfo{ + ScsiVendor: "NVMe", + ScsiProduct: "SSSTC CA6-8D1024", + } + if got := smartctlModelName(info); got != "NVMe SSSTC CA6-8D1024" { + t.Fatalf("model=%q want %q", got, "NVMe SSSTC CA6-8D1024") + } +} + +func TestSmartctlModelNameEmptyWhenNoFieldsSet(t *testing.T) { + t.Parallel() + + if got := smartctlModelName(smartctlInfo{}); got != "" { + t.Fatalf("model=%q want empty", got) + } +}