Skip to content

Commit 07b7068

Browse files
author
Muhammad Ibrahim
committed
dnf improvements on Security package updates
1 parent 1c13713 commit 07b7068

File tree

2 files changed

+165
-6
lines changed

2 files changed

+165
-6
lines changed

internal/packages/dnf.go

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ func (m *DNFManager) GetPackages() []models.Package {
5555
m.logger.WithField("count", len(installedPackages)).Debug("Found installed packages")
5656
}
5757

58+
// Get security updates first to identify which packages are security updates
59+
m.logger.Debug("Getting security updates...")
60+
securityPackages := m.getSecurityPackages(packageManager)
61+
m.logger.WithField("count", len(securityPackages)).Debug("Found security packages")
62+
5863
// Get upgradable packages
5964
m.logger.Debug("Getting upgradable packages...")
6065
checkCmd := exec.Command(packageManager, "check-update")
@@ -63,7 +68,7 @@ func (m *DNFManager) GetPackages() []models.Package {
6368
var upgradablePackages []models.Package
6469
if len(checkOutput) > 0 {
6570
m.logger.Debug("Parsing DNF/yum check-update output...")
66-
upgradablePackages = m.parseUpgradablePackages(string(checkOutput), packageManager, installedPackages)
71+
upgradablePackages = m.parseUpgradablePackages(string(checkOutput), packageManager, installedPackages, securityPackages)
6772
m.logger.WithField("count", len(upgradablePackages)).Debug("Found upgradable packages")
6873
} else {
6974
m.logger.Debug("No updates available")
@@ -77,8 +82,98 @@ func (m *DNFManager) GetPackages() []models.Package {
7782
return packages
7883
}
7984

85+
// getSecurityPackages gets the list of security packages from dnf/yum updateinfo
86+
func (m *DNFManager) getSecurityPackages(packageManager string) map[string]bool {
87+
securityPackages := make(map[string]bool)
88+
89+
// Try dnf updateinfo list security (works for dnf)
90+
updateInfoCmd := exec.Command(packageManager, "updateinfo", "list", "security")
91+
updateInfoOutput, err := updateInfoCmd.Output()
92+
if err != nil {
93+
// Fall back to "sec" if "security" doesn't work
94+
updateInfoCmd = exec.Command(packageManager, "updateinfo", "list", "sec")
95+
updateInfoOutput, err = updateInfoCmd.Output()
96+
}
97+
98+
if err != nil {
99+
m.logger.WithError(err).Debug("Failed to get security updates, will not mark packages as security updates")
100+
return securityPackages
101+
}
102+
103+
// Parse the output to extract package names
104+
scanner := bufio.NewScanner(strings.NewReader(string(updateInfoOutput)))
105+
for scanner.Scan() {
106+
line := strings.TrimSpace(scanner.Text())
107+
108+
// Skip header lines and empty lines
109+
if line == "" || strings.Contains(line, "Last metadata") ||
110+
strings.Contains(line, "expiration") || strings.HasPrefix(line, "Loading") {
111+
continue
112+
}
113+
114+
// Format: ALSA-2025:11140 Moderate/Sec. glib2-2.68.4-16.el9_6.2.x86_64
115+
// We need to extract the package name (3rd field) and get the base name
116+
fields := slices.Collect(strings.FieldsSeq(line))
117+
if len(fields) < 3 {
118+
continue
119+
}
120+
121+
// Skip lines that don't start with ALSA/RHSA (advisory IDs)
122+
// This filters out header lines like "expiration"
123+
if !strings.HasPrefix(fields[0], "ALSA") && !strings.HasPrefix(fields[0], "RHSA") {
124+
continue
125+
}
126+
127+
// The package name is in the format: package-name-version-release.arch
128+
// We need to extract just the base package name
129+
packageNameWithVersion := fields[2]
130+
basePackageName := m.extractBasePackageName(packageNameWithVersion)
131+
132+
if basePackageName != "" {
133+
securityPackages[basePackageName] = true
134+
}
135+
}
136+
137+
return securityPackages
138+
}
139+
140+
// extractBasePackageName extracts the base package name from a package string
141+
// Handles formats like:
142+
// - package-name-version-release.arch (from updateinfo)
143+
// - package-name.arch (from check-update)
144+
func (m *DNFManager) extractBasePackageName(packageString string) string {
145+
// Remove architecture suffix first (e.g., .x86_64, .noarch)
146+
baseName := packageString
147+
if idx := strings.LastIndex(packageString, "."); idx > 0 {
148+
archSuffix := packageString[idx+1:]
149+
// Check if it's a known architecture
150+
if archSuffix == "x86_64" || archSuffix == "i686" || archSuffix == "i386" ||
151+
archSuffix == "noarch" || archSuffix == "aarch64" || archSuffix == "arm64" {
152+
baseName = packageString[:idx]
153+
}
154+
}
155+
156+
// If the base name contains a version pattern (starts with a digit after a dash),
157+
// extract just the package name part
158+
// Format: package-name-version-release
159+
// We look for the FIRST dash that's followed by a digit (version starts)
160+
// This handles packages with dashes in their names like "glibc-common-2.34-168.el9_6.19"
161+
for i := 0; i < len(baseName); i++ {
162+
if baseName[i] == '-' && i+1 < len(baseName) {
163+
nextChar := baseName[i+1]
164+
// Check if the next character is a digit (version starts)
165+
if nextChar >= '0' && nextChar <= '9' {
166+
// This is the start of version, return everything before this dash
167+
return baseName[:i]
168+
}
169+
}
170+
}
171+
172+
return baseName
173+
}
174+
80175
// parseUpgradablePackages parses dnf/yum check-update output
81-
func (m *DNFManager) parseUpgradablePackages(output string, packageManager string, installedPackages map[string]string) []models.Package {
176+
func (m *DNFManager) parseUpgradablePackages(output string, packageManager string, installedPackages map[string]string, securityPackages map[string]bool) []models.Package {
82177
var packages []models.Package
83178

84179
scanner := bufio.NewScanner(strings.NewReader(output))
@@ -98,7 +193,6 @@ func (m *DNFManager) parseUpgradablePackages(output string, packageManager strin
98193

99194
packageName := fields[0]
100195
availableVersion := fields[1]
101-
repo := fields[2]
102196

103197
// Get current version from installed packages map (already collected)
104198
// Try exact match first
@@ -162,7 +256,9 @@ func (m *DNFManager) parseUpgradablePackages(output string, packageManager strin
162256
// Only add package if we have both current and available versions
163257
// This prevents empty currentVersion errors on the server
164258
if packageName != "" && currentVersion != "" && availableVersion != "" {
165-
isSecurityUpdate := strings.Contains(strings.ToLower(repo), "security")
259+
// Extract base package name to check against security packages
260+
basePackageName := m.extractBasePackageName(packageName)
261+
isSecurityUpdate := securityPackages[basePackageName]
166262

167263
packages = append(packages, models.Package{
168264
Name: packageName,

internal/packages/dnf_test.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ func TestDNFManager_parseUpgradablePackages(t *testing.T) {
5252
input string
5353
pkgMgr string
5454
installedPackages map[string]string
55+
securityPackages map[string]bool
5556
expected int
57+
expectedSecurity int
5658
}{
5759
{
5860
name: "upgradable packages",
@@ -63,14 +65,75 @@ systemd.x86_64 252-14.el9_2.2 baseos`,
6365
"kernel.x86_64": "5.14.0-284.30.1.el9_1",
6466
"systemd.x86_64": "252-14.el9_2.1",
6567
},
66-
expected: 2,
68+
securityPackages: map[string]bool{
69+
"kernel": true,
70+
},
71+
expected: 2,
72+
expectedSecurity: 1,
6773
},
6874
}
6975

7076
for _, tt := range tests {
7177
t.Run(tt.name, func(t *testing.T) {
72-
result := manager.parseUpgradablePackages(tt.input, tt.pkgMgr, tt.installedPackages)
78+
result := manager.parseUpgradablePackages(tt.input, tt.pkgMgr, tt.installedPackages, tt.securityPackages)
7379
assert.Equal(t, tt.expected, len(result))
80+
securityCount := 0
81+
for _, pkg := range result {
82+
if pkg.IsSecurityUpdate {
83+
securityCount++
84+
}
85+
}
86+
assert.Equal(t, tt.expectedSecurity, securityCount)
87+
})
88+
}
89+
}
90+
91+
func TestDNFManager_extractBasePackageName(t *testing.T) {
92+
logger := logrus.New()
93+
logger.SetLevel(logrus.ErrorLevel)
94+
manager := NewDNFManager(logger)
95+
96+
tests := []struct {
97+
name string
98+
input string
99+
expected string
100+
}{
101+
{
102+
name: "package with version and arch from updateinfo",
103+
input: "glib2-2.68.4-16.el9_6.2.x86_64",
104+
expected: "glib2",
105+
},
106+
{
107+
name: "package with dashes in name",
108+
input: "glibc-common-2.34-168.el9_6.19.x86_64",
109+
expected: "glibc-common",
110+
},
111+
{
112+
name: "package with arch from check-update",
113+
input: "glib2.x86_64",
114+
expected: "glib2",
115+
},
116+
{
117+
name: "package with noarch",
118+
input: "firewalld-filesystem.noarch",
119+
expected: "firewalld-filesystem",
120+
},
121+
{
122+
name: "package with version but no arch",
123+
input: "glib2-2.68.4-16.el9_6.2",
124+
expected: "glib2",
125+
},
126+
{
127+
name: "simple package name",
128+
input: "kernel",
129+
expected: "kernel",
130+
},
131+
}
132+
133+
for _, tt := range tests {
134+
t.Run(tt.name, func(t *testing.T) {
135+
result := manager.extractBasePackageName(tt.input)
136+
assert.Equal(t, tt.expected, result)
74137
})
75138
}
76139
}

0 commit comments

Comments
 (0)