Skip to content

Commit

Permalink
Windows Battery Status (#22455)
Browse files Browse the repository at this point in the history
  • Loading branch information
mostlikelee authored Sep 30, 2024
1 parent 658431e commit 937627f
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 15 deletions.
1 change: 1 addition & 0 deletions changes/19619-win-battery
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Windows host details now include battery status
16 changes: 15 additions & 1 deletion docs/Contributing/Understanding-host-vitals.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Following is a summary of the detail queries hardcoded in Fleet used to populate the device details:

## battery
## battery_macos

- Platforms: darwin

Expand All @@ -12,6 +12,20 @@ Following is a summary of the detail queries hardcoded in Fleet used to populate
SELECT serial_number, cycle_count, health FROM battery;
```

## battery_windows

- Platforms: windows

- Discovery query:
```sql
SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'battery'
```

- Query:
```sql
SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery
```

## chromeos_profile_user_info

- Platforms: chrome
Expand Down
1 change: 1 addition & 0 deletions server/service/osquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) {
hostDetailQueryPrefix + "orbit_info": {},
hostDetailQueryPrefix + "software_vscode_extensions": {},
hostDetailQueryPrefix + "software_macos_firefox": {},
hostDetailQueryPrefix + "battery_windows": {},
}
for name := range queries {
require.NotEmpty(t, discovery[name])
Expand Down
75 changes: 64 additions & 11 deletions server/service/osquery_utils/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,14 +555,20 @@ var extraDetailQueries = map[string]DetailQuery{
DirectIngestFunc: directIngestChromeProfiles,
Discovery: discoveryTable("google_chrome_profiles"),
},
"battery": {
"battery_macos": {
Query: `SELECT serial_number, cycle_count, health FROM battery;`,
Platforms: []string{"darwin"},
DirectIngestFunc: directIngestBattery,
// the "battery" table doesn't need a Discovery query as it is an official
// osquery table on darwin (https://osquery.io/schema/5.3.0#battery), it is
// always present.
},
"battery_windows": {
Query: `SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery`,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestBattery,
Discovery: discoveryTable("battery"), // added to Windows in v5.12.1 (https://github.com/osquery/osquery/releases/tag/5.12.1)
},
"os_windows": {
// This query is used to populate the `operating_systems` and `host_operating_system`
// tables. Separately, the `hosts` table is populated via the `os_version` and
Expand Down Expand Up @@ -1297,23 +1303,70 @@ func directIngestChromeProfiles(ctx context.Context, logger log.Logger, host *fl
func directIngestBattery(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
mapping := make([]*fleet.HostBattery, 0, len(rows))
for _, row := range rows {
cycleCount, err := strconv.ParseInt(EmptyToZero(row["cycle_count"]), 10, 64)
cycleCount, err := strconv.Atoi(EmptyToZero(row["cycle_count"]))
if err != nil {
return err
}
mapping = append(mapping, &fleet.HostBattery{
HostID: host.ID,
SerialNumber: row["serial_number"],
CycleCount: int(cycleCount),
// database type is VARCHAR(40) and since there isn't a
// canonical list of strings we can get for health, we
// truncate the value just in case.
Health: fmt.Sprintf("%.40s", row["health"]),
})

switch host.Platform {
case "darwin":
mapping = append(mapping, &fleet.HostBattery{
HostID: host.ID,
SerialNumber: row["serial_number"],
CycleCount: cycleCount,
// database type is VARCHAR(40) and since there isn't a
// canonical list of strings we can get for health, we
// truncate the value just in case.
Health: fmt.Sprintf("%.40s", row["health"]),
})
case "windows":
health, err := generateWindowsBatteryHealth(row["designed_capacity"], row["max_capacity"])
if err != nil {
level.Error(logger).Log("op", "directIngestBattery", "hostID", host.ID, "err", err)
}

mapping = append(mapping, &fleet.HostBattery{
HostID: host.ID,
SerialNumber: row["serial_number"],
CycleCount: cycleCount,
Health: health,
})
}
}
return ds.ReplaceHostBatteries(ctx, host.ID, mapping)
}

const (
batteryStatusUnknown = "Unknown"
batteryStatusDegraded = "Check Battery"
batteryStatusGood = "Good"
batteryDegradedThreshold = 80
)

func generateWindowsBatteryHealth(designedCapacity, maxCapacity string) (string, error) {
if designedCapacity == "" || maxCapacity == "" {
return batteryStatusUnknown, fmt.Errorf("missing battery capacity values, designed: %s, max: %s", designedCapacity, maxCapacity)
}

designed, err := strconv.ParseInt(designedCapacity, 10, 64)
if err != nil {
return batteryStatusUnknown, err
}

max, err := strconv.ParseInt(maxCapacity, 10, 64)
if err != nil {
return batteryStatusUnknown, err
}

health := float64(max) / float64(designed) * 100

if health < batteryDegradedThreshold {
return batteryStatusDegraded, nil
}

return batteryStatusGood, nil
}

func directIngestWindowsUpdateHistory(
ctx context.Context,
logger log.Logger,
Expand Down
39 changes: 36 additions & 3 deletions server/service/osquery_utils/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ func TestGetDetailQueries(t *testing.T) {
"mdm_windows",
"munki_info",
"google_chrome_profiles",
"battery",
"battery_macos",
"battery_windows",
"os_windows",
"os_unix_like",
"os_chrome",
Expand All @@ -296,7 +297,7 @@ func TestGetDetailQueries(t *testing.T) {
sortedKeysCompare(t, queriesNoConfig, baseQueries)

queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil)
require.Len(t, queriesWithoutWinOSVuln, 25)
require.Len(t, queriesWithoutWinOSVuln, 26)

queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true})
qs := append(baseQueries, "users", "users_chrome", "scheduled_query_stats")
Expand Down Expand Up @@ -984,7 +985,8 @@ func TestDirectIngestBattery(t *testing.T) {
}

host := fleet.Host{
ID: 1,
ID: 1,
Platform: "darwin",
}

err := directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
Expand All @@ -994,6 +996,37 @@ func TestDirectIngestBattery(t *testing.T) {

require.NoError(t, err)
require.True(t, ds.ReplaceHostBatteriesFuncInvoked)

ds.ReplaceHostBatteriesFunc = func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error {
require.Equal(t, mappings, []*fleet.HostBattery{
{HostID: uint(2), SerialNumber: "a", CycleCount: 2, Health: batteryStatusGood},
{HostID: uint(2), SerialNumber: "b", CycleCount: 3, Health: batteryStatusDegraded},
{HostID: uint(2), SerialNumber: "c", CycleCount: 4, Health: batteryStatusUnknown},
{HostID: uint(2), SerialNumber: "d", CycleCount: 5, Health: batteryStatusUnknown},
{HostID: uint(2), SerialNumber: "e", CycleCount: 6, Health: batteryStatusUnknown},
{HostID: uint(2), SerialNumber: "f", CycleCount: 7, Health: batteryStatusUnknown},
})
return nil
}

// reset the ds flag
ds.ReplaceHostBatteriesFuncInvoked = false

host = fleet.Host{
ID: 2,
Platform: "windows",
}

err = directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
{"serial_number": "a", "cycle_count": "2", "designed_capacity": "3000", "max_capacity": "2400"}, // max_capacity >= 80%
{"serial_number": "b", "cycle_count": "3", "designed_capacity": "3000", "max_capacity": "2399"}, // max_capacity < 50%
{"serial_number": "c", "cycle_count": "4", "designed_capacity": "3000", "max_capacity": ""}, // missing max_capacity
{"serial_number": "d", "cycle_count": "5", "designed_capacity": "", "max_capacity": ""}, // missing designed_capacity and max_capacity
{"serial_number": "e", "cycle_count": "6", "designed_capacity": "", "max_capacity": "2000"}, // missing designed_capacity
{"serial_number": "f", "cycle_count": "7", "designed_capacity": "foo", "max_capacity": "bar"}, // invalid designed_capacity and max_capacity
})
require.NoError(t, err)
require.True(t, ds.ReplaceHostBatteriesFuncInvoked)
}

func TestDirectIngestOSWindows(t *testing.T) {
Expand Down

0 comments on commit 937627f

Please sign in to comment.