Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/39737-dup-kernel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- resolved issue where duplicate kernels were reported in the OS versions API for RHEL-family distributions (RHEL, AlmaLinux, CentOS, Rocky, Fedora)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20260211200153, Down_20260211200153)
}

func Up_20260211200153(tx *sql.Tx) error {
// Previously, both "kernel" and "kernel-core" were marked as kernels for RPM-based distributions.
// However, on RHEL-family systems (RHEL, AlmaLinux, CentOS, Rocky, Fedora), both packages exist:
// - "kernel-core"
// - "kernel"
//
// To avoid showing duplicate kernels in the OS versions API, we now only mark "kernel" as the
// kernel package for all RPM-based distributions (including Amazon Linux and RHEL-family).
// This unmarks "kernel-core" as a kernel.
if _, err := tx.Exec(`
UPDATE software_titles
SET is_kernel = 0
WHERE name = 'kernel-core' AND source = 'rpm_packages'
`); err != nil {
return fmt.Errorf("failed to unmark kernel-core as kernel: %w", err)
}

// Clean up kernel_host_counts entries for kernel-core titles that are no longer kernels
if _, err := tx.Exec(`
DELETE khc FROM kernel_host_counts khc
JOIN software_titles st ON st.id = khc.software_title_id
WHERE st.name = 'kernel-core' AND st.source = 'rpm_packages'
`); err != nil {
return fmt.Errorf("failed to clean up kernel_host_counts for kernel-core: %w", err)
}

return nil
}

