**Related issue:** Resolves #36799, Sub-task: #41556 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <img width="924" height="278" alt="Screenshot 2026-03-16 at 10 46 38 AM" src="https://github.com/user-attachments/assets/313b6650-a849-4bc2-ba14-a62d3d13b60c" /> <img width="1441" height="300" alt="Screenshot 2026-03-16 at 10 46 44 AM" src="https://github.com/user-attachments/assets/915cfd26-168f-4621-bcf5-6c26c40e5faf" /> <img width="1923" height="788" alt="Screenshot 2026-03-16 at 10 54 04 AM" src="https://github.com/user-attachments/assets/62356a3e-84fe-4561-b7ad-0a35c9db3b2a" /> <img width="2529" height="483" alt="Screenshot 2026-03-16 at 10 47 02 AM" src="https://github.com/user-attachments/assets/4dc51073-2c24-4934-bd9d-c5ee648d5ae1" /> Tested that with latest released fleetd (1.53.0), we still ingest the available disk space. There's about 5% difference in the UI vs in the macOS "Get Info" dialog (expected, since we use the old query, now called `disk_space_darwin_legacy`): <img width="267" height="306" alt="Screenshot 2026-03-17 at 8 47 22 AM" src="https://github.com/user-attachments/assets/73fc1eef-a32c-4d8d-a9ca-13980885f8fe" /> <img width="883" height="407" alt="Screenshot 2026-03-17 at 8 47 33 AM" src="https://github.com/user-attachments/assets/98851b9b-82a8-4ac8-af5c-dbb878f85fad" /> <img width="159" height="127" alt="Screenshot 2026-03-17 at 8 47 40 AM" src="https://github.com/user-attachments/assets/209f784a-29a8-4af5-b95d-0f9bd59917c9" /> Also tested running with vanilla osquery by stopping fleetd and then running osquery manually (adding the `--allow_unsafe` flag). Result is same as above, `disk_space_darwin_legacy` is used: <img width="1152" height="418" alt="Screenshot 2026-03-17 at 8 59 23 AM" src="https://github.com/user-attachments/assets/2b34d23d-61de-4ec1-8d1c-2d3ddb682d11" /> <img width="893" height="414" alt="Screenshot 2026-03-17 at 8 59 28 AM" src="https://github.com/user-attachments/assets/d28ee8fb-08c5-434f-abfa-3825b27ac73b" /> ## Summary - Adds a new macOS-only fleetd table `disk_space` that uses `NSURLVolumeAvailableCapacityForImportantUsageKey` to report available disk capacity including purgeable storage — matching what macOS shows in Finder's "Get Info" dialog. - Adds a new `disk_space_darwin` detail query that uses the new table (with Discovery, so it only runs on hosts with fleetd ≥ 1.54.0). - Restricts the existing `disk_space_unix` query to Linux only (darwin was removed since the new query handles it). - Adds schema documentation for the new table. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
//go:build darwin
|
|
|
|
// Package disk_space provides a fleetd table that reports available and
|
|
// total disk capacity on macOS using NSURLVolumeAvailableCapacityForImportantUsageKey,
|
|
// which matches the "Available" space shown in macOS Finder's "Get Info" dialog.
|
|
package disk_space
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"github.com/osquery/osquery-go/plugin/table"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
func Columns() []table.ColumnDefinition {
|
|
return []table.ColumnDefinition{
|
|
table.BigIntColumn("bytes_available"),
|
|
table.BigIntColumn("bytes_total"),
|
|
}
|
|
}
|
|
|
|
func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
|
|
bytesAvailable, bytesTotal, err := getDiskSpace(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []map[string]string{
|
|
{
|
|
"bytes_available": fmt.Sprintf("%d", bytesAvailable),
|
|
"bytes_total": fmt.Sprintf("%d", bytesTotal),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func getDiskSpace(ctx context.Context) (bytesAvailable, bytesTotal int64, err error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
script := `
|
|
ObjC.import('Foundation');
|
|
var url = $.NSURL.fileURLWithPath('/');
|
|
var err = Ref();
|
|
var availRef = Ref();
|
|
if (!url.getResourceValueForKeyError(availRef, $.NSURLVolumeAvailableCapacityForImportantUsageKey, err)) {
|
|
throw new Error('failed to get available capacity: ' + ObjC.unwrap(err[0].localizedDescription));
|
|
}
|
|
var totalRef = Ref();
|
|
if (!url.getResourceValueForKeyError(totalRef, $.NSURLVolumeTotalCapacityKey, err)) {
|
|
throw new Error('failed to get total capacity: ' + ObjC.unwrap(err[0].localizedDescription));
|
|
}
|
|
JSON.stringify({available: availRef[0].js, total: totalRef[0].js})
|
|
`
|
|
cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript", "-e", script)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
log.Debug().Err(err).Str("stderr", stderr.String()).Msg("failed to get disk space via osascript")
|
|
return 0, 0, fmt.Errorf("failed to run osascript: %w (stderr: %s)", err, stderr.String())
|
|
}
|
|
|
|
var result struct {
|
|
Available int64 `json:"available"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(bytes.TrimSpace(out), &result); err != nil {
|
|
return 0, 0, fmt.Errorf("failed to parse disk space result (stdout: %q, stderr: %q): %w", out, stderr.String(), err)
|
|
}
|
|
|
|
return result.Available, result.Total, nil
|
|
}
|