104 lines
2.4 KiB
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) != ""
|
|
}
|