No One Can Stop Me Now
This commit is contained in:
213
services/setec-manager/internal/system/packages.go
Normal file
213
services/setec-manager/internal/system/packages.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"` // installed size in bytes
|
||||
SizeStr string `json:"size_str"` // human-readable
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ── APT Operations ──────────────────────────────────────────────────
|
||||
|
||||
// PackageUpdate runs `apt-get update` to refresh the package index.
|
||||
func PackageUpdate() (string, error) {
|
||||
out, err := exec.Command("apt-get", "update", "-qq").CombinedOutput()
|
||||
output := string(out)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("apt-get update failed: %w (%s)", err, output)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// PackageInstall installs one or more packages with apt-get install -y.
|
||||
// Package names are passed as separate arguments to avoid shell injection.
|
||||
func PackageInstall(packages ...string) (string, error) {
|
||||
if len(packages) == 0 {
|
||||
return "", fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if err := validatePackageName(pkg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
args := append([]string{"install", "-y"}, packages...)
|
||||
out, err := exec.Command("apt-get", args...).CombinedOutput()
|
||||
output := string(out)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("apt-get install failed: %w (%s)", err, output)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// PackageRemove removes one or more packages with apt-get remove -y.
|
||||
func PackageRemove(packages ...string) (string, error) {
|
||||
if len(packages) == 0 {
|
||||
return "", fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if err := validatePackageName(pkg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
args := append([]string{"remove", "-y"}, packages...)
|
||||
out, err := exec.Command("apt-get", args...).CombinedOutput()
|
||||
output := string(out)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("apt-get remove failed: %w (%s)", err, output)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// PackageListInstalled returns all installed packages via dpkg-query.
|
||||
func PackageListInstalled() ([]PackageInfo, error) {
|
||||
// dpkg-query format: name\tversion\tinstalled-size (in kB)
|
||||
out, err := exec.Command(
|
||||
"dpkg-query",
|
||||
"--show",
|
||||
"--showformat=${Package}\t${Version}\t${Installed-Size}\n",
|
||||
).CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dpkg-query failed: %w (%s)", err, string(out))
|
||||
}
|
||||
|
||||
var packages []PackageInfo
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(line, "\t")
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Installed-Size from dpkg is in kibibytes
|
||||
sizeKB, _ := strconv.ParseInt(strings.TrimSpace(fields[2]), 10, 64)
|
||||
sizeBytes := sizeKB * 1024
|
||||
|
||||
packages = append(packages, PackageInfo{
|
||||
Name: fields[0],
|
||||
Version: fields[1],
|
||||
Size: sizeBytes,
|
||||
SizeStr: humanBytes(uint64(sizeBytes)),
|
||||
})
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// PackageIsInstalled checks if a single package is installed using dpkg -l.
|
||||
func PackageIsInstalled(pkg string) bool {
|
||||
if err := validatePackageName(pkg); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
out, err := exec.Command("dpkg", "-l", pkg).CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// dpkg -l output has lines starting with "ii" for installed packages
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 && fields[0] == "ii" && fields[1] == pkg {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// PackageUpgrade runs `apt-get upgrade -y` to upgrade all packages.
|
||||
func PackageUpgrade() (string, error) {
|
||||
out, err := exec.Command("apt-get", "upgrade", "-y").CombinedOutput()
|
||||
output := string(out)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("apt-get upgrade failed: %w (%s)", err, output)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// PackageSecurityUpdates returns a list of packages with available security updates.
|
||||
func PackageSecurityUpdates() ([]PackageInfo, error) {
|
||||
// apt list --upgradable outputs lines like:
|
||||
// package/suite version arch [upgradable from: old-version]
|
||||
out, err := exec.Command("apt", "list", "--upgradable").CombinedOutput()
|
||||
if err != nil {
|
||||
// apt list may return exit code 1 even with valid output
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("apt list --upgradable failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var securityPkgs []PackageInfo
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
for _, line := range lines {
|
||||
// Skip header/warning lines
|
||||
if strings.HasPrefix(line, "Listing") || strings.HasPrefix(line, "WARNING") || strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter for security updates: look for "-security" in the suite name
|
||||
if !strings.Contains(line, "-security") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse: "name/suite version arch [upgradable from: old]"
|
||||
slashIdx := strings.Index(line, "/")
|
||||
if slashIdx < 0 {
|
||||
continue
|
||||
}
|
||||
name := line[:slashIdx]
|
||||
|
||||
// Get version from the fields after the suite
|
||||
rest := line[slashIdx+1:]
|
||||
fields := strings.Fields(rest)
|
||||
var version string
|
||||
if len(fields) >= 2 {
|
||||
version = fields[1]
|
||||
}
|
||||
|
||||
securityPkgs = append(securityPkgs, PackageInfo{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Status: "security-update",
|
||||
})
|
||||
}
|
||||
|
||||
return securityPkgs, nil
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// validatePackageName does basic validation to prevent obvious injection attempts.
|
||||
// Package names in Debian must consist of lowercase alphanumerics, +, -, . and
|
||||
// must be at least 2 characters long.
|
||||
func validatePackageName(pkg string) error {
|
||||
if len(pkg) < 2 {
|
||||
return fmt.Errorf("invalid package name %q: too short", pkg)
|
||||
}
|
||||
if len(pkg) > 128 {
|
||||
return fmt.Errorf("invalid package name %q: too long", pkg)
|
||||
}
|
||||
for _, c := range pkg {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.' || c == ':') {
|
||||
return fmt.Errorf("invalid character %q in package name %q", c, pkg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user