package packages import ( "context" "fmt" "os/exec" "runtime" "time" ) const collectTimeout = 2 * time.Minute // Collect enumerates installed packages. Linux only: Windows agents are // second-class by design, and vulnerability scanning there needs a different // source, a different collector and a different matcher, all out of scope. // // The format strings below are raw string literals on purpose. The "\t" and // "\n" reach dpkg-query and rpm as two characters each, and those tools do the // interpreting themselves - Go must not consume the escapes first. func Collect() (OSRelease, []Package, error) { if runtime.GOOS != "linux" { return OSRelease{}, nil, fmt.Errorf("package collection is linux-only, got %s", runtime.GOOS) } osrel, err := DetectOS() if err != nil { return OSRelease{}, nil, fmt.Errorf("detect os: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), collectTimeout) defer cancel() switch { case have("dpkg-query"): out, err := run(ctx, "dpkg-query", "-W", "-f", `${Package}\t${Version}\t${Architecture}\t${source:Package}\t${db:Status-Status}\n`) if err != nil { return osrel, nil, err } return osrel, ParseDpkg(out), nil case have("rpm"): out, err := run(ctx, "rpm", "-qa", "--qf", `%{NAME}\t%{EPOCH}\t%{VERSION}-%{RELEASE}\t%{ARCH}\t%{SOURCERPM}\n`) if err != nil { return osrel, nil, err } return osrel, ParseRPM(out), nil case have("apk"): out, err := run(ctx, "apk", "info", "-v") if err != nil { return osrel, nil, err } return osrel, ParseAPK(out), nil default: return osrel, nil, fmt.Errorf("no supported package manager found") } } func have(bin string) bool { _, err := exec.LookPath(bin) return err == nil } func run(ctx context.Context, name string, args ...string) (string, error) { out, err := exec.CommandContext(ctx, name, args...).Output() if err != nil { return "", fmt.Errorf("%s: %w", name, err) } return string(out), nil }