feat(go): enrich snapshots with package ownership
This commit is contained in:
parent
6355fb403d
commit
88d7a6e3b8
8 changed files with 149 additions and 54 deletions
|
|
@ -14,57 +14,91 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
tool *string
|
||||
cache = map[string]bool{}
|
||||
toolOnce sync.Once
|
||||
tool string
|
||||
cacheMu sync.Mutex
|
||||
cache = map[string]string{}
|
||||
seen = map[string]bool{}
|
||||
)
|
||||
|
||||
func packageTool() string {
|
||||
if tool != nil {
|
||||
return *tool
|
||||
}
|
||||
found := ""
|
||||
for _, name := range []string{"pacman", "dpkg", "rpm"} {
|
||||
if _, err := exec.LookPath(name); err == nil {
|
||||
found = name
|
||||
break
|
||||
toolOnce.Do(func() {
|
||||
for _, name := range []string{"pacman", "dpkg", "rpm"} {
|
||||
if _, err := exec.LookPath(name); err == nil {
|
||||
tool = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
tool = &found
|
||||
return found
|
||||
})
|
||||
return tool
|
||||
}
|
||||
|
||||
func query(name, path string) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
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()
|
||||
return err == nil && strings.Contains(string(output), "owned by")
|
||||
output, err = exec.CommandContext(ctx, "pacman", "-Qo", path).Output()
|
||||
case "dpkg":
|
||||
output, err := exec.CommandContext(ctx, "dpkg", "-S", path).Output()
|
||||
return err == nil && strings.Contains(string(output), ":")
|
||||
output, err = exec.CommandContext(ctx, "dpkg", "-S", path).Output()
|
||||
case "rpm":
|
||||
output, err := exec.CommandContext(ctx, "rpm", "-qf", path).Output()
|
||||
return err == nil && !strings.Contains(string(output), "not owned")
|
||||
output, err = exec.CommandContext(ctx, "rpm", "-qf", path).Output()
|
||||
}
|
||||
return false
|
||||
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 {
|
||||
if path == "" || path == "(deleted)" {
|
||||
return false
|
||||
}
|
||||
path = strings.ReplaceAll(path, " (deleted)", "")
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if owned, seen := cache[path]; seen {
|
||||
return owned
|
||||
}
|
||||
name := packageTool()
|
||||
owned := name != "" && query(name, path)
|
||||
cache[path] = owned
|
||||
return owned
|
||||
return PackageOwner(path) != ""
|
||||
}
|
||||
|
|
|
|||
19
go-agent/internal/provenance/provenance_test.go
Normal file
19
go-agent/internal/provenance/provenance_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package provenance
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseOwnerMatchesSupportedPackageManagers(t *testing.T) {
|
||||
cases := []struct{ tool, output, want string }{
|
||||
{"pacman", "/usr/bin/x is owned by core/example 1.2-1", "core/example 1.2-1"},
|
||||
{"dpkg", "example: /usr/bin/x", "example"},
|
||||
{"rpm", "example-1.2-1.x86_64", "example-1.2-1.x86_64"},
|
||||
{"rpm", "file /tmp/x is not owned by any package", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := parseOwner(tc.tool, tc.output); got != tc.want {
|
||||
t.Fatalf("%s %q -> %q, want %q", tc.tool, tc.output, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue