Skip to content

Commit de17362

Browse files
Merge pull request #83 from macadmins/gg/thermal-power-tables
Add macos_thermal_pressure and macos_soc_power tables
2 parents a209518 + f49a544 commit de17362

12 files changed

Lines changed: 563 additions & 2 deletions

BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ go_library(
5252
"//tables/networkquality",
5353
"//tables/pendingappleupdates",
5454
"//tables/puppet",
55+
"//tables/socpower",
5556
"//tables/sofa",
57+
"//tables/thermalthrottling",
5658
"//tables/unifiedlog",
5759
"//tables/wifi_network",
5860
"@com_github_osquery_osquery_go//:osquery-go",

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ For production deployment, you should refer to the [osquery documentation](https
1919
| `alt_system_info` | Alternative system_info table | macOS | This table is an alternative to the built-in system_info table in osquery, which triggers an `Allow "osquery" to find devices on local networks?` prompt on macOS 15.0. On versions other than 15.0, this table falls back to the built-in system_info table. Note: this table returns an empty `cpu_subtype` field. See [#58](https://github.com/macadmins/osquery-extension/pull/58) for more details. |
2020
| `authdb` | macOS Authorization database | macOS | Use the constraint `name` to specify a right name to query, otherwise all rights will be returned. |
2121
| `crowdstrike_falcon` | Provides basic information about the currently installed Falcon sensor. | Linux / macOS | Requires Falcon to be installed. |
22-
| `energy_impact` | Process energy impact data from `powermetrics` | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 1000ms). |
22+
| `energy_impact` | Process energy impact data from `powermetrics` | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 1000). |
2323
| `file_lines` | Read an arbitrary file | Linux / macOS / Windows | Use the constraint `path` and `last` to specify the file to read lines from |
2424
| `filevault_users` | Information on the users able to unlock the current boot volume when encrypted with Filevault | macOS | |
2525
| `google_chrome_profiles` | Profiles configured in Google Chrome. | Linux / macOS / Windows | |
2626
| `local_network_permissions` | Local network permission state for applications | macOS | Shows apps that have responded to the "Allow [app] to find devices on local networks?" prompt. Reads from `/Library/Preferences/com.apple.networkextension.plist`. State values: 0 = denied, 1 = allowed. |
2727
| `macos_profiles` | High level information on installed profiles enrollment | macOS |
28+
| `macos_soc_power` | Power draw in milliwatts for the CPU, GPU, Apple Neural Engine (ANE), and total System on a Chip (SoC), plus GPU active ratio, sampled via `powermetrics` | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 3000). Longer intervals produce more accurate averages. Requires root. |
29+
| `macos_thermal_pressure` | Reports whether macOS is [thermally throttling](https://developer.apple.com/documentation/foundation/processinfo/thermalstate) the device, via `powermetrics`. Returns `thermal_pressure` (Nominal/Light/Moderate/Heavy/Sleeping) and a derived `is_throttling` integer (1 if not Nominal). | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 1000). Requires root. |
2830
| `mdm` | Information on the device's MDM enrollment | macOS | Code based on work by [Kolide](https://github.com/kolide/launcher). Due to changes in macOS 12.3, the output of `profiles show -type enrollment` can only be generated once a day. If you are running this command with another tool, you should set the `PROFILES_SHOW_ENROLLMENT_CACHE_PATH` environment variable to the path you are caching this. The cache file should be `json` with the keys `dep_capable` and `rate_limited` present, both booleans representing whether the device is capable of DEP enrollment and whether the response from `profiles show -type enrollment` is being rate limited or not. |
2931
| `munki_info` | Information from the last [Munki](https://github.com/munki/munki) run | macOS | Code based on work by [Kolide](https://github.com/kolide/launcher) |
3032
| `munki_installs` | Items [Munki](https://github.com/munki/munki) is managing | macOS | Code based on work by [Kolide](https://github.com/kolide/launcher) |

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.3.2
1+
1.4.0

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"github.com/macadmins/osquery-extension/tables/networkquality"
2222
"github.com/macadmins/osquery-extension/tables/pendingappleupdates"
2323
"github.com/macadmins/osquery-extension/tables/puppet"
24+
"github.com/macadmins/osquery-extension/tables/socpower"
2425
"github.com/macadmins/osquery-extension/tables/sofa"
26+
"github.com/macadmins/osquery-extension/tables/thermalthrottling"
2527
"github.com/macadmins/osquery-extension/tables/unifiedlog"
2628
"github.com/macadmins/osquery-extension/tables/wifi_network"
2729

@@ -123,6 +125,8 @@ func main() {
123125
return alt_system_info.AltSystemInfoGenerate(ctx, queryContext, *flSocketPath)
124126
},
125127
),
128+
table.NewPlugin("macos_thermal_pressure", thermalthrottling.ThermalPressureColumns(), thermalthrottling.ThermalPressureGenerate),
129+
table.NewPlugin("macos_soc_power", socpower.SocPowerColumns(), socpower.SocPowerGenerate),
126130
}
127131
plugins = append(plugins, darwinPlugins...)
128132
}

