Running Kernel Older Than Installed Kernel
Checks that the kernel in memory is the newest kernel installed, so kernel security fixes are actually in effect.
- Why It Matters
- A kernel update protects nothing until the machine boots into it. The package manager reports the update as installed, so the gap is invisible unless the running version is compared with the installed one. HOST-UPD-002 reads the Debian reboot marker; this check works on every distribution by comparing versions.
- How It Detects
- Runs the allow-listed command uname -r and compares it with the installed kernel versions (linux-image-<version> packages, or kernel and kernel-core from rpm -qa). If the newest installed version is newer than the running one, the check fails. Incomplete when the running version or the package list cannot be read; skipped when no kernel package is installed.
- How to Fix
- Schedule a maintenance window and reboot into the newest kernel. Where downtime is a problem, use the distribution's live patching service and still plan regular reboots.
- How to Verify
uname -r should match the newest version in dpkg -l 'linux-image-*' | grep ^ii or rpm -q kernel-core .- CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L- Control Category
- Patch and Vulnerability Management: CIS Controls v8 7, 2; NIST CSF 2.0 ID.RA, PR.PS
- Fix Effort
- Medium
- CIS Controls v8 Safeguard
- 7.4
- NIST CSF 2.0 Subcategory
- ID.RA-01, PR.PS-02
- CWE
- CWE-1104
- OWASP Top 10
- A06:2021
- NIST SP 800-53 Rev. 5
- SI-2, RA-5
- Benchmark Topic
- Apply installed kernel updates (CIS Controls v8 7.4) (topic only, no benchmark text)
- References
- NIST SP 800-40 Rev. 4, Guide to Enterprise Patch Management Planning
NIST SP 800-53 Rev. 5, Security and Privacy Controls
Source Code
The complete file that defines this check. Run it with openvapt scan --only HOST-PKG-005, or read it offline with openvapt explain HOST-PKG-005.
internal/checks/host/pkg_depth.go · Lines 1-381View on GitHub at This Commit
| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "net/url" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/openvapt/openvapt/internal/check" |
| 11 | "github.com/openvapt/openvapt/internal/checks/collect" |
| 12 | "github.com/openvapt/openvapt/internal/sys" |
| 13 | ) |
| 14 | |
| 15 | // HOST-PKG depth: how packages arrive and whether the running kernel is the |
| 16 | // one installed. HOST-PKG-001 (pending security updates), HOST-UPD-001 |
| 17 | // (automatic security updates) and HOST-UPD-002 (Debian reboot marker) are |
| 18 | // not repeated. Repository files are read; apt-cache and dnf are not run. |
| 19 | |
| 20 | var ( |
| 21 | refAptSecure = check.Ref{Title: "apt-secure(8), Debian manual pages", URL: "https://manpages.debian.org/trixie/apt/apt-secure.8.en.html"} |
| 22 | refDnfConf = check.Ref{Title: "dnf.conf(5), Ubuntu noble manual pages (gpgcheck)", URL: "https://manpages.ubuntu.com/manpages/noble/en/man5/dnf.conf.5.html"} |
| 23 | refNIST80040 = check.Ref{Title: "NIST SP 800-40 Rev. 4, Guide to Enterprise Patch Management Planning", URL: "https://csrc.nist.gov/pubs/sp/800/40/r4/final"} |
| 24 | refSourcesLst = check.Ref{Title: "sources.list(5), Debian manual pages", URL: "https://manpages.debian.org/trixie/apt/sources.list.5.en.html"} |
| 25 | ) |
| 26 | |
| 27 | // vendorRepoSuffixes are host name suffixes of distribution repositories. |
| 28 | // Anything else is listed as third party. Matching is by suffix so regional |
| 29 | // mirrors (my.archive.ubuntu.com) count as vendor hosts. |
| 30 | var vendorRepoSuffixes = []string{ |
| 31 | ".ubuntu.com", ".canonical.com", ".debian.org", ".debian.net", |
| 32 | ".fedoraproject.org", ".centos.org", ".redhat.com", ".rockylinux.org", ".almalinux.org", |
| 33 | ".oracle.com", ".amazonaws.com", ".opensuse.org", ".suse.com", ".raspbian.org", ".raspberrypi.org", ".raspberrypi.com", |
| 34 | } |
| 35 | |
| 36 | func vendorRepoHost(h string) bool { |
| 37 | h = strings.ToLower(h) |
| 38 | for _, s := range vendorRepoSuffixes { |
| 39 | if strings.HasSuffix(h, s) || h == strings.TrimPrefix(s, ".") { |
| 40 | return true |
| 41 | } |
| 42 | } |
| 43 | return false |
| 44 | } |
| 45 | |
| 46 | // repoHost extracts the host of a repository URI, or "" for local or |
| 47 | // unparseable ones (file:, cdrom:, copy:). |
| 48 | func repoHost(uri string) string { |
| 49 | u, err := url.Parse(strings.Trim(uri, `"'`)) |
| 50 | if err != nil || u.Host == "" { |
| 51 | return "" |
| 52 | } |
| 53 | return strings.ToLower(u.Hostname()) |
| 54 | } |
| 55 | |
| 56 | // aptSourceURIs returns the URIs of one-line (deb ...) and Deb822 (URIs:) |
| 57 | // apt source files, with the option block of one-line entries. |
| 58 | type aptSource struct { |
| 59 | URI, Options string |
| 60 | } |
| 61 | |
| 62 | func aptSources(text string) []aptSource { |
| 63 | var out []aptSource |
| 64 | for _, l := range activeLines(text) { |
| 65 | f := strings.Fields(l) |
| 66 | switch { |
| 67 | case len(f) >= 2 && (f[0] == "deb" || f[0] == "deb-src"): |
| 68 | opts := "" |
| 69 | rest := f[1:] |
| 70 | if strings.HasPrefix(rest[0], "[") { |
| 71 | // Collect "[ ... ]" which may span several fields. |
| 72 | var o []string |
| 73 | for len(rest) > 0 { |
| 74 | o = append(o, rest[0]) |
| 75 | done := strings.HasSuffix(rest[0], "]") |
| 76 | rest = rest[1:] |
| 77 | if done { |
| 78 | break |
| 79 | } |
| 80 | } |
| 81 | opts = strings.Join(o, " ") |
| 82 | } |
| 83 | if len(rest) > 0 { |
| 84 | out = append(out, aptSource{URI: rest[0], Options: opts}) |
| 85 | } |
| 86 | case strings.HasPrefix(l, "URIs:"): |
| 87 | for _, u := range strings.Fields(strings.TrimPrefix(l, "URIs:")) { |
| 88 | out = append(out, aptSource{URI: u}) |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | return out |
| 93 | } |
| 94 | |
| 95 | // repoFiles are the package repository definitions the checks read. |
| 96 | var ( |
| 97 | aptSourceFiles = []string{"/etc/apt/sources.list", "/etc/apt/sources.list.d/*.list", "/etc/apt/sources.list.d/*.sources"} |
| 98 | aptConfFiles = []string{"/etc/apt/apt.conf", "/etc/apt/apt.conf.d/*"} |
| 99 | yumRepoFiles = []string{"/etc/yum.repos.d/*.repo", "/etc/dnf/dnf.conf", "/etc/yum.conf"} |
| 100 | ) |
| 101 | |
| 102 | // aptPresent reports whether this machine uses apt. |
| 103 | func aptPresent(s sys.System) bool { |
| 104 | if exists(s, "/etc/apt") { |
| 105 | return true |
| 106 | } |
| 107 | for _, pat := range []string{"/etc/apt/sources.list*", "/etc/apt/apt.conf.d/*", "/etc/apt/sources.list.d/*"} { |
| 108 | if m, _ := s.Glob(pat); len(m) > 0 { |
| 109 | return true |
| 110 | } |
| 111 | } |
| 112 | return false |
| 113 | } |
| 114 | |
| 115 | // dnfPresent reports whether this machine uses dnf or yum. |
| 116 | func dnfPresent(s sys.System) bool { |
| 117 | if exists(s, "/etc/yum.repos.d") || exists(s, "/etc/dnf") || exists(s, "/etc/yum.conf") { |
| 118 | return true |
| 119 | } |
| 120 | m, _ := s.Glob("/etc/yum.repos.d/*.repo") |
| 121 | return len(m) > 0 |
| 122 | } |
| 123 | |
| 124 | func init() { |
| 125 | check.Register(&check.Check{ |
| 126 | ID: "HOST-PKG-002", Title: "Repository Signature Checks Disabled", Domain: check.DomainHost, |
| 127 | Severity: check.High, Category: check.CatPatch, Effort: check.EffortLow, |
| 128 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", |
| 129 | Summary: "Checks that the package manager verifies repository signatures, so a tampered mirror or intercepted download cannot install code as root.", |
| 130 | Rationale: "Package installation runs as root. apt and dnf verify a signature over the repository index before trusting any package in it; an entry marked trusted=yes, AllowUnauthenticated or gpgcheck=0 switches that off, and a mirror operator or anyone on the network path can then deliver a modified package at the next update.", |
| 131 | Detection: "apt: reads /etc/apt/sources.list, /etc/apt/sources.list.d/*.list (entries with [trusted=yes]) and *.sources (Trusted: yes), plus /etc/apt/apt.conf and /etc/apt/apt.conf.d/* for AllowUnauthenticated or AllowInsecureRepositories set to true. dnf and yum: reads /etc/yum.repos.d/*.repo, /etc/dnf/dnf.conf and /etc/yum.conf for gpgcheck=0 in any section. Skipped when no repository files exist; Incomplete when some exist but cannot be read.", |
| 132 | Fix: "Remove trusted=yes (or Trusted: yes) from the listed source, install the repository's signing key under /etc/apt/keyrings and reference it with signed-by=; remove AllowUnauthenticated and AllowInsecureRepositories settings. On dnf and yum set gpgcheck=1 (and repo_gpgcheck=1 where supported) in each listed repository and import its key with rpm --import.", |
| 133 | Verify: "grep -r 'trusted=yes' /etc/apt/ ; grep -r 'gpgcheck=0' /etc/yum.repos.d/ /etc/dnf/dnf.conf 2>/dev/null", |
| 134 | Refs: []check.Ref{refAptSecure, refDnfConf, refNIST80040}, |
| 135 | Standards: check.Standards{CWE: []string{"CWE-494", "CWE-347"}, OWASP: []string{"A08:2021"}, NIST: []string{"SI-7", "CM-14", "SR-3", "SR-11"}, CISTopic: "Package manager repositories and GPG keys are configured"}, |
| 136 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 137 | var bad []string |
| 138 | seen := 0 |
| 139 | var unreadable []string |
| 140 | src, u := readAll(ctx, s, aptSourceFiles...) |
| 141 | unreadable = append(unreadable, u...) |
| 142 | for _, f := range src { |
| 143 | seen++ |
| 144 | for _, e := range aptSources(f.Text) { |
| 145 | if strings.Contains(strings.ToLower(e.Options), "trusted=yes") { |
| 146 | bad = append(bad, f.Path+": "+repoHostOrURI(e.URI)+" marked trusted=yes (signature check off)") |
| 147 | } |
| 148 | } |
| 149 | for _, l := range activeLines(f.Text) { |
| 150 | if strings.EqualFold(strings.Join(strings.Fields(l), " "), "Trusted: yes") { |
| 151 | bad = append(bad, f.Path+": Trusted: yes (signature check off)") |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | conf, u := readAll(ctx, s, aptConfFiles...) |
| 156 | unreadable = append(unreadable, u...) |
| 157 | for _, f := range conf { |
| 158 | for _, l := range activeLines(f.Text) { |
| 159 | if strings.HasPrefix(l, "//") { |
| 160 | continue |
| 161 | } |
| 162 | low := strings.ToLower(l) |
| 163 | if (strings.Contains(low, "allowunauthenticated") || strings.Contains(low, "allowinsecurerepositories")) && strings.Contains(low, "true") { |
| 164 | bad = append(bad, f.Path+": "+l) |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | repos, u := readAll(ctx, s, yumRepoFiles...) |
| 169 | unreadable = append(unreadable, u...) |
| 170 | for _, f := range repos { |
| 171 | seen++ |
| 172 | var keys []string |
| 173 | for k := range collect.ParseKV(f.Text) { |
| 174 | if k == "gpgcheck" || strings.HasSuffix(k, ".gpgcheck") { |
| 175 | keys = append(keys, k) |
| 176 | } |
| 177 | } |
| 178 | sort.Strings(keys) |
| 179 | vals := collect.ParseKV(f.Text) |
| 180 | for _, k := range keys { |
| 181 | if k == "gpgcheck" && len(keys) > 1 { |
| 182 | continue // the unprefixed copy duplicates the section keys |
| 183 | } |
| 184 | for _, v := range vals[k] { |
| 185 | if strings.TrimSpace(v) == "0" { |
| 186 | bad = append(bad, f.Path+": "+k+"=0") |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | if len(bad) > 0 { |
| 192 | return []check.Result{check.FailR("package repositories", "Repository signature verification is disabled", bad...)} |
| 193 | } |
| 194 | if len(unreadable) > 0 { |
| 195 | return []check.Result{check.IncompleteR("package repositories", "Some repository files could not be read", unreadable...)} |
| 196 | } |
| 197 | if seen == 0 { |
| 198 | return []check.Result{check.SkipR("package repositories", "No apt or dnf repository files found")} |
| 199 | } |
| 200 | return []check.Result{check.PassR("package repositories", fmt.Sprintf("%d repository file(s) keep signature verification on", seen))} |
| 201 | }, |
| 202 | }) |
| 203 | |
| 204 | check.Register(&check.Check{ |
| 205 | ID: "HOST-PKG-003", Title: "Third-Party Package Repositories Present", Domain: check.DomainHost, |
| 206 | Severity: check.Info, Category: check.CatPatch, Effort: check.EffortMedium, |
| 207 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N", |
| 208 | Summary: "Lists package repositories that are not run by the distribution vendor, so each one can be justified.", |
| 209 | Rationale: "Every repository can install software as root at the next update, so each one is a supplier you trust completely. Personal package archives, vendor repositories and forgotten test repositories are a common way for unpatched or abandoned software to stay on a server. This is informational: a third-party repository is a decision to review, not a defect by itself.", |
| 210 | Detection: "Reads /etc/apt/sources.list, /etc/apt/sources.list.d/*.list and *.sources, and /etc/yum.repos.d/*.repo (baseurl, mirrorlist, metalink), and lists repository host names that do not end in a known distribution domain (ubuntu.com, canonical.com, debian.org, fedoraproject.org, centos.org, redhat.com, rockylinux.org, almalinux.org, oracle.com, amazonaws.com, opensuse.org, suse.com, raspberrypi.com). Only host names are reported.", |
| 211 | Fix: "For each listed repository, record who runs it and why it is needed; remove the ones nobody can justify (delete the file under /etc/apt/sources.list.d or /etc/yum.repos.d and apt-get update or dnf clean all). Keep the rest pinned to the packages they are for (apt preferences, dnf includepkgs).", |
| 212 | Verify: "ls /etc/apt/sources.list.d/ /etc/yum.repos.d/ 2>/dev/null", |
| 213 | Refs: []check.Ref{refSourcesLst, refNIST80040, {Title: "NIST SP 800-161 Rev. 1, Cybersecurity Supply Chain Risk Management Practices", URL: "https://csrc.nist.gov/pubs/sp/800/161/r1/upd1/final"}}, |
| 214 | Standards: check.Standards{CWE: []string{"CWE-1357"}, OWASP: []string{"A08:2021"}, NIST: []string{"CM-8", "SR-3", "SR-4"}, CISTopic: "Software inventory: authorised repositories (CIS Controls v8 2.5)"}, |
| 215 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 216 | hosts := map[string]string{} |
| 217 | var order []string |
| 218 | add := func(path, h string) { |
| 219 | if h == "" || vendorRepoHost(h) { |
| 220 | return |
| 221 | } |
| 222 | if _, ok := hosts[h]; !ok { |
| 223 | order = append(order, h) |
| 224 | hosts[h] = path |
| 225 | } |
| 226 | } |
| 227 | src, unreadable := readAll(ctx, s, aptSourceFiles...) |
| 228 | for _, f := range src { |
| 229 | for _, e := range aptSources(f.Text) { |
| 230 | add(f.Path, repoHost(e.URI)) |
| 231 | } |
| 232 | } |
| 233 | repos, u := readAll(ctx, s, "/etc/yum.repos.d/*.repo") |
| 234 | unreadable = append(unreadable, u...) |
| 235 | for _, f := range repos { |
| 236 | vals := collect.ParseKV(f.Text) |
| 237 | for k, vs := range vals { |
| 238 | if strings.HasSuffix(k, ".baseurl") || strings.HasSuffix(k, ".mirrorlist") || strings.HasSuffix(k, ".metalink") { |
| 239 | for _, v := range vs { |
| 240 | for _, uri := range strings.Fields(v) { |
| 241 | add(f.Path, repoHost(uri)) |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | if len(order) > 0 { |
| 248 | sort.Strings(order) |
| 249 | ev := make([]string, 0, len(order)) |
| 250 | for _, h := range order { |
| 251 | ev = append(ev, h+" ("+hosts[h]+")") |
| 252 | } |
| 253 | return []check.Result{check.FailR("package repositories", fmt.Sprintf("%d third-party repository host(s) configured", len(order)), ev...)} |
| 254 | } |
| 255 | if len(unreadable) > 0 { |
| 256 | return []check.Result{check.IncompleteR("package repositories", "Some repository files could not be read", unreadable...)} |
| 257 | } |
| 258 | if len(src)+len(repos) == 0 { |
| 259 | return []check.Result{check.SkipR("package repositories", "No apt or dnf repository files found")} |
| 260 | } |
| 261 | return []check.Result{check.PassR("package repositories", "Only distribution vendor repositories are configured")} |
| 262 | }, |
| 263 | }) |
| 264 | |
| 265 | check.Register(&check.Check{ |
| 266 | ID: "HOST-PKG-004", Title: "Many Old Kernels Retained", Domain: check.DomainHost, |
| 267 | Severity: check.Low, Category: check.CatPatch, Effort: check.EffortLow, |
| 268 | CVSS: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N", |
| 269 | Summary: "Checks that no more than three kernel versions are installed.", |
| 270 | Rationale: "Every installed kernel stays bootable from the boot menu, with every vulnerability it had. Old kernels also fill /boot until updates fail to install, which is a quiet way to stop receiving kernel patches. Keeping the running kernel plus one or two fallbacks is enough.", |
| 271 | Detection: "Counts kernel versions among installed packages: linux-image-<version> packages whose /var/lib/dpkg/status entry reads 'install ok installed' on Debian and Ubuntu (the /var/lib/dpkg/info/*.list files are used only when the status file cannot be read, because removed but not purged packages keep them), kernel and kernel-core from the allow-listed rpm -qa on the RHEL family. More than three fails. Skipped when no kernel package is installed (containers); Incomplete when no package database could be read.", |
| 272 | Fix: "Debian/Ubuntu: apt-get autoremove --purge removes kernels outside the keep list. RHEL family: set installonly_limit=3 in /etc/dnf/dnf.conf and run dnf remove --oldinstallonly . Never remove the running kernel (uname -r).", |
| 273 | Verify: "dpkg -l 'linux-image-*' | grep '^ii' or rpm -q kernel kernel-core", |
| 274 | Refs: []check.Ref{refNIST80040, {Title: "apt-get(8), Debian trixie manual pages (autoremove)", URL: "https://manpages.debian.org/trixie/apt/apt-get.8.en.html"}}, |
| 275 | Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2", "CM-7"}, CISTopic: "Remove unneeded software versions"}, |
| 276 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 277 | pkgs, ok := installedPackages(ctx, s) |
| 278 | if !ok { |
| 279 | return []check.Result{check.IncompleteR("kernels", "No package database could be read (dpkg status, dpkg info or rpm)")} |
| 280 | } |
| 281 | vers := kernelVersions(pkgs) |
| 282 | switch { |
| 283 | case len(vers) == 0: |
| 284 | return []check.Result{check.SkipR("kernels", "No kernel packages are installed (container or unusual packaging)")} |
| 285 | case len(vers) > 3: |
| 286 | return []check.Result{check.FailR("kernels", fmt.Sprintf("%d kernel versions installed (recommended at most 3)", len(vers)), vers...)} |
| 287 | } |
| 288 | return []check.Result{check.PassR("kernels", fmt.Sprintf("%d kernel version(s) installed", len(vers)), vers...)} |
| 289 | }, |
| 290 | }) |
| 291 | |
| 292 | check.Register(&check.Check{ |
| 293 | ID: "HOST-PKG-005", Title: "Running Kernel Older Than Installed Kernel", Domain: check.DomainHost, |
| 294 | Severity: check.Medium, Category: check.CatPatch, Effort: check.EffortMedium, |
| 295 | CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L", |
| 296 | Summary: "Checks that the kernel in memory is the newest kernel installed, so kernel security fixes are actually in effect.", |
| 297 | Rationale: "A kernel update protects nothing until the machine boots into it. The package manager reports the update as installed, so the gap is invisible unless the running version is compared with the installed one. HOST-UPD-002 reads the Debian reboot marker; this check works on every distribution by comparing versions.", |
| 298 | Detection: "Runs the allow-listed command uname -r and compares it with the installed kernel versions (linux-image-<version> packages, or kernel and kernel-core from rpm -qa). If the newest installed version is newer than the running one, the check fails. Incomplete when the running version or the package list cannot be read; skipped when no kernel package is installed.", |
| 299 | Fix: "Schedule a maintenance window and reboot into the newest kernel. Where downtime is a problem, use the distribution's live patching service and still plan regular reboots.", |
| 300 | Verify: "uname -r should match the newest version in dpkg -l 'linux-image-*' | grep ^ii or rpm -q kernel-core .", |
| 301 | Refs: []check.Ref{refNIST80040, refNIST80053}, |
| 302 | Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2", "RA-5"}, CISTopic: "Apply installed kernel updates (CIS Controls v8 7.4)"}, |
| 303 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 304 | out, err := s.Run(ctx, "uname", "-r") |
| 305 | running := strings.TrimSpace(out) |
| 306 | if err != nil || running == "" { |
| 307 | return []check.Result{check.IncompleteR("kernel", "uname -r could not be run")} |
| 308 | } |
| 309 | pkgs, ok := installedPackages(ctx, s) |
| 310 | if !ok { |
| 311 | return []check.Result{check.IncompleteR("kernel", "No package database could be read (dpkg status, dpkg info or rpm)")} |
| 312 | } |
| 313 | vers := kernelVersions(pkgs) |
| 314 | if len(vers) == 0 { |
| 315 | return []check.Result{check.SkipR("kernel", "No kernel packages are installed (container or unusual packaging)")} |
| 316 | } |
| 317 | newest := vers[len(vers)-1] |
| 318 | // RPM kernels report an architecture suffix in uname that the |
| 319 | // package version lacks. |
| 320 | cmp := running |
| 321 | for _, arch := range []string{".x86_64", ".aarch64", ".ppc64le", ".s390x"} { |
| 322 | cmp = strings.TrimSuffix(cmp, arch) |
| 323 | } |
| 324 | if kernelLess(cmp, newest) { |
| 325 | return []check.Result{check.FailR("kernel", "Running kernel "+running+" is older than installed kernel "+newest+"; a reboot is needed")} |
| 326 | } |
| 327 | return []check.Result{check.PassR("kernel", "Running kernel "+running+" is the newest installed")} |
| 328 | }, |
| 329 | }) |
| 330 | |
| 331 | check.Register(&check.Check{ |
| 332 | ID: "HOST-PKG-006", Title: "Package Lists Not Refreshed Automatically", Domain: check.DomainHost, |
| 333 | Severity: check.Low, Category: check.CatPatch, Effort: check.EffortLow, |
| 334 | CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", |
| 335 | Summary: "Checks that the package manager downloads fresh repository metadata on a schedule, so pending updates can be seen at all.", |
| 336 | Rationale: "apt and dnf only know about updates listed in the metadata they last downloaded. Without a timer that refreshes it, HOST-PKG-001 and the administrator's own apt list --upgradable both report a clean system that is weeks behind. The refresh is read-only and separate from installing anything.", |
| 337 | Detection: "apt: reads /etc/apt/apt.conf.d/* for APT::Periodic::Update-Package-Lists \"1\" or asks the allow-listed systemctl is-enabled whether apt-daily.timer is enabled. dnf: asks whether dnf-makecache.timer is enabled. Skipped when neither package manager is present; Incomplete when apt configuration cannot be read.", |
| 338 | Fix: "Debian/Ubuntu: create /etc/apt/apt.conf.d/20auto-upgrades with APT::Periodic::Update-Package-Lists \"1\"; (dpkg-reconfigure unattended-upgrades writes it) and systemctl enable apt-daily.timer . RHEL family: systemctl enable --now dnf-makecache.timer .", |
| 339 | Verify: "systemctl is-enabled apt-daily.timer or systemctl is-enabled dnf-makecache.timer", |
| 340 | Refs: []check.Ref{{Title: "Debian, UnattendedUpgrades (APT::Periodic settings)", URL: "https://wiki.debian.org/UnattendedUpgrades"}, refNIST80040}, |
| 341 | Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2", "RA-5"}, CISTopic: "Automated patch management (CIS Controls v8 7.4)"}, |
| 342 | Run: func(ctx context.Context, s sys.System) []check.Result { |
| 343 | switch { |
| 344 | case aptPresent(s): |
| 345 | files, unreadable := readAll(ctx, s, aptConfFiles...) |
| 346 | for _, f := range files { |
| 347 | for _, l := range activeLines(f.Text) { |
| 348 | if strings.HasPrefix(l, "//") { |
| 349 | continue |
| 350 | } |
| 351 | if strings.Contains(l, "APT::Periodic::Update-Package-Lists") && strings.Contains(l, `"1"`) { |
| 352 | return []check.Result{check.PassR(f.Path, "APT::Periodic::Update-Package-Lists is 1")} |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | if len(enabledUnits(ctx, s, "apt-daily.timer")) > 0 { |
| 357 | return []check.Result{check.PassR("apt-daily.timer", "apt-daily.timer is enabled")} |
| 358 | } |
| 359 | if len(unreadable) > 0 { |
| 360 | return []check.Result{check.IncompleteR("apt", "Some apt configuration could not be read", unreadable...)} |
| 361 | } |
| 362 | return []check.Result{check.FailR("apt", "Package lists are not refreshed automatically (no APT::Periodic::Update-Package-Lists and apt-daily.timer not enabled)")} |
| 363 | case dnfPresent(s): |
| 364 | if len(enabledUnits(ctx, s, "dnf-makecache.timer")) > 0 { |
| 365 | return []check.Result{check.PassR("dnf-makecache.timer", "dnf-makecache.timer is enabled")} |
| 366 | } |
| 367 | return []check.Result{check.FailR("dnf", "dnf-makecache.timer is not enabled")} |
| 368 | } |
| 369 | return []check.Result{check.SkipR("packages", "No apt or dnf configuration found")} |
| 370 | }, |
| 371 | }) |
| 372 | } |
| 373 | |
| 374 | // repoHostOrURI returns the host of a repository URI, or the URI itself for |
| 375 | // local sources, for use in evidence. |
| 376 | func repoHostOrURI(uri string) string { |
| 377 | if h := repoHost(uri); h != "" { |
| 378 | return h |
| 379 | } |
| 380 | return uri |
| 381 | } |
Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.
Cite This Page
If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.
OpenVAPT. (2026). HOST-PKG-005 Running Kernel Older Than Installed Kernel (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-PKG-005.html
In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
@misc{openvapt_host_pkg_005,
author = {{OpenVAPT}},
title = {{HOST-PKG-005 Running Kernel Older Than Installed Kernel}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/HOST-PKG-005.html}},
note = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}
Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.
Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.