Skip to content

Commit 1a6e12a

Browse files
committed
packages/repostitories: add pacman support
Adds support for package and repository reporting on pacman-based distributions. As implemented--currently omits security update information, these rolling distributions only support complete updates and do not support security-only update options. PatchMon/PatchMon#373
1 parent 36aec47 commit 1a6e12a

File tree

9 files changed

+733
-16
lines changed

9 files changed

+733
-16
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/spf13/cobra v1.10.1
1212
github.com/spf13/viper v1.21.0
1313
github.com/stretchr/testify v1.11.1
14+
gopkg.in/ini.v1 v1.67.0
1415
gopkg.in/natefinch/lumberjack.v2 v2.2.1
1516
)
1617

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
157157
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
158158
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
159159
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
160+
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
161+
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
160162
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
161163
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
162164
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/constants/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
RepoTypeDebSrc = "deb-src"
5252
RepoTypeRPM = "rpm"
5353
RepoTypeAPK = "apk"
54+
RepoTypePacman = "pacman"
5455
)
5556

5657
// Log level constants

internal/packages/packages.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,26 @@ import (
1111

1212
// Manager handles package information collection
1313
type Manager struct {
14-
logger *logrus.Logger
15-
aptManager *APTManager
16-
dnfManager *DNFManager
17-
apkManager *APKManager
14+
logger *logrus.Logger
15+
aptManager *APTManager
16+
dnfManager *DNFManager
17+
apkManager *APKManager
18+
pacmanManager *PacmanManager
1819
}
1920

2021
// New creates a new package manager
2122
func New(logger *logrus.Logger) *Manager {
2223
aptManager := NewAPTManager(logger)
2324
dnfManager := NewDNFManager(logger)
2425
apkManager := NewAPKManager(logger)
26+
pacmanManager := NewPacmanManager(logger)
2527

2628
return &Manager{
27-
logger: logger,
28-
aptManager: aptManager,
29-
dnfManager: dnfManager,
30-
apkManager: apkManager,
29+
logger: logger,
30+
aptManager: aptManager,
31+
dnfManager: dnfManager,
32+
apkManager: apkManager,
33+
pacmanManager: pacmanManager,
3134
}
3235
}
3336

@@ -44,6 +47,8 @@ func (m *Manager) GetPackages() ([]models.Package, error) {
4447
return m.dnfManager.GetPackages(), nil
4548
case "apk":
4649
return m.apkManager.GetPackages(), nil
50+
case "pacman":
51+
return m.pacmanManager.GetPackages()
4752
default:
4853
return nil, fmt.Errorf("unsupported package manager: %s", packageManager)
4954
}
@@ -72,6 +77,11 @@ func (m *Manager) detectPackageManager() string {
7277
return "yum"
7378
}
7479

80+
// Check for Pacman
81+
if _, err := exec.LookPath("pacman"); err == nil {
82+
return "pacman"
83+
}
84+
7585
return "unknown"
7686
}
7787

internal/packages/pacman.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package packages
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"os/exec"
7+
"regexp"
8+
"strings"
9+
10+
"patchmon-agent/pkg/models"
11+
12+
"github.com/sirupsen/logrus"
13+
)
14+
15+
var installedPackageRe = regexp.MustCompile(`^(\S+)\s+(\S+)$`)
16+
var checkUpdateRe = regexp.MustCompile(`^(\S+)\s+(\S+)\s+->\s+(\S+)$`)
17+
18+
// PacmanManager handles pacman package information collection
19+
type PacmanManager struct {
20+
logger *logrus.Logger
21+
}
22+
23+
// NewPacmanManager creates a new Pacman package manager
24+
func NewPacmanManager(logger *logrus.Logger) *PacmanManager {
25+
return &PacmanManager{
26+
logger: logger,
27+
}
28+
}
29+
30+
// indirections for testability
31+
var (
32+
lookPath = exec.LookPath
33+
runCommand = exec.Command
34+
)
35+
36+
// GetPackages gets package information for pacman-based systems
37+
func (m *PacmanManager) GetPackages() ([]models.Package, error) {
38+
// Get installed packages
39+
installedCmd := runCommand("pacman", "-Q")
40+
installedOutput, err := installedCmd.Output()
41+
var installedPackages map[string]string
42+
if err != nil {
43+
m.logger.WithError(err).Error("Failed to get installed packages")
44+
installedPackages = make(map[string]string)
45+
} else {
46+
installedPackages = m.parseInstalledPackages(string(installedOutput))
47+
}
48+
49+
upgradablePackages, err := m.getUpgradablePackages()
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
// Merge and deduplicate packages
55+
packages := CombinePackageData(installedPackages, upgradablePackages)
56+
return packages, nil
57+
}
58+
59+
// getUpgradablePackages runs checkupdates and returns parsed packages.
60+
func (m *PacmanManager) getUpgradablePackages() ([]models.Package, error) {
61+
if _, err := lookPath("checkupdates"); err != nil {
62+
m.logger.WithError(err).Error("checkupdates not found (pacman-contrib not installed)")
63+
return nil, err
64+
}
65+
66+
upgradeCmd := runCommand("checkupdates")
67+
upgradeOutput, err := upgradeCmd.Output()
68+
if err != nil {
69+
// 0 = success with output, 1 = unknown failure, 2 = no updates available.
70+
var exitErr *exec.ExitError
71+
if errors.As(err, &exitErr) {
72+
if exitErr.ExitCode() == 2 {
73+
return []models.Package{}, nil
74+
}
75+
}
76+
m.logger.WithError(err).Error("checkupdates failed")
77+
return nil, err
78+
}
79+
80+
pkgs := m.parseCheckUpdate(string(upgradeOutput))
81+
return pkgs, nil
82+
}
83+
84+
// parseCheckUpdate parses checkupdates output
85+
func (m *PacmanManager) parseCheckUpdate(output string) []models.Package {
86+
packages := make([]models.Package, 0)
87+
88+
scanner := bufio.NewScanner(strings.NewReader(output))
89+
for scanner.Scan() {
90+
line := scanner.Text()
91+
matches := checkUpdateRe.FindStringSubmatch(line)
92+
if matches == nil {
93+
continue
94+
}
95+
96+
pkg := models.Package{
97+
Name: matches[1],
98+
CurrentVersion: matches[2],
99+
AvailableVersion: matches[3],
100+
NeedsUpdate: true,
101+
IsSecurityUpdate: false, // Data not provided
102+
}
103+
packages = append(packages, pkg)
104+
}
105+
106+
return packages
107+
}
108+
109+
// parseInstalledPackages parses pacman -Q output and returns a map of package name to version
110+
func (m *PacmanManager) parseInstalledPackages(output string) map[string]string {
111+
installedPackages := make(map[string]string)
112+
113+
scanner := bufio.NewScanner(strings.NewReader(output))
114+
for scanner.Scan() {
115+
matches := installedPackageRe.FindStringSubmatch(scanner.Text())
116+
if matches == nil {
117+
continue
118+
}
119+
120+
packageName := matches[1]
121+
version := matches[2]
122+
installedPackages[packageName] = version
123+
}
124+
125+
return installedPackages
126+
}

0 commit comments

Comments
 (0)