Skip to content

Commit 25fd6da

Browse files
committed
fix: RHEL backport-aware version checks for kernel, OpenSSH, nginx, apache
On RHEL-based distros (AlmaLinux, Rocky, CentOS, RHEL), packages like kernel 4.18.x and OpenSSH 8.0 receive backported security patches but keep older version numbers. The hardening scanner now checks if an update is actually available (dnf/yum check-update) instead of comparing raw version numbers against upstream. If no update is available, the check passes with "latest available for this OS".
1 parent 2798506 commit 25fd6da

1 file changed

Lines changed: 86 additions & 15 deletions

File tree

‎internal/scanner/versions.go‎

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,50 @@ func checkOSVersion() []Finding {
6767
}}
6868
}
6969

70+
// ─── RHEL Backport Helpers ───
71+
72+
// isRHELBased returns true for distros that backport security patches
73+
// to older version numbers (RHEL, AlmaLinux, Rocky, CentOS, Oracle Linux).
74+
func isRHELBased() bool {
75+
data, err := os.ReadFile("/etc/os-release")
76+
if err != nil {
77+
return false
78+
}
79+
id := strings.ToLower(osReleaseValue(string(data), "ID"))
80+
idLike := strings.ToLower(osReleaseValue(string(data), "ID_LIKE"))
81+
rhelIDs := []string{"rhel", "almalinux", "rocky", "centos", "ol", "amzn", "fedora"}
82+
for _, rid := range rhelIDs {
83+
if id == rid || strings.Contains(idLike, rid) || strings.Contains(idLike, "rhel") {
84+
return true
85+
}
86+
}
87+
return false
88+
}
89+
90+
// hasPackageUpdate checks if a specific package has an available update
91+
// using dnf or yum. Returns true if an update IS available.
92+
func hasPackageUpdate(pkg string) bool {
93+
// dnf check-update exits 100 if updates available, 0 if none
94+
if _, err := exec.LookPath("dnf"); err == nil {
95+
cmd := exec.Command("dnf", "check-update", "--quiet", pkg)
96+
err := cmd.Run()
97+
// exit 100 = updates available, exit 0 = no updates
98+
if exitErr, ok := err.(*exec.ExitError); ok {
99+
return exitErr.ExitCode() == 100
100+
}
101+
return false // exit 0 = no updates
102+
}
103+
if _, err := exec.LookPath("yum"); err == nil {
104+
cmd := exec.Command("yum", "check-update", "--quiet", pkg)
105+
err := cmd.Run()
106+
if exitErr, ok := err.(*exec.ExitError); ok {
107+
return exitErr.ExitCode() == 100
108+
}
109+
return false
110+
}
111+
return false
112+
}
113+
70114
// ─── Kernel Version Check ───
71115

72116
func checkKernelVersion() []Finding {
@@ -94,9 +138,16 @@ func checkKernelVersion() []Finding {
94138
passed = false
95139
desc += " — very old kernel, known vulnerabilities"
96140
} else if major < 5 || (major == 5 && minor < 4) {
97-
severity = "medium"
98-
passed = false
99-
desc += " — outdated kernel"
141+
// On RHEL-based distros, check if the kernel has a pending update
142+
// RHEL 8 ships 4.18.x with all security patches backported
143+
if isRHELBased() && !hasPackageUpdate("kernel") {
144+
desc += " — latest available for this OS (security patches backported)"
145+
passed = true
146+
} else {
147+
severity = "medium"
148+
passed = false
149+
desc += " — outdated kernel"
150+
}
100151
} else {
101152
desc += " — up to date"
102153
}
@@ -317,13 +368,20 @@ func checkOpenSSHVersion() []Finding {
317368
desc := fmt.Sprintf("OpenSSH %s", version)
318369

319370
if major < 8 {
371+
// Even on RHEL, OpenSSH < 8 is truly old (RHEL 7 = EOL)
320372
severity = "high"
321373
passed = false
322374
desc += " — very old, known vulnerabilities"
323375
} else if major == 8 && minor < 9 {
324-
severity = "medium"
325-
passed = false
326-
desc += " — outdated"
376+
// RHEL 8 ships OpenSSH 8.0 with backported security patches
377+
if isRHELBased() && !hasPackageUpdate("openssh-server") {
378+
desc += " — latest available for this OS (security patches backported)"
379+
passed = true
380+
} else {
381+
severity = "medium"
382+
passed = false
383+
desc += " — outdated"
384+
}
327385
} else {
328386
desc += " — up to date"
329387
}
@@ -353,13 +411,22 @@ func checkWebServerVersion() []Finding {
353411
desc := fmt.Sprintf("Nginx %s", version)
354412

355413
if major == 1 && minor < 22 {
356-
severity = "high"
357-
passed = false
358-
desc += " — outdated, known vulnerabilities"
414+
if isRHELBased() && !hasPackageUpdate("nginx") {
415+
desc += " — latest available for this OS"
416+
// pass — no update available in repos
417+
} else {
418+
severity = "high"
419+
passed = false
420+
desc += " — outdated, known vulnerabilities"
421+
}
359422
} else if major == 1 && minor < 24 {
360-
severity = "medium"
361-
passed = false
362-
desc += " — consider upgrading"
423+
if isRHELBased() && !hasPackageUpdate("nginx") {
424+
desc += " — latest available for this OS"
425+
} else {
426+
severity = "medium"
427+
passed = false
428+
desc += " — consider upgrading"
429+
}
363430
} else {
364431
desc += " — up to date"
365432
}
@@ -396,9 +463,13 @@ func checkWebServerVersion() []Finding {
396463

397464
_ = minor // Apache is always 2.4.x nowadays
398465
if patch < 54 {
399-
severity = "high"
400-
passed = false
401-
desc += " — outdated, known vulnerabilities"
466+
if isRHELBased() && !hasPackageUpdate("httpd") {
467+
desc += " — latest available for this OS"
468+
} else {
469+
severity = "high"
470+
passed = false
471+
desc += " — outdated, known vulnerabilities"
472+
}
402473
} else {
403474
desc += " — up to date"
404475
}

0 commit comments

Comments
 (0)