Skip to content

Commit

Permalink
Implement battery table for Windows (#8267)
Browse files Browse the repository at this point in the history
Almost all of the columns are able to line up with those already available on macOS.

This has been tested on a single Windows 11 Pro laptop. All columns look correct except for `cycle_count` in which this device reports `0`. We seem to be using the correct API though and will hopefully get values on other devices.

Testing also performed on a macOS laptop with Windows 10/11 in VMs - the table successfully reports the VMWare virtual battery when that is configured and no results (along with appropriate logging) when there is no virtual battery.
  • Loading branch information
zwass committed Mar 1, 2024
1 parent 01ed1f6 commit fdabe5a
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 8 deletions.
1 change: 1 addition & 0 deletions osquery/tables/system/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ function(generateOsqueryTablesSystemSystemtable)
windows/authenticode.cpp
windows/autoexec.cpp
windows/background_activities_moderator.cpp
windows/battery.cpp
windows/bitlocker_info.cpp
windows/certificates.cpp
windows/chassis_info.cpp
Expand Down
264 changes: 264 additions & 0 deletions osquery/tables/system/windows/battery.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/**
* Copyright (c) 2014-present, The osquery authors
*
* This source code is licensed as defined by the LICENSE file found in the
* root directory of this source tree.
*
* SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only)
*/

#include <string>

// clang-format off
#include <windows.h>
#include <ioapiset.h>
#include <Batclass.h>
#include <Poclass.h>
#include <setupapi.h>
#include <devguid.h>
#include <devioctl.h>
// clang-format on

#include <osquery/logger/logger.h>
#include <osquery/sql/sql.h>
#include <osquery/utils/conversions/windows/strings.h>
#include <osquery/utils/scope_guard.h>
#include <osquery/utils/system/errno.h>

namespace osquery {
namespace tables {

std::string batteryQueryInformationString(
HANDLE hBattery,
ULONG batteryTag,
BATTERY_QUERY_INFORMATION_LEVEL informationLevel) {
BATTERY_QUERY_INFORMATION bqi = {0};
bqi.InformationLevel = informationLevel;
bqi.BatteryTag = batteryTag;
// 1025 characters should be way more than enough for the values retrieved
// from this function. It shouldn't overflow anyway due to providing size in
// the DeviceIoControl call.
std::wstring resWstring(1025, L'\0');
DWORD resSize(0);

if (!DeviceIoControl(hBattery,
IOCTL_BATTERY_QUERY_INFORMATION,
&bqi,
sizeof(bqi),
resWstring.data(),
static_cast<DWORD>(resWstring.size()) * sizeof(wchar_t),
nullptr,
nullptr)) {
if (ERROR_INVALID_FUNCTION == GetLastError()) {
LOG(INFO) << "Battery does not support information level "
<< informationLevel;
} else {
LOG(ERROR) << "Failed to get battery information level "
<< informationLevel << ": code " << GetLastError();
}
return "";
}

return wstringToString(resWstring);
}

QueryData genBatteryInfo(QueryContext& context) {
QueryData results;

// Adapted from Microsoft example:
// https://learn.microsoft.com/en-us/windows/win32/power/enumerating-battery-devices
// Enumerate the batteries and ask each one for information.
HDEVINFO hdev = SetupDiGetClassDevs(
&GUID_DEVCLASS_BATTERY, 0, 0, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (hdev == INVALID_HANDLE_VALUE) {
LOG(ERROR) << "Failed to initialize handle for enumerating batteries: "
<< GetLastError();
return results;
}
auto const hdevGuard =
scope_guard::create([&]() { SetupDiDestroyDeviceInfoList(hdev); });

// Limit search to 100 batteries max
for (int idev = 0; idev < 100; idev++) {
SP_DEVICE_INTERFACE_DATA did = {0};
did.cbSize = sizeof(did);

if (!SetupDiEnumDeviceInterfaces(
hdev, 0, &GUID_DEVCLASS_BATTERY, idev, &did)) {
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
// Only log if it's an unexpected error
LOG(ERROR) << "Failed to set up enumeration for batteries: "
<< GetLastError();
}
break;
}

DWORD cbRequired = 0;
SetupDiGetDeviceInterfaceDetail(hdev, &did, 0, 0, &cbRequired, 0);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
LOG(ERROR)
<< "Failed to get buffer size for get device interface detail: "
<< GetLastError();
continue;
}

PSP_DEVICE_INTERFACE_DETAIL_DATA pdidd =
(PSP_DEVICE_INTERFACE_DETAIL_DATA)LocalAlloc(LPTR, cbRequired);
if (pdidd == nullptr) {
LOG(ERROR) << "Failed to allocate buffer for device interface detail: "
<< GetLastError();
continue;
}
auto const pdiddGuard = scope_guard::create([&]() { LocalFree(pdidd); });

pdidd->cbSize = sizeof(*pdidd);
if (!SetupDiGetDeviceInterfaceDetail(
hdev, &did, pdidd, cbRequired, &cbRequired, 0)) {
LOG(ERROR) << "Failed to get battery device detail: " << GetLastError();
continue;
}

// Enumerated a battery. Ask it for information.
HANDLE hBattery = CreateFile(pdidd->DevicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (hBattery == INVALID_HANDLE_VALUE) {
LOG(ERROR) << "Failed to open handle for battery device "
<< wstringToString(pdidd->DevicePath) << ": "
<< GetLastError();
continue;
}
auto const hBatteryGuard =
scope_guard::create([&]() { CloseHandle(hBattery); });

// Ask the battery for its tag - needed for later queries
BATTERY_QUERY_INFORMATION bqi = {0};
DWORD dwWait = 0; // do not wait for a battery, return immediately
DWORD dwOut;
if (!(DeviceIoControl(hBattery,
IOCTL_BATTERY_QUERY_TAG,
&dwWait,
sizeof(dwWait),
&bqi.BatteryTag,
sizeof(bqi.BatteryTag),
&dwOut,
nullptr) &&
bqi.BatteryTag)) {
LOG(ERROR) << "Failed to get tag for battery device "
<< wstringToString(pdidd->DevicePath) << ": "
<< GetLastError();
continue;
}

BATTERY_INFORMATION bi = {0};
bqi.InformationLevel = BatteryInformation;
if (DeviceIoControl(hBattery,
IOCTL_BATTERY_QUERY_INFORMATION,
&bqi,
sizeof(bqi),
&bi,
sizeof(bi),
&dwOut,
nullptr)) {
// Only non-UPS system batteries count
if (!(bi.Capabilities & BATTERY_SYSTEM_BATTERY) ||
(bi.Capabilities & BATTERY_IS_SHORT_TERM)) {
continue;
}

if (bi.Capabilities & BATTERY_CAPACITY_RELATIVE) {
LOG(WARNING) << "Battery is reporting in unknown (relative) units. "
"Values may not be in mAh, mA, and mV.";
}

Row row;

// Some possible values for chemistry, though we already have
// seen LiP which is not listed
// https://learn.microsoft.com/en-us/windows/win32/power/battery-information-str
row["chemistry"] = SQL_TEXT(bi.Chemistry);

// Assume that 12 volts is the intended voltage for the
// battery in order to convert from the mWh units that
// Microsoft provides to match the mAh units that the battery
// table already uses for macOS.
const int designedVoltage = 12;
row["max_capacity"] = INTEGER(bi.FullChargedCapacity / designedVoltage);
row["designed_capacity"] = INTEGER(bi.DesignedCapacity / designedVoltage);
if (bi.CycleCount != 0) {
row["cycle_count"] = INTEGER(bi.CycleCount);
}

// Query the battery power status.
BATTERY_WAIT_STATUS bws = {0};
bws.BatteryTag = bqi.BatteryTag;
BATTERY_STATUS bs;
if (DeviceIoControl(hBattery,
IOCTL_BATTERY_QUERY_STATUS,
&bws,
sizeof(bws),
&bs,
sizeof(bs),
&dwOut,
nullptr)) {
// https://learn.microsoft.com/en-us/windows/win32/power/battery-wait-status-str
if (bs.PowerState & BATTERY_POWER_ON_LINE) {
row["state"] = "AC Power";
row["charging"] = INTEGER((bs.PowerState & BATTERY_CHARGING) > 0);
} else if (bs.PowerState & BATTERY_DISCHARGING) {
row["state"] = "Battery Power";
row["charging"] = INTEGER(0);
}
row["charged"] = INTEGER(bs.Capacity == bi.FullChargedCapacity);
row["current_capacity"] = INTEGER(bs.Capacity / designedVoltage);
row["voltage"] = INTEGER(bs.Voltage);
if (bs.Voltage > 0) {
row["amperage"] = INTEGER((1000 * static_cast<int>(bs.Rate)) /
static_cast<int>(bs.Voltage));
} else {
LOG(WARNING) << "Battery table read a voltage of 0.";
}
if (bs.Capacity != bi.FullChargedCapacity && bs.Rate > 0) {
row["minutes_to_full_charge"] =
INTEGER(60 * (bi.FullChargedCapacity - bs.Capacity) / bs.Rate);
}
}

SYSTEM_POWER_STATUS sps;
if (GetSystemPowerStatus(&sps)) {
if (sps.BatteryLifePercent != -1) {
row["percent_remaining"] =
INTEGER((unsigned int)sps.BatteryLifePercent);
}
if (sps.BatteryLifeTime != -1) {
row["minutes_until_empty"] =
INTEGER(sps.BatteryLifeTime / 60); // convert seconds to minutes
}
} else {
LOG(WARNING) << "Failed to get system power status";
}

row["manufacturer"] = batteryQueryInformationString(
hBattery, bqi.BatteryTag, BatteryManufactureName);

row["serial_number"] = batteryQueryInformationString(
hBattery, bqi.BatteryTag, BatterySerialNumber);

row["model"] = batteryQueryInformationString(
hBattery, bqi.BatteryTag, BatteryDeviceName);

results.push_back(row);
}
}

if (results.empty()) {
VLOG(1) << "Battery table did not find a system battery";
}
return results;
}
} // namespace tables
} // namespace osquery
2 changes: 1 addition & 1 deletion specs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ function(generateNativeTables)
"darwin/asl.table:macos"
"darwin/authorization_mechanisms.table:macos"
"darwin/authorizations.table:macos"
"darwin/battery.table:macos"
"darwin/browser_plugins.table:macos"
"darwin/connected_displays.table:macos"
"darwin/crashes.table:macos"
Expand Down Expand Up @@ -155,6 +154,7 @@ function(generateNativeTables)
"darwin/xprotect_entries.table:macos"
"darwin/xprotect_meta.table:macos"
"darwin/xprotect_reports.table:macos"
"darwindows/battery.table:macos,windows"
"linux/apparmor_events.table:linux"
"linux/apparmor_profiles.table:linux"
"linux/apt_sources.table:linux"
Expand Down
19 changes: 12 additions & 7 deletions specs/darwin/battery.table → specs/darwindows/battery.table
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
table_name("battery")
description("Provides information about the internal battery of a Macbook.")
description("Provides information about the internal battery of a laptop. Note: On Windows, columns with Ah or mAh units assume that the battery is 12V.")
schema([
Column("manufacturer", TEXT, "The battery manufacturer's name"),
Column("manufacture_date", INTEGER, "The date the battery was manufactured UNIX Epoch"),
Column("model", TEXT, "The battery's model number"),
Column("serial_number", TEXT, "The battery's unique serial number"),
Column("serial_number", TEXT, "The battery's serial number"),
Column("cycle_count", INTEGER, "The number of charge/discharge cycles"),
Column("health", TEXT, "One of the following: \"Good\" describes a well-performing battery, \"Fair\" describes a functional battery with limited capacity, or \"Poor\" describes a battery that's not capable of providing power"),
Column("condition", TEXT, "One of the following: \"Normal\" indicates the condition of the battery is within normal tolerances, \"Service Needed\" indicates that the battery should be checked out by a licensed Mac repair service, \"Permanent Failure\" indicates the battery needs replacement"),
Column("state", TEXT, "One of the following: \"AC Power\" indicates the battery is connected to an external power source, \"Battery Power\" indicates that the battery is drawing internal power, \"Off Line\" indicates the battery is off-line or no longer connected"),
Column("charging", INTEGER, "1 if the battery is currently being charged by a power source. 0 otherwise"),
Column("charged", INTEGER, "1 if the battery is currently completely charged. 0 otherwise"),
Column("designed_capacity", INTEGER, "The battery's designed capacity in mAh"),
Column("max_capacity", INTEGER, "The battery's actual capacity when it is fully charged in mAh"),
Column("current_capacity", INTEGER, "The battery's current charged capacity in mAh"),
Column("current_capacity", INTEGER, "The battery's current capacity (level of charge) in mAh"),
Column("percent_remaining", INTEGER, "The percentage of battery remaining before it is drained"),
Column("amperage", INTEGER, "The current amperage in/out of the battery in mA (positive means charging, negative means discharging)"),
Column("voltage", INTEGER, "The battery's current voltage in mV"),
Column("minutes_until_empty", INTEGER, "The number of minutes until the battery is fully depleted. This value is -1 if this time is still being calculated"),
Column("minutes_to_full_charge", INTEGER, "The number of minutes until the battery is fully charged. This value is -1 if this time is still being calculated"),
Column("minutes_to_full_charge", INTEGER, "The number of minutes until the battery is fully charged. This value is -1 if this time is still being calculated. On Windows this is calculated from the charge rate and capacity and may not agree with the number reported in \"Power & Battery\""),
])
extended_schema(WINDOWS, [
Column("chemistry", TEXT, "The battery chemistry type (eg. LiP). Some possible values are documented in https://learn.microsoft.com/en-us/windows/win32/power/battery-information-str."),
])
extended_schema(DARWIN, [
Column("health", TEXT, "One of the following: \"Good\" describes a well-performing battery, \"Fair\" describes a functional battery with limited capacity, or \"Poor\" describes a battery that's not capable of providing power"),
Column("condition", TEXT, "One of the following: \"Normal\" indicates the condition of the battery is within normal tolerances, \"Service Needed\" indicates that the battery should be checked out by a licensed Mac repair service, \"Permanent Failure\" indicates the battery needs replacement"),
Column("manufacture_date", INTEGER, "The date the battery was manufactured UNIX Epoch"),
])
implementation("battery@genBatteryInfo")
1 change: 1 addition & 0 deletions tests/integration/tables/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ function(generateTestsIntegrationTablesTestsTest)
authenticode.cpp
autoexec.cpp
background_activities_moderator.cpp
battery.cpp
bitlocker_info.cpp
connectivity.cpp
windows_firewall_rules.cpp
Expand Down

0 comments on commit fdabe5a

Please sign in to comment.