Windows Battery Status (#22455)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Windows host details now include battery status
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -555,7 +555,7 @@ 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,
|
||||
@@ -563,6 +563,12 @@ var extraDetailQueries = map[string]DetailQuery{
|
||||
// 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
@@ -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")
|
||||
@@ -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{
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user