diff --git a/tables/pendingappleupdates/pendingappleupdates.go b/tables/pendingappleupdates/pendingappleupdates.go index c81cffe..f74f518 100644 --- a/tables/pendingappleupdates/pendingappleupdates.go +++ b/tables/pendingappleupdates/pendingappleupdates.go @@ -2,7 +2,6 @@ package pendingappleupdates import ( "context" - "log" "os" "github.com/macadmins/osquery-extension/pkg/utils" @@ -22,6 +21,8 @@ type recommendedUpdate struct { ProductKey string `plist:"Product Key"` } +var softwareUpdatePlistPath = "/Library/Preferences/com.apple.SoftwareUpdate.plist" + func PendingAppleUpdatesColumns() []table.ColumnDefinition { return []table.ColumnDefinition{ table.TextColumn("display_name"), @@ -56,21 +57,15 @@ func PendingAppleUpdatesGenerate(ctx context.Context, queryContext table.QueryCo func readSoftwareUpdatePlist(fs utils.FileSystem) (*softwareUpdatePlist, error) { var updatePlist softwareUpdatePlist - const plistPath = "/Library/Preferences/com.apple.SoftwareUpdate.plist" - if !utils.FileExists(fs, plistPath) { + if _, err := fs.Stat(softwareUpdatePlistPath); err != nil { return nil, nil } - file, err := os.Open(plistPath) + data, err := os.ReadFile(softwareUpdatePlistPath) if err != nil { return &updatePlist, errors.Wrap(err, "open com.apple.SoftwareUpdate plist") } - defer func() { - if err := file.Close(); err != nil { - log.Printf("close com.apple.SoftwareUpdate plist: %v", err) - } - }() - if err := plist.NewBinaryDecoder(file).Decode(&updatePlist); err != nil { + if err := plist.Unmarshal(data, &updatePlist); err != nil { return &updatePlist, errors.Wrap(err, "decode com.apple.SoftwareUpdate plist") } diff --git a/tables/pendingappleupdates/pendingappleupdates_test.go b/tables/pendingappleupdates/pendingappleupdates_test.go new file mode 100644 index 0000000..e12597b --- /dev/null +++ b/tables/pendingappleupdates/pendingappleupdates_test.go @@ -0,0 +1,90 @@ +package pendingappleupdates + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/macadmins/osquery-extension/pkg/utils" + "github.com/micromdm/plist" + "github.com/osquery/osquery-go/plugin/table" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func withSoftwareUpdatePlistPath(t *testing.T, path string) { + t.Helper() + original := softwareUpdatePlistPath + softwareUpdatePlistPath = path + t.Cleanup(func() { + softwareUpdatePlistPath = original + }) +} + +func writeSoftwareUpdatePlist(t *testing.T, path string, updatePlist softwareUpdatePlist) { + t.Helper() + data, err := plist.Marshal(updatePlist) + require.NoError(t, err) + require.NoError(t, os.WriteFile(path, data, 0600)) +} + +func TestPendingAppleUpdatesColumns(t *testing.T) { + assert.Equal(t, []table.ColumnDefinition{ + table.TextColumn("display_name"), + table.TextColumn("display_version"), + table.TextColumn("identifier"), + table.TextColumn("product_key"), + }, PendingAppleUpdatesColumns()) +} + +func TestPendingAppleUpdatesGenerate(t *testing.T) { + path := filepath.Join(t.TempDir(), "com.apple.SoftwareUpdate.plist") + writeSoftwareUpdatePlist(t, path, softwareUpdatePlist{ + RecommendedUpdates: []recommendedUpdate{ + { + DisplayName: "macOS Example Update", + DisplayVersion: "14.1", + Identifier: "MSU_UPDATE_1", + ProductKey: "012-34567", + }, + }, + }) + withSoftwareUpdatePlistPath(t, path) + + results, err := PendingAppleUpdatesGenerate(context.Background(), table.QueryContext{}) + require.NoError(t, err) + assert.Equal(t, []map[string]string{{ + "display_name": "macOS Example Update", + "display_version": "14.1", + "identifier": "MSU_UPDATE_1", + "product_key": "012-34567", + }}, results) +} + +func TestPendingAppleUpdatesGenerateMissingPlist(t *testing.T) { + withSoftwareUpdatePlistPath(t, filepath.Join(t.TempDir(), "missing.plist")) + + results, err := PendingAppleUpdatesGenerate(context.Background(), table.QueryContext{}) + require.NoError(t, err) + assert.Nil(t, results) +} + +func TestPendingAppleUpdatesGenerateInvalidPlist(t *testing.T) { + path := filepath.Join(t.TempDir(), "com.apple.SoftwareUpdate.plist") + require.NoError(t, os.WriteFile(path, []byte("not plist"), 0600)) + withSoftwareUpdatePlistPath(t, path) + + results, err := PendingAppleUpdatesGenerate(context.Background(), table.QueryContext{}) + assert.Error(t, err) + assert.Nil(t, results) +} + +func TestReadSoftwareUpdatePlistStatErrorIsTreatedAsMissing(t *testing.T) { + withSoftwareUpdatePlistPath(t, filepath.Join(t.TempDir(), "missing.plist")) + + updates, err := readSoftwareUpdatePlist(utils.MockFileSystem{Err: errors.New("stat failed")}) + require.NoError(t, err) + assert.Nil(t, updates) +}