mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Harden TrueNAS SMART percentage parsing
This commit is contained in:
@@ -2643,7 +2643,9 @@ classes, typed `ue` and `sb` arguments from `alert.list` project onto canonical
|
||||
media-error and available-spare SMART fields after exact disk resolution. The
|
||||
provider must reject negative or out-of-range values, retain the worst value
|
||||
when duplicate evidence is present, and never derive a counter from formatted
|
||||
alert text.
|
||||
alert text. Available-spare parsing must validate the integer and its 0..100
|
||||
domain before conversion to the canonical `int` field; oversized, fractional,
|
||||
or wrapped malformed values are rejected rather than narrowed or truncated.
|
||||
The same boundary owns TrueNAS `smart_status` normalization. `internal/truenas/client.go`
|
||||
must parse REST and RPC SMART status separately from native disk state, and
|
||||
`internal/truenas/disk_health.go` plus `internal/truenas/provider.go` must map
|
||||
|
||||
@@ -1298,12 +1298,11 @@ func smartAlertEvidenceFromArgs(class string, args map[string]any) (int64, bool,
|
||||
return *value, true, 0, false
|
||||
}
|
||||
case "smartspareblockcount", "smartspareblockcountalert":
|
||||
value := readInt64PtrAny(args, "sb")
|
||||
// TrueNAS publishes the normalized spare-block reserve as a percentage.
|
||||
// Reject malformed/out-of-range values rather than projecting them onto
|
||||
// the canonical percentage field.
|
||||
if value != nil && *value >= 0 && *value <= 100 {
|
||||
return 0, false, int(*value), true
|
||||
// Parse directly into the canonical integer width and reject malformed or
|
||||
// out-of-range values before projecting them onto the percentage field.
|
||||
if value, ok := readSMARTPercentageAny(args, "sb"); ok {
|
||||
return 0, false, value, true
|
||||
}
|
||||
}
|
||||
return 0, false, 0, false
|
||||
@@ -4788,6 +4787,64 @@ func readInt64PtrAny(record map[string]any, keys ...string) *int64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSMARTPercentageAny(record map[string]any, keys ...string) (int, bool) {
|
||||
if record == nil {
|
||||
return 0, false
|
||||
}
|
||||
for _, key := range keys {
|
||||
value, ok := record[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if parsed, ok := parseSMARTPercentageAny(value); ok {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func parseSMARTPercentageAny(value any) (int, bool) {
|
||||
var parsed int
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
parsed = typed
|
||||
case int64:
|
||||
if typed < 0 || typed > 100 {
|
||||
return 0, false
|
||||
}
|
||||
parsed = int(typed)
|
||||
case float64:
|
||||
if math.Trunc(typed) != typed || typed < 0 || typed > 100 {
|
||||
return 0, false
|
||||
}
|
||||
parsed = int(typed)
|
||||
case json.Number:
|
||||
return parseSMARTPercentageAny(typed.String())
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(typed)
|
||||
if trimmed == "" {
|
||||
return 0, false
|
||||
}
|
||||
integer, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
parsed = integer
|
||||
case map[string]any:
|
||||
if nested, ok := firstAny(typed, "parsed", "value", "rawvalue", "raw"); ok {
|
||||
return parseSMARTPercentageAny(nested)
|
||||
}
|
||||
return 0, false
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if parsed < 0 || parsed > 100 {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
func parseInt64Any(value any) (int64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case int64:
|
||||
|
||||
@@ -374,8 +374,18 @@ func TestSMARTAlertEvidenceFromArgsUsesOnlyTypedNativeFields(t *testing.T) {
|
||||
name: "spare reserve", class: "SMARTSpareBlockCountAlert",
|
||||
args: map[string]any{"sb": "8"}, wantSpare: 8, wantSpareSeen: true,
|
||||
},
|
||||
{
|
||||
name: "zero spare reserve", class: "SMARTSpareBlockCount",
|
||||
args: map[string]any{"sb": json.Number("0")}, wantSpareSeen: true,
|
||||
},
|
||||
{
|
||||
name: "maximum spare reserve", class: "SMARTSpareBlockCountAlert",
|
||||
args: map[string]any{"sb": map[string]any{"parsed": int64(100)}}, wantSpare: 100, wantSpareSeen: true,
|
||||
},
|
||||
{name: "negative counter rejected", class: "SMARTUncorrectedErrors", args: map[string]any{"ue": -1}},
|
||||
{name: "out of range percentage rejected", class: "SMARTSpareBlockCount", args: map[string]any{"sb": 101}},
|
||||
{name: "oversized percentage rejected", class: "SMARTSpareBlockCount", args: map[string]any{"sb": "9223372036854775807"}},
|
||||
{name: "fractional percentage rejected", class: "SMARTSpareBlockCount", args: map[string]any{"sb": 8.5}},
|
||||
{name: "formatted text is not parsed", class: "SMARTUncorrectedErrors", args: map[string]any{"text": "53 errors"}},
|
||||
{name: "unrelated alert cannot inject smart data", class: "DiskTemperatureAlert", args: map[string]any{"ue": 53, "sb": 8}},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user