@@ -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 ,
0 commit comments