func Down_20260211200153(tx *sql.Tx) error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package tables

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestUp_20260211200153(t *testing.T) {
db := applyUpToPrev(t)

// Insert software titles with is_kernel = 1 for various kernel packages

// kernel-core with rpm_packages source - should be unmarked as kernel
kernelCoreTitleID := execNoErrLastID(t, db, `
INSERT INTO software_titles (name, source, is_kernel)
VALUES ('kernel-core', 'rpm_packages', 1)
`)

// kernel with rpm_packages source - should remain as kernel
kernelTitleID := execNoErrLastID(t, db, `
INSERT INTO software_titles (name, source, is_kernel)
VALUES ('kernel', 'rpm_packages', 1)
`)

// Insert kernel_host_counts for each software title
_, err := db.Exec(`
INSERT INTO kernel_host_counts (software_title_id, software_id, os_version_id, hosts_count, team_id)
VALUES (?, 1, 100, 5, 0)
`, kernelCoreTitleID)
require.NoError(t, err)

_, err = db.Exec(`
INSERT INTO kernel_host_counts (software_title_id, software_id, os_version_id, hosts_count, team_id)
VALUES (?, 2, 100, 3, 0)
`, kernelTitleID)
require.NoError(t, err)

// Verify initial state
var initialCount int
err = db.Get(&initialCount, `SELECT COUNT(*) FROM kernel_host_counts`)
require.NoError(t, err)
require.Equal(t, 2, initialCount, "Should have 2 kernel_host_counts entries before migration")

// Apply migration
applyNext(t, db)

// Verify software_titles is_kernel values after migration
t.Run("kernel-core with rpm_packages is unmarked", func(t *testing.T) {
var isKernel int
err := db.Get(&isKernel, `SELECT is_kernel FROM software_titles WHERE id = ?`, kernelCoreTitleID)
require.NoError(t, err)
require.Equal(t, 0, isKernel, "kernel-core with rpm_packages should have is_kernel = 0")
})

t.Run("kernel with rpm_packages remains marked", func(t *testing.T) {
var isKernel int
err := db.Get(&isKernel, `SELECT is_kernel FROM software_titles WHERE id = ?`, kernelTitleID)
require.NoError(t, err)
require.Equal(t, 1, isKernel, "kernel with rpm_packages should still have is_kernel = 1")
})

// Verify kernel_host_counts cleanup
t.Run("kernel_host_counts for kernel-core rpm_packages deleted", func(t *testing.T) {
var count int
err := db.Get(&count, `
SELECT COUNT(*) FROM kernel_host_counts khc
JOIN software_titles st ON st.id = khc.software_title_id
WHERE st.name = 'kernel-core' AND st.source = 'rpm_packages'
`)
require.NoError(t, err)
require.Equal(t, 0, count, "kernel_host_counts for kernel-core/rpm_packages should be deleted")
})

t.Run("kernel_host_counts for kernel rpm_packages preserved", func(t *testing.T) {
var count int
err := db.Get(&count, `
SELECT COUNT(*) FROM kernel_host_counts khc
JOIN software_titles st ON st.id = khc.software_title_id
WHERE st.name = 'kernel' AND st.source = 'rpm_packages'
`)
require.NoError(t, err)
require.Equal(t, 1, count, "kernel_host_counts for kernel/rpm_packages should be preserved")
})

t.Run("total kernel_host_counts reduced by one", func(t *testing.T) {
var finalCount int
err := db.Get(&finalCount, `SELECT COUNT(*) FROM kernel_host_counts`)
require.NoError(t, err)
require.Equal(t, 1, finalCount, "Should have 1 kernel_host_counts entries after migration (one deleted)")
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ func testListKernelsByOS(t *testing.T, ds *Datastore) {
name: "RHEL with team",
team: true,
host: test.NewHost(t, ds, "host_fedora41", "", "hostkey_fedora41", "hostuuid_fedora41", time.Now(), test.WithPlatform("rhel")),
software: []fleet.Software{{Name: "kernel-core", Version: "6.11.4", Arch: "aarch64", Source: "rpm_packages", IsKernel: true}},
software: []fleet.Software{{Name: "kernel", Version: "6.11.4", Arch: "aarch64", Source: "rpm_packages", IsKernel: true}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0007"}},
vulnsByKernelVersion: map[string][]string{
"6.11.4": {"CVE-2025-0007"},
Expand Down
4 changes: 2 additions & 2 deletions server/datastore/mysql/schema.sql

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/service/integration_enterprise_vulns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (s *integrationEnterpriseTestSuite) TestLinuxOSVulns() {
{
name: "RHEL",
host: test.NewHost(t, s.ds, "host_fedora41", "", "hostkey_fedora41", "hostuuid_fedora41", time.Now(), test.WithPlatform("rhel")),
software: []fleet.Software{{Name: "kernel-core", Version: "6.11.4", Arch: "aarch64", Source: "rpm_packages", IsKernel: true}},
software: []fleet.Software{{Name: "kernel", Version: "6.11.4", Arch: "aarch64", Source: "rpm_packages", IsKernel: true}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0007"}},
vulnsByKernelVersion: map[string][]string{
"6.11.4": {"CVE-2025-0007"},
Expand Down
9 changes: 4 additions & 5 deletions server/service/osquery_utils/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -2015,10 +2015,9 @@ func directIngestScheduledQueryStats(ctx context.Context, logger log.Logger, hos
}

const (
linuxImageRegex = `^linux-image-[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-[[:digit:]]+-[[:alnum:]]+`
amazonLinuxKernelName = "kernel"
rhelKernelName = "kernel-core"
archKernelName = `^linux(?:-(?:lts|zen|hardened))?$`
linuxImageRegex = `^linux-image-[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-[[:digit:]]+-[[:alnum:]]+`
rpmKernelName = "kernel"
archKernelName = `^linux(?:-(?:lts|zen|hardened))?$`
)

var (
Expand Down Expand Up @@ -2064,7 +2063,7 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho
continue
}

if fleet.IsLinux(host.Platform) && (kernelRegex.MatchString(s.Name) || s.Name == amazonLinuxKernelName || s.Name == rhelKernelName || archKernelRegex.MatchString(s.Name)) {
if fleet.IsLinux(host.Platform) && (kernelRegex.MatchString(s.Name) || s.Name == rpmKernelName || archKernelRegex.MatchString(s.Name)) {
s.IsKernel = true
}

Expand Down
Loading