tables/socpower/BUILD.bazel

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "socpower",
5+
srcs = ["soc_power.go"],
6+
importpath = "github.com/macadmins/osquery-extension/tables/socpower",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//pkg/utils",
10+
"@com_github_micromdm_plist//:plist",
11+
"@com_github_osquery_osquery_go//plugin/table",
12+
"@com_github_pkg_errors//:errors",
13+
],
14+
)
15+
16+
go_test(
17+
name = "socpower_test",
18+
srcs = ["soc_power_test.go"],
19+
embed = [":socpower"],
20+
embedsrcs = ["test_powermetrics_output.plist"],
21+
deps = [
22+
"//pkg/utils",
23+
"@com_github_stretchr_testify//assert",
24+
],
25+
)

tables/socpower/soc_power.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package socpower
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strconv"
8+
9+
"github.com/macadmins/osquery-extension/pkg/utils"
10+
"github.com/micromdm/plist"
11+
"github.com/osquery/osquery-go/plugin/table"
12+
"github.com/pkg/errors"
13+
)
14+
15+
const defaultInterval = 3000
16+
17+
type powermetricsOutput struct {
18+
Processor struct {
19+
CPUPower float64 `plist:"cpu_power"`
20+
GPUPower float64 `plist:"gpu_power"`
21+
ANEPower float64 `plist:"ane_power"`
22+
CombinedPower float64 `plist:"combined_power"`
23+
} `plist:"processor"`
24+
GPU struct {
25+
IdleRatio float64 `plist:"idle_ratio"`
26+
} `plist:"gpu"`
27+
}
28+
29+
func SocPowerColumns() []table.ColumnDefinition {
30+
return []table.ColumnDefinition{
31+
table.TextColumn("cpu_power_mw"),
32+
table.TextColumn("gpu_power_mw"),
33+
table.TextColumn("ane_power_mw"),
34+
table.TextColumn("combined_power_mw"),
35+
table.TextColumn("gpu_active_ratio"),
36+
table.IntegerColumn("interval"),
37+
}
38+
}
39+
40+
func SocPowerGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
41+
interval := defaultInterval
42+
if constraintList, present := queryContext.Constraints["interval"]; present {
43+
for _, constraint := range constraintList.Constraints {
44+
if constraint.Operator == table.OperatorEquals {
45+
if parsed, err := strconv.Atoi(constraint.Expression); err == nil {
46+
interval = parsed
47+
}
48+
}
49+
}
50+
}
51+
52+
r := utils.NewRunner()
53+
fs := utils.OSFileSystem{}
54+
result, err := runPowermetrics(r, fs, interval)
55+
if err != nil {
56+
fmt.Println(err)
57+
return nil, err
58+
}
59+
if result == nil {
60+
return nil, nil
61+
}
62+
63+
gpuActiveRatio := 1.0 - result.GPU.IdleRatio
64+
65+
return []map[string]string{{
66+
"cpu_power_mw": fmt.Sprintf("%.2f", result.Processor.CPUPower),
67+
"gpu_power_mw": fmt.Sprintf("%.2f", result.Processor.GPUPower),
68+
"ane_power_mw": fmt.Sprintf("%.2f", result.Processor.ANEPower),
69+
"combined_power_mw": fmt.Sprintf("%.2f", result.Processor.CombinedPower),
70+
"gpu_active_ratio": fmt.Sprintf("%.4f", gpuActiveRatio),
71+
"interval": strconv.Itoa(interval),
72+
}}, nil
73+
}
74+
75+
func runPowermetrics(r utils.Runner, fs utils.FileSystem, interval int) (*powermetricsOutput, error) {
76+
_, err := fs.Stat("/usr/bin/powermetrics")
77+
if err != nil {
78+
if errors.Is(err, os.ErrNotExist) {
79+
return nil, nil
80+
}
81+
return nil, err
82+
}
83+
84+
out, err := r.Runner.RunCmd(
85+
"/usr/bin/powermetrics",
86+
"-f", "plist",
87+
"-n", "1",
88+
"-i", strconv.Itoa(interval),
89+
"--samplers", "cpu_power,gpu_power,ane_power",
90+
)
91+
if err != nil {
92+
return nil, errors.Wrap(err, "running powermetrics")
93+
}
94+
95+
var output powermetricsOutput
96+
if err := plist.Unmarshal(out, &output); err != nil {
97+
return nil, errors.Wrap(err, "unmarshalling powermetrics output")
98+
}
99+
100+
return &output, nil
101+
}

