Skip to content
61 changes: 0 additions & 61 deletions pkg/inventory/software/collector_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -368,66 +367,6 @@ func getPublisherFromInfoPlist(bundlePath string) string {
return getPublisherFromPlistData(plistData)
}

// pkgInfo contains information about a package installation from pkgutil
type pkgInfo struct {
// PkgID is the package identifier (e.g., "com.microsoft.Word")
PkgID string
// Volume is the install volume (e.g., "/")
Volume string
// InstallTime is the installation timestamp
InstallTime string
}

// getPkgInfo queries the macOS package receipt database to find which PKG installed
// a specific file or directory. This uses `pkgutil --file-info` which is the official
// way to link applications to their installer receipts.
//
// Parameters:
// - path: The path to query (e.g., "/Applications/Numbers.app")
//
// Returns:
// - *pkgInfo: Package information if the path was installed by a PKG, nil otherwise
//
// Note: Returns nil for apps installed via drag-and-drop (no PKG receipt) or
// Mac App Store apps (receipt stored inside the app bundle, not in pkgutil database).
func getPkgInfo(path string) *pkgInfo {
// Run pkgutil --file-info to query which package installed this path
cmd := exec.Command("pkgutil", "--file-info", path)
output, err := cmd.Output()
if err != nil {
// No package owns this path (drag-and-drop install or error)
return nil
}

// Parse the output which looks like:
// volume: /
// path: Applications/Numbers.app
// pkgid: com.apple.pkg.Numbers
// pkg-version: 14.0
// install-time: 1654432493
info := &pkgInfo{}
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "pkgid: ") {
info.PkgID = strings.TrimPrefix(line, "pkgid: ")
} else if strings.HasPrefix(line, "volume: ") {
info.Volume = strings.TrimPrefix(line, "volume: ")
} else if strings.HasPrefix(line, "install-time: ") {
// Convert Unix timestamp to ISO 8601 format for cross-platform consistency
timestampStr := strings.TrimPrefix(line, "install-time: ")
if unixTime, err := strconv.ParseInt(timestampStr, 10, 64); err == nil {
info.InstallTime = time.Unix(unixTime, 0).Format(time.RFC3339)
}
}
}

// Only return if we found a package ID
if info.PkgID != "" {
return info
}
return nil
}

// entryWithPath pairs an Entry with its bundle path for parallel processing.
// When plistData is non-nil, the worker uses it to compute publisher without reading the file.
type entryWithPath struct {
Expand Down
49 changes: 12 additions & 37 deletions pkg/inventory/software/collector_darwin_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)

Expand Down Expand Up @@ -69,47 +68,26 @@ func getLocalUsers() ([]string, []*Warning) {
return users, warnings
}

// appPkgLookup holds info needed for parallel pkgutil lookup
// appPkgLookup holds info needed for PKG install source lookup
type appPkgLookup struct {
entry *Entry
appPath string
}

// populatePkgInfoParallel queries pkgutil for multiple apps in parallel
// Uses a worker pool to limit concurrent pkgutil processes
func populatePkgInfoParallel(items []appPkgLookup) {
const maxWorkers = 10 // Limit concurrent pkgutil processes

// populatePkgInfoFromIndex looks up PKG install source for each app using
// the BOM-derived reverse index, avoiding per-app subprocess spawning.
func populatePkgInfoFromIndex(items []appPkgLookup) {
if len(items) == 0 {
return
}

jobs := make(chan *appPkgLookup, len(items))
idx := getGlobalAppToPkgIndex()
for i := range items {
jobs <- &items[i]
}
close(jobs)

var wg sync.WaitGroup
workerCount := maxWorkers
if len(items) < maxWorkers {
workerCount = len(items)
}

for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range jobs {
if pkgInfo := getPkgInfo(item.appPath); pkgInfo != nil {
item.entry.InstallSource = installSourcePkg
item.entry.PkgID = pkgInfo.PkgID
}
}
}()
if pkgID := idx.lookupPkgForApp(items[i].appPath); pkgID != "" {
items[i].entry.InstallSource = installSourcePkg
items[i].entry.PkgID = pkgID
}
}

wg.Wait()
}

// Collect scans the /Applications directory recursively for installed applications.
Expand Down Expand Up @@ -220,10 +198,8 @@ func (c *applicationsCollector) Collect() ([]*Entry, []*Warning, error) {
if _, err := os.Stat(masReceiptPath); err == nil {
source = softwareTypeMAS
installSource = installSourceMAS
} else {
// Not a MAS app or system app - will need to check pkgutil later (in parallel)
needsPkgLookup = true
}
needsPkgLookup = true
}

// Determine architecture
Expand Down Expand Up @@ -267,9 +243,8 @@ func (c *applicationsCollector) Collect() ([]*Entry, []*Warning, error) {
}
}

// Populate PKG info in parallel for non-MAS apps
// This queries pkgutil --file-info to determine if the app was installed via PKG
populatePkgInfoParallel(itemsForPkgLookup)
// Populate PKG install source from BOM-derived reverse index (zero subprocesses)
populatePkgInfoFromIndex(itemsForPkgLookup)

// Populate publisher info in parallel using Info.plist extraction
populatePublishersParallel(itemsForPublisher)
Expand Down
Loading
Loading