Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ config.mk
bazel-*
coverage/*
version_config.go
cover.out
cover.out
.claude/settings.local.json
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ go_library(
"//tables/authdb",
"//tables/chromeuserprofiles",
"//tables/crowdstrike_falcon",
"//tables/energyimpact",
"//tables/fileline",
"//tables/filevaultusers",
"//tables/localnetworkpermissions",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ For production deployment, you should refer to the [osquery documentation](https
|------------------------------| --------------------------------------------------------------------------------------------- |-------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `authdb` | macOS Authorization database | macOS | Use the constraint `name` to specify a right name to query, otherwise all rights will be returned. |
| `crowdstrike_falcon` | Provides basic information about the currently installed Falcon sensor. | Linux / macOS | Requires Falcon to be installed. |
| `crowdstrike_falcon` | Provides basic information about the currently installed Falcon sensor. | Linux / macOS | Requires Falcon to be installed. |
| `energy_impact` | Process energy impact data from `powermetrics` | macOS | Use the `interval` constraint to specify sampling duration in milliseconds (default: 1000ms). |
| `file_lines` | Read an arbitrary file | Linux / macOS / Windows | Use the constraint `path` and `last` to specify the file to read lines from |
| `filevault_users` | Information on the users able to unlock the current boot volume when encrypted with Filevault | macOS | |
| `google_chrome_profiles` | Profiles configured in Google Chrome. | Linux / macOS / Windows | |
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.1
1.3.2
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/macadmins/osquery-extension/tables/alt_system_info"
"github.com/macadmins/osquery-extension/tables/chromeuserprofiles"
"github.com/macadmins/osquery-extension/tables/crowdstrike_falcon"
"github.com/macadmins/osquery-extension/tables/energyimpact"
"github.com/macadmins/osquery-extension/tables/fileline"
"github.com/macadmins/osquery-extension/tables/filevaultusers"
"github.com/macadmins/osquery-extension/tables/localnetworkpermissions"
Expand Down Expand Up @@ -92,6 +93,7 @@ func main() {

if runtime.GOOS == "darwin" {
darwinPlugins := []osquery.OsqueryPlugin{
table.NewPlugin("energy_impact", energyimpact.EnergyImpactColumns(), energyimpact.EnergyImpactGenerate),
table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate),
table.NewPlugin("local_network_permissions", localnetworkpermissions.LocalNetworkPermissionsColumns(), localnetworkpermissions.LocalNetworkPermissionsGenerate),
table.NewPlugin("macos_profiles", macosprofiles.MacOSProfilesColumns(), macosprofiles.MacOSProfilesGenerate),
Expand Down
26 changes: 26 additions & 0 deletions tables/energyimpact/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "energyimpact",
srcs = ["energy_impact.go"],
importpath = "github.com/macadmins/osquery-extension/tables/energyimpact",
visibility = ["//visibility:public"],
deps = [
"//pkg/utils",
"@com_github_micromdm_plist//:plist",
"@com_github_osquery_osquery_go//plugin/table",
"@com_github_pkg_errors//:errors",
],
)

go_test(
name = "energyimpact_test",
srcs = ["energy_impact_test.go"],
embed = [":energyimpact"],
embedsrcs = ["test_powermetrics_output.plist"],
deps = [
"//pkg/utils",
"@com_github_osquery_osquery_go//plugin/table",
"@com_github_stretchr_testify//assert",
],
)
155 changes: 155 additions & 0 deletions tables/energyimpact/energy_impact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package energyimpact

import (
"context"
"fmt"
"os"
"strconv"

"github.com/macadmins/osquery-extension/pkg/utils"
"github.com/micromdm/plist"
"github.com/osquery/osquery-go/plugin/table"
"github.com/pkg/errors"
)

const defaultInterval = 1000

// powermetricsOutput represents the top-level plist structure from powermetrics
type powermetricsOutput struct {
Tasks []task `plist:"tasks"`
}

// task represents individual process data from powermetrics
type task struct {
PID int `plist:"pid"`
Name string `plist:"name"`
EnergyImpact float64 `plist:"energy_impact"`
EnergyImpactPerS float64 `plist:"energy_impact_per_s"`
CPUTimeNS int64 `plist:"cputime_ns"`
CPUTimeMSPerS float64 `plist:"cputime_ms_per_s"`
CPUTimeUserlandRatio float64 `plist:"cputime_userland_ratio"`
IntrWakeups int `plist:"intr_wakeups"`
IntrWakeupsPerS float64 `plist:"intr_wakeups_per_s"`
IdleWakeups int `plist:"idle_wakeups"`
IdleWakeupsPerS float64 `plist:"idle_wakeups_per_s"`
DiskIOBytesRead int64 `plist:"diskio_bytesread"`
DiskIOBytesReadPerS float64 `plist:"diskio_bytesread_per_s"`
DiskIOBytesWritten int64 `plist:"diskio_byteswritten"`
DiskIOBytesWrittenPerS float64 `plist:"diskio_byteswritten_per_s"`
PacketsReceived int `plist:"packets_received"`
PacketsSent int `plist:"packets_sent"`
BytesReceived int64 `plist:"bytes_received"`
BytesSent int64 `plist:"bytes_sent"`
}

// EnergyImpactColumns returns the column definitions for the energy_impact table
func EnergyImpactColumns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.IntegerColumn("pid"),
table.TextColumn("name"),
table.TextColumn("energy_impact"),
table.TextColumn("energy_impact_per_s"),
table.IntegerColumn("cputime_ns"),
table.TextColumn("cputime_ms_per_s"),
table.TextColumn("cputime_userland_ratio"),
table.IntegerColumn("intr_wakeups"),
table.TextColumn("intr_wakeups_per_s"),
table.IntegerColumn("idle_wakeups"),
table.TextColumn("idle_wakeups_per_s"),
table.IntegerColumn("diskio_bytesread"),
table.TextColumn("diskio_bytesread_per_s"),
table.IntegerColumn("diskio_byteswritten"),
table.TextColumn("diskio_byteswritten_per_s"),
table.IntegerColumn("packets_received"),
table.IntegerColumn("packets_sent"),
table.IntegerColumn("bytes_received"),
table.IntegerColumn("bytes_sent"),
table.IntegerColumn("interval"),
}
}

// EnergyImpactGenerate generates the table data when queried
func EnergyImpactGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string

// Get interval from WHERE clause, default to 1000ms
interval := defaultInterval
if constraintList, present := queryContext.Constraints["interval"]; present {
for _, constraint := range constraintList.Constraints {
if constraint.Operator == table.OperatorEquals {
if parsed, err := strconv.Atoi(constraint.Expression); err == nil {
interval = parsed
}
}
}
}

r := utils.NewRunner()
fs := utils.OSFileSystem{}
tasks, err := runPowermetrics(r, fs, interval)
if err != nil {
fmt.Println(err)
return results, err
}

for _, t := range tasks {
results = append(results, map[string]string{
"pid": strconv.Itoa(t.PID),
"name": t.Name,
"energy_impact": fmt.Sprintf("%.2f", t.EnergyImpact),
"energy_impact_per_s": fmt.Sprintf("%.2f", t.EnergyImpactPerS),
"cputime_ns": strconv.FormatInt(t.CPUTimeNS, 10),
"cputime_ms_per_s": fmt.Sprintf("%.2f", t.CPUTimeMSPerS),
"cputime_userland_ratio": fmt.Sprintf("%.2f", t.CPUTimeUserlandRatio),
"intr_wakeups": strconv.Itoa(t.IntrWakeups),
"intr_wakeups_per_s": fmt.Sprintf("%.2f", t.IntrWakeupsPerS),
"idle_wakeups": strconv.Itoa(t.IdleWakeups),
"idle_wakeups_per_s": fmt.Sprintf("%.2f", t.IdleWakeupsPerS),
"diskio_bytesread": strconv.FormatInt(t.DiskIOBytesRead, 10),
"diskio_bytesread_per_s": fmt.Sprintf("%.2f", t.DiskIOBytesReadPerS),
"diskio_byteswritten": strconv.FormatInt(t.DiskIOBytesWritten, 10),
"diskio_byteswritten_per_s": fmt.Sprintf("%.2f", t.DiskIOBytesWrittenPerS),
"packets_received": strconv.Itoa(t.PacketsReceived),
"packets_sent": strconv.Itoa(t.PacketsSent),
"bytes_received": strconv.FormatInt(t.BytesReceived, 10),
"bytes_sent": strconv.FormatInt(t.BytesSent, 10),
"interval": strconv.Itoa(interval),
})
}

return results, nil
}

// runPowermetrics executes the powermetrics command and parses the output
func runPowermetrics(r utils.Runner, fs utils.FileSystem, interval int) ([]task, error) {
var output powermetricsOutput

// Check if powermetrics binary exists
_, err := fs.Stat("/usr/bin/powermetrics")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}

// Run powermetrics command
out, err := r.Runner.RunCmd(
"/usr/bin/powermetrics",
"-f", "plist",
"-n", "1",
"-i", strconv.Itoa(interval),
"--samplers", "tasks",
"--show-process-energy",
)
if err != nil {
return nil, errors.Wrap(err, "running powermetrics")
}

// Parse plist output
if err := plist.Unmarshal(out, &output); err != nil {
return nil, errors.Wrap(err, "unmarshalling powermetrics output")
}

return output.Tasks, nil
}
Loading
Loading