tables/socpower/soc_power_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package socpower
2+
3+
import (
4+
_ "embed"
5+
"errors"
6+
"testing"
7+
8+
"github.com/macadmins/osquery-extension/pkg/utils"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
//go:embed test_powermetrics_output.plist
13+
var testPlist string
14+
15+
func TestSocPowerColumns(t *testing.T) {
16+
columns := SocPowerColumns()
17+
18+
assert.Len(t, columns, 6)
19+
20+
columnNames := make(map[string]bool)
21+
for _, col := range columns {
22+
columnNames[col.Name] = true
23+
}
24+
25+
for _, name := range []string{"cpu_power_mw", "gpu_power_mw", "ane_power_mw", "combined_power_mw", "gpu_active_ratio", "interval"} {
26+
assert.True(t, columnNames[name], "expected column %s not found", name)
27+
}
28+
}
29+
30+
func TestRunPowermetrics(t *testing.T) {
31+
tests := []struct {
32+
name string
33+
mockCmd utils.MockCmdRunner
34+
fileExists bool
35+
interval int
36+
wantErr bool
37+
wantNil bool
38+
checkResult func(t *testing.T, result *powermetricsOutput)
39+
}{
40+
{
41+
name: "Binary not present",
42+
fileExists: false,
43+
interval: 3000,
44+
wantNil: true,
45+
},
46+
{
47+
name: "Successful execution",
48+
mockCmd: utils.MockCmdRunner{
49+
Output: testPlist,
50+
},
51+
fileExists: true,
52+
interval: 3000,
53+
checkResult: func(t *testing.T, result *powermetricsOutput) {
54+
assert.InDelta(t, 9924.35, result.Processor.CPUPower, 0.01)
55+
assert.InDelta(t, 169.958, result.Processor.GPUPower, 0.01)
56+
assert.InDelta(t, 0.0, result.Processor.ANEPower, 0.01)
57+
assert.InDelta(t, 10094.3, result.Processor.CombinedPower, 0.01)
58+
assert.InDelta(t, 0.901357, result.GPU.IdleRatio, 0.0001)
59+
},
60+
},
61+
{
62+
name: "Command execution error",
63+
mockCmd: utils.MockCmdRunner{
64+
Err: errors.New("command error"),
65+
},
66+
fileExists: true,
67+
interval: 3000,
68+
wantErr: true,
69+
},
70+
{
71+
name: "Invalid plist output",
72+
mockCmd: utils.MockCmdRunner{
73+
Output: "invalid plist data",
74+
},
75+
fileExists: true,
76+
interval: 3000,
77+
wantErr: true,
78+
},
79+
{
80+
name: "Custom interval",
81+
mockCmd: utils.MockCmdRunner{
82+
Output: testPlist,
83+
},
84+
fileExists: true,
85+
interval: 5000,
86+
checkResult: func(t *testing.T, result *powermetricsOutput) {
87+
assert.InDelta(t, 9924.35, result.Processor.CPUPower, 0.01)
88+
},
89+
},
90+
}
91+
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
runner := utils.Runner{Runner: tt.mockCmd}
95+
fs := utils.MockFileSystem{FileExists: tt.fileExists}
96+
97+
result, err := runPowermetrics(runner, fs, tt.interval)
98+
99+
if tt.wantErr {
100+
assert.Error(t, err)
101+
assert.Nil(t, result)
102+
return
103+
}
104+
105+
assert.NoError(t, err)
106+
107+
if tt.wantNil {
108+
assert.Nil(t, result)
109+
return
110+
}
111+
112+
if tt.checkResult != nil {
113+
tt.checkResult(t, result)
114+
}
115+
})
116+
}
117+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>is_delta</key>
6+
<true/>
7+
<key>elapsed_ns</key>
8+
<integer>3017304750</integer>
9+
<key>processor</key>
10+
<dict>
11+
<key>cpu_power</key>
12+
<real>9924.35</real>
13+
<key>gpu_power</key>
14+
<real>169.958</real>
15+
<key>ane_power</key>
16+
<real>0.0</real>
17+
<key>combined_power</key>
18+
<real>10094.3</real>
19+
</dict>
20+
<key>gpu</key>
21+
<dict>
22+
<key>idle_ratio</key>
23+
<real>0.901357</real>
24+
</dict>
25+
</dict>
26+
</plist>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "thermalthrottling",
5+
srcs = ["thermal_throttling.go"],
6+
importpath = "github.com/macadmins/osquery-extension/tables/thermalthrottling",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//pkg/utils",
10+
"@com_github_micromdm_plist//:plist",
11+
"@com_github_osquery_osquery_go//plugin/table",
12+
"@com_github_pkg_errors//:errors",
13+
],
14+
)
15+
16+
go_test(
17+
name = "thermalthrottling_test",
18+
srcs = ["thermal_throttling_test.go"],
19+
embed = [":thermalthrottling"],
20+
embedsrcs = ["test_powermetrics_output.plist"],
21+
deps = [
22+
"//pkg/utils",
23+
"@com_github_osquery_osquery_go//plugin/table",
24+
"@com_github_stretchr_testify//assert",
25+
],
26+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>is_delta</key>
6+
<true/>
7+
<key>elapsed_ns</key>
8+
<integer>1017304750</integer>
9+
<key>hw_model</key>
10+
<string>Mac16,6</string>
11+
<key>thermal_pressure</key>
12+
<string>Nominal</string>
13+
</dict>
14+
</plist>

0 commit comments

Comments
 (0)