Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
gopkg.in/ini.v1 v1.67.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
1 change: 1 addition & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const (
RepoTypeDebSrc = "deb-src"
RepoTypeRPM = "rpm"
RepoTypeAPK = "apk"
RepoTypePacman = "pacman"
)

// Log level constants
Expand Down
26 changes: 18 additions & 8 deletions internal/packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,26 @@ import (

// Manager handles package information collection
type Manager struct {
logger *logrus.Logger
aptManager *APTManager
dnfManager *DNFManager
apkManager *APKManager
logger *logrus.Logger
aptManager *APTManager
dnfManager *DNFManager
apkManager *APKManager
pacmanManager *PacmanManager
}

// New creates a new package manager
func New(logger *logrus.Logger) *Manager {
aptManager := NewAPTManager(logger)
dnfManager := NewDNFManager(logger)
apkManager := NewAPKManager(logger)
pacmanManager := NewPacmanManager(logger)

return &Manager{
logger: logger,
aptManager: aptManager,
dnfManager: dnfManager,
apkManager: apkManager,
logger: logger,
aptManager: aptManager,
dnfManager: dnfManager,
apkManager: apkManager,
pacmanManager: pacmanManager,
}
}

Expand All @@ -44,6 +47,8 @@ func (m *Manager) GetPackages() ([]models.Package, error) {
return m.dnfManager.GetPackages(), nil
case "apk":
return m.apkManager.GetPackages(), nil
case "pacman":
return m.pacmanManager.GetPackages()
default:
return nil, fmt.Errorf("unsupported package manager: %s", packageManager)
}
Expand Down Expand Up @@ -72,6 +77,11 @@ func (m *Manager) detectPackageManager() string {
return "yum"
}

// Check for Pacman
if _, err := exec.LookPath("pacman"); err == nil {
return "pacman"
}

return "unknown"
}

Expand Down
126 changes: 126 additions & 0 deletions internal/packages/pacman.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package packages

import (
"bufio"
"errors"
"os/exec"
"regexp"
"strings"

"patchmon-agent/pkg/models"

"github.com/sirupsen/logrus"
)

var installedPackageRe = regexp.MustCompile(`^(\S+)\s+(\S+)$`)
var checkUpdateRe = regexp.MustCompile(`^(\S+)\s+(\S+)\s+->\s+(\S+)$`)

// PacmanManager handles pacman package information collection
type PacmanManager struct {
logger *logrus.Logger
}

// NewPacmanManager creates a new Pacman package manager
func NewPacmanManager(logger *logrus.Logger) *PacmanManager {
return &PacmanManager{
logger: logger,
}
}

// indirections for testability
var (
lookPath = exec.LookPath
runCommand = exec.Command
)

// GetPackages gets package information for pacman-based systems
func (m *PacmanManager) GetPackages() ([]models.Package, error) {
// Get installed packages
installedCmd := runCommand("pacman", "-Q")
installedOutput, err := installedCmd.Output()
var installedPackages map[string]string
if err != nil {
m.logger.WithError(err).Error("Failed to get installed packages")
installedPackages = make(map[string]string)
} else {
installedPackages = m.parseInstalledPackages(string(installedOutput))
}

upgradablePackages, err := m.getUpgradablePackages()
if err != nil {
return nil, err
}

// Merge and deduplicate packages
packages := CombinePackageData(installedPackages, upgradablePackages)
return packages, nil
}

// getUpgradablePackages runs checkupdates and returns parsed packages.
func (m *PacmanManager) getUpgradablePackages() ([]models.Package, error) {
if _, err := lookPath("checkupdates"); err != nil {
m.logger.WithError(err).Error("checkupdates not found (pacman-contrib not installed)")
return nil, err
}

upgradeCmd := runCommand("checkupdates")
upgradeOutput, err := upgradeCmd.Output()
if err != nil {
// 0 = success with output, 1 = unknown failure, 2 = no updates available.
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitCode() == 2 {
return []models.Package{}, nil
}
}
m.logger.WithError(err).Error("checkupdates failed")
return nil, err
}

pkgs := m.parseCheckUpdate(string(upgradeOutput))
return pkgs, nil
}

// parseCheckUpdate parses checkupdates output
func (m *PacmanManager) parseCheckUpdate(output string) []models.Package {
packages := make([]models.Package, 0)

scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
line := scanner.Text()
matches := checkUpdateRe.FindStringSubmatch(line)
if matches == nil {
continue
}

pkg := models.Package{
Name: matches[1],
CurrentVersion: matches[2],
AvailableVersion: matches[3],
NeedsUpdate: true,
IsSecurityUpdate: false, // Data not provided
}
packages = append(packages, pkg)
}

return packages
}

// parseInstalledPackages parses pacman -Q output and returns a map of package name to version
func (m *PacmanManager) parseInstalledPackages(output string) map[string]string {
installedPackages := make(map[string]string)

scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
matches := installedPackageRe.FindStringSubmatch(scanner.Text())
if matches == nil {
continue
}

packageName := matches[1]
version := matches[2]
installedPackages[packageName] = version
}

return installedPackages
}
Loading