enodia-sentinal/go-agent/internal/provenance/provenance.go

104 lines
2.4 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
// Package provenance answers "is this file shipped by an installed package?".
// Ownership raises confidence that a binary is legitimate; it never proves
// safety, so callers use it to suppress or rank alerts, not to delete evidence.
package provenance
import (
"context"
"os/exec"
"strings"
"sync"
"time"
)
var (
toolOnce sync.Once
tool string
cacheMu sync.Mutex
cache = map[string]string{}
seen = map[string]bool{}
)
func packageTool() string {
toolOnce.Do(func() {
for _, name := range []string{"pacman", "dpkg", "rpm"} {
if _, err := exec.LookPath(name); err == nil {
tool = name
break
}
}
})
return tool
}
func queryOwner(name, path string) string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var output []byte
var err error
switch name {
case "pacman":
output, err = exec.CommandContext(ctx, "pacman", "-Qo", path).Output()
case "dpkg":
output, err = exec.CommandContext(ctx, "dpkg", "-S", path).Output()
case "rpm":
output, err = exec.CommandContext(ctx, "rpm", "-qf", path).Output()
}
if err != nil {
return ""
}
return parseOwner(name, string(output))
}
func parseOwner(name, output string) string {
output = strings.TrimSpace(output)
switch name {
case "pacman":
_, owner, found := strings.Cut(output, " is owned by ")
if found {
return strings.TrimSpace(owner)
}
case "dpkg":
owner, _, found := strings.Cut(output, ":")
if found {
return strings.TrimSpace(owner)
}
case "rpm":
if output != "" && !strings.Contains(strings.ToLower(output), "not owned") {
return output
}
}
return ""
}
// PackageOwner returns the owning installed-package description, or an empty
// string. The command is bounded and runs without holding the cache lock.
func PackageOwner(path string) string {
path = strings.ReplaceAll(path, " (deleted)", "")
if path == "" || path == "(deleted)" {
return ""
}
cacheMu.Lock()
if seen[path] {
owner := cache[path]
cacheMu.Unlock()
return owner
}
cacheMu.Unlock()
owner := ""
if name := packageTool(); name != "" {
owner = queryOwner(name, path)
}
cacheMu.Lock()
cache[path], seen[path] = owner, true
cacheMu.Unlock()
return owner
}
// IsPackageOwned reports whether path belongs to an installed package.
// Results are cached because package ownership is stable within a sweep run.
func IsPackageOwned(path string) bool {
return PackageOwner(path) != ""
}