30738 linux vulns (#31893)

- **linux vulns API changes (#31490)**
- **31214 linux vulns optimization (#31722)**

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Checked table schema to confirm autoupdate
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
This commit is contained in:
Jahziel Villasana-Espinoza
2025-08-14 10:13:37 -04:00
committed by GitHub
parent b784a539ec
commit 153f73c8ca
17 changed files with 902 additions and 44 deletions
+4
View File
@@ -697,6 +697,10 @@ func TestCronVulnerabilitiesSkipMkdirIfDisabled(t *testing.T) {
return nil
}
ds.InsertKernelSoftwareMappingFunc = func(ctx context.Context) error {
return nil
}
mockLocker := schedule.SetupMockLocker("vulnerabilities", "test_instance", time.Now().UTC())
ds.LockFunc = mockLocker.Lock
ds.UnlockFunc = mockLocker.Unlock
+8 -2
View File
@@ -29,9 +29,9 @@ func createVulnProcessingCmd(configManager config.Manager) *cobra.Command {
vulnProcessingCmd := &cobra.Command{
Use: "vuln_processing",
Short: "Run the vulnerability processing features of Fleet",
Long: `The vuln_processing command is intended for advanced configurations that want to externally manage
Long: `The vuln_processing command is intended for advanced configurations that want to externally manage
vulnerability processing. By default the Fleet server command internally manages vulnerability processing via scheduled
'cron' style jobs, but setting 'vulnerabilities.disable_schedule=true' or 'FLEET_VULNERABILITIES_DISABLE_SCHEDULE=true'
'cron' style jobs, but setting 'vulnerabilities.disable_schedule=true' or 'FLEET_VULNERABILITIES_DISABLE_SCHEDULE=true'
will disable it on the server allowing the user configure their own 'cron' mechanism. Successful processing will be indicated
by an exit code of zero.`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
@@ -189,6 +189,12 @@ func getVulnFuncs(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger,
return ds.UpdateHostIssuesVulnerabilities(ctx)
},
},
{
Name: "insert_kernel_software_mapping",
VulnFunc: func(ctx context.Context) error {
return ds.InsertKernelSoftwareMapping(ctx)
},
},
}
return vulnFuncs
Binary file not shown.
@@ -0,0 +1,60 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20250813205039, Down_20250813205039)
}
func Up_20250813205039(tx *sql.Tx) error {
if _, err := tx.Exec(`
ALTER TABLE software_titles
ADD COLUMN is_kernel TINYINT(1) NOT NULL DEFAULT '0'`); err != nil {
return fmt.Errorf("failed to add software_titles.is_kernel column: %w", err)
}
// Backfill existing software titles
if _, err := tx.Exec(`
UPDATE software_titles
SET is_kernel =
-- Debian/Ubuntu
CASE WHEN name REGEXP '^linux-image-[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-[[:digit:]]+-[[:alnum:]]+' THEN
1
-- Amazon Linux
WHEN name = 'kernel' THEN
1
-- RHEL
WHEN name = 'kernel-core' THEN
1
ELSE
0
END
WHERE source IN ('rpm_packages', 'deb_packages')
`); err != nil {
return fmt.Errorf("failed to backfill software_titles.is_kernel column: %w", err)
}
if _, err := tx.Exec(`
CREATE TABLE kernel_host_counts (
id int unsigned NOT NULL AUTO_INCREMENT,
software_title_id int unsigned DEFAULT NULL,
software_id int unsigned DEFAULT NULL,
os_version_id int unsigned DEFAULT NULL,
hosts_count int unsigned NOT NULL,
team_id int unsigned NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_kernels_unique_mapping (os_version_id,team_id,software_id),
FOREIGN KEY (software_title_id) REFERENCES software_titles (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`); err != nil {
return fmt.Errorf("failed to create kernel_host_counts table: %w", err)
}
return nil
}
func Down_20250813205039(tx *sql.Tx) error {
return nil
}
@@ -0,0 +1,76 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20250813205039(t *testing.T) {
db := applyUpToPrev(t)
// Name as reported for Ubuntu
kernelID1 := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("linux-image-6.11.0-9-generic", "deb_packages", "")`)
// Name as reported for Debian
kernelID2 := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("linux-image-6.1.0-37-cloud-arm64", "deb_packages", "")`)
amazonKernelID := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("kernel", "rpm_packages", "")`)
rhelKernelID := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("kernel-core", "rpm_packages", "")`)
otherLinuxAppID := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("vim", "deb_packages", "")`)
otherAppMacOSID := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("Calculator", "apps", "")`)
otherAppWindowsID := execNoErrLastID(t, db, `INSERT INTO software_titles (name, source, browser) VALUES ("Notepad", "programs", "")`)
// Apply current migration.
applyNext(t, db)
tests := []struct {
name string
titleID int64
shouldBeKernel bool
}{
{
name: "ubuntu kernel",
titleID: kernelID1,
shouldBeKernel: true,
},
{
name: "debian kernel",
titleID: kernelID2,
shouldBeKernel: true,
},
{
name: "amazon linuxkernel",
titleID: amazonKernelID,
shouldBeKernel: true,
},
{
name: "rhel kernel",
titleID: rhelKernelID,
shouldBeKernel: true,
},
{
name: "other linux title",
titleID: otherLinuxAppID,
shouldBeKernel: false,
},
{
name: "other title macOS",
titleID: otherAppMacOSID,
shouldBeKernel: false,
},
{
name: "other title Windows",
titleID: otherAppWindowsID,
shouldBeKernel: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var isKernel bool
err := db.Get(&isKernel, `SELECT is_kernel FROM software_titles WHERE id = ?`, tt.titleID)
require.NoError(t, err)
require.Equal(t, tt.shouldBeKernel, isKernel)
})
}
}
@@ -31,7 +31,7 @@ func (ds *Datastore) ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) (
return r, nil
}
func (ds *Datastore) ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
func (ds *Datastore) ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool, teamID *uint) (fleet.Vulnerabilities, error) {
r := fleet.Vulnerabilities{}
stmt := `
@@ -42,6 +42,23 @@ func (ds *Datastore) ListVulnsByOsNameAndVersion(ctx context.Context, name, vers
JOIN operating_systems os ON os.id = osv.operating_system_id
AND os.name = ? AND os.version = ?
GROUP BY osv.cve
UNION
SELECT DISTINCT
software_cve.cve,
MIN(software_cve.created_at) created_at
FROM
software_cve
JOIN kernel_host_counts ON kernel_host_counts.software_id = software_cve.software_id
JOIN operating_systems ON operating_systems.os_version_id = kernel_host_counts.os_version_id
WHERE
operating_systems.name = ?
AND operating_systems.version = ?
AND kernel_host_counts.hosts_count > 0
%s
GROUP BY software_cve.cve
`
if includeCVSS {
@@ -68,12 +85,40 @@ func (ds *Datastore) ListVulnsByOsNameAndVersion(ctx context.Context, name, vers
JOIN operating_systems os ON os.id = v.operating_system_id
AND os.name = ? AND os.version = ?
GROUP BY v.cve
UNION
SELECT DISTINCT
software_cve.cve,
MIN(software_cve.created_at) created_at,
GROUP_CONCAT(DISTINCT software_cve.resolved_in_version SEPARATOR ',') resolved_in_version
FROM
software_cve
JOIN kernel_host_counts ON kernel_host_counts.software_id = software_cve.software_id
JOIN operating_systems ON operating_systems.os_version_id = kernel_host_counts.os_version_id
WHERE
operating_systems.name = ?
AND operating_systems.version = ?
AND kernel_host_counts.hosts_count > 0
%s
GROUP BY software_cve.cve
) osv
LEFT JOIN cve_meta cm ON cm.cve = osv.cve
`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &r, stmt, name, version); err != nil {
var tmID uint
var teamFilter string
args := []any{name, version, name, version}
if teamID != nil {
tmID = *teamID
teamFilter = "AND kernel_host_counts.team_id = ?"
args = append(args, tmID)
}
stmt = fmt.Sprintf(stmt, teamFilter)
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &r, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
}
@@ -168,3 +213,117 @@ func (ds *Datastore) DeleteOutOfDateOSVulnerabilities(ctx context.Context, src f
}
return nil
}
func (ds *Datastore) ListKernelsByOS(ctx context.Context, osVersionID uint, teamID *uint) ([]*fleet.Kernel, error) {
var kernels []*fleet.Kernel
stmt := `
SELECT DISTINCT
software.id AS id,
software_cve.cve AS cve,
software.version AS version,
SUM(kernel_host_counts.hosts_count) AS hosts_count
FROM
software
LEFT JOIN software_cve ON software.id = software_cve.software_id
JOIN kernel_host_counts ON kernel_host_counts.software_id = software.id
WHERE
kernel_host_counts.os_version_id = ? %s GROUP BY id, cve, version
`
var tmID uint
var teamFilter string
args := []any{osVersionID}
if teamID != nil {
tmID = *teamID
teamFilter = "AND kernel_host_counts.team_id = ?"
args = append(args, tmID)
}
stmt = fmt.Sprintf(stmt, teamFilter)
var results []struct {
ID uint `db:"id"`
CVE *string `db:"cve"`
Version string `db:"version"`
HostsCount uint `db:"hosts_count"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing kernels by OS name")
}
kernelSet := make(map[uint]*fleet.Kernel)
for _, result := range results {
k, ok := kernelSet[result.ID]
if !ok {
kernel := &fleet.Kernel{
ID: result.ID,
Version: result.Version,
HostsCount: result.HostsCount,
}
kernelSet[kernel.ID] = kernel
k = kernel
}
if result.CVE != nil {
k.Vulnerabilities = append(k.Vulnerabilities, *result.CVE)
}
}
for _, kernel := range kernelSet {
kernels = append(kernels, kernel)
}
return kernels, nil
}
func (ds *Datastore) InsertKernelSoftwareMapping(ctx context.Context) error {
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE kernel_host_counts SET hosts_count = 0`)
if err != nil {
return ctxerr.Wrap(ctx, err, "zero out existing kernel hosts counts")
}
statsStmt := `
INSERT INTO kernel_host_counts (software_title_id, software_id, os_version_id, hosts_count, team_id)
SELECT
software_titles.id AS software_title_id,
software.id AS software_id,
operating_systems.os_version_id AS os_version_id,
COUNT(host_operating_system.host_id) AS hosts_count,
COALESCE(hosts.team_id, 0) AS team_id
FROM
software_titles
JOIN software ON software.title_id = software_titles.id
JOIN host_software ON host_software.software_id = software.id
JOIN host_operating_system ON host_operating_system.host_id = host_software.host_id
JOIN operating_systems ON operating_systems.id = host_operating_system.os_id
JOIN hosts ON hosts.id = host_software.host_id
WHERE
software_titles.is_kernel = TRUE
GROUP BY
software_title_id,
software_id,
os_version_id,
team_id
ON DUPLICATE KEY UPDATE
hosts_count=VALUES(hosts_count)
`
_, err = ds.writer(ctx).ExecContext(ctx, statsStmt)
if err != nil {
return ctxerr.Wrap(ctx, err, "insert kernel software mapping")
}
_, err = ds.writer(ctx).ExecContext(ctx, `DELETE k FROM kernel_host_counts k LEFT JOIN software ON k.software_id = software.id WHERE software.id IS NULL`)
if err != nil {
return ctxerr.Wrap(ctx, err, "clean up orphan kernels by software id")
}
_, err = ds.writer(ctx).ExecContext(ctx, `DELETE k FROM kernel_host_counts k LEFT JOIN operating_systems ON k.os_version_id = operating_systems.os_version_id WHERE operating_systems.id IS NULL`)
if err != nil {
return ctxerr.Wrap(ctx, err, "clean up orphan kernels by os version id")
}
return nil
}
@@ -2,11 +2,13 @@ package mysql
import (
"context"
"sort"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -27,6 +29,8 @@ func TestOperatingSystemVulnerabilities(t *testing.T) {
{"DeleteOSVulnerabilitiesEmpty", testDeleteOSVulnerabilitiesEmpty},
{"DeleteOSVulnerabilities", testDeleteOSVulnerabilities},
{"DeleteOutOfDateOSVulnerabilities", testDeleteOutOfDateOSVulnerabilities},
{"TestListKernelsByOS", testListKernelsByOS},
{"TestKernelVulnsHostCount", testKernelVulnsHostCount},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -109,7 +113,7 @@ func testListVulnsByOsNameAndVersion(t *testing.T, ds *Datastore) {
dbOS = append(dbOS, *os)
}
cves, err := ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false)
cves, err := ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false, nil)
require.NoError(t, err)
require.Empty(t, cves)
@@ -165,7 +169,7 @@ func testListVulnsByOsNameAndVersion(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// test without CVS meta
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false)
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false, nil)
require.NoError(t, err)
expected := []string{"CVE-2021-1234", "CVE-2021-1235"}
@@ -176,7 +180,7 @@ func testListVulnsByOsNameAndVersion(t *testing.T, ds *Datastore) {
}
// test with CVS meta
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", true)
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", true, nil)
require.NoError(t, err)
require.Len(t, cves, 2)
@@ -348,3 +352,311 @@ func testDeleteOutOfDateOSVulnerabilities(t *testing.T, ds *Datastore) {
require.Len(t, actual, 1)
require.ElementsMatch(t, []fleet.OSVulnerability{newVuln}, actual)
}
func testListKernelsByOS(t *testing.T, ds *Datastore) {
ctx := context.Background()
kernel1 := fleet.Software{Name: "linux-image-6.11.0-9-generic", Version: "6.11.0-9.9", Source: "deb_packages", IsKernel: true}
kernel2 := fleet.Software{Name: "linux-image-7.11.0-10-generic", Version: "7.11.0-10.10", Source: "deb_packages", IsKernel: true}
kernel3 := fleet.Software{Name: "linux-image-8.11.0-11-generic", Version: "8.11.0-11.11", Source: "deb_packages", IsKernel: true}
software := []fleet.Software{
kernel1,
kernel2,
kernel3, // this one will have 0 vulns
}
cases := []struct {
name string
team bool
host *fleet.Host
software []fleet.Software
vulns []fleet.SoftwareVulnerability
vulnsByKernelVersion map[string][]string
os fleet.OperatingSystem
}{
{
name: "ubuntu no team",
team: false,
host: test.NewHost(t, ds, "host_ubuntu2410", "", "hostkey_ubuntu2410", "hostuuid_ubuntu2410", time.Now(), test.WithPlatform("linux")),
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0001"}, {CVE: "CVE-2025-0002"}, {CVE: "CVE-2025-0003"}},
vulnsByKernelVersion: map[string][]string{
kernel1.Version: {"CVE-2025-0001", "CVE-2025-0002"},
kernel2.Version: {"CVE-2025-0003"},
kernel3.Version: nil,
},
software: software,
os: fleet.OperatingSystem{Name: "Ubuntu", Version: "24.10", Arch: "x86_64", KernelVersion: "6.11.0-9-generic", Platform: "ubuntu"},
},
{
name: "ubuntu with team",
team: true,
host: test.NewHost(t, ds, "host_ubuntu2404", "", "hostkey_ubuntu2404", "hostuuid_ubuntu2404", time.Now(), test.WithPlatform("linux")),
software: software[1:],
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0004"}, {CVE: "CVE-2025-0005"}, {CVE: "CVE-2025-0003"}}, // Note the overlap; kernel2 has 0003 from the previous test
vulnsByKernelVersion: map[string][]string{
kernel2.Version: {"CVE-2025-0004", "CVE-2025-0005", "CVE-2025-0003"},
kernel3.Version: nil,
},
os: fleet.OperatingSystem{Name: "Ubuntu", Version: "24.04", Arch: "x86_64", KernelVersion: "6.11.0-9-generic", Platform: "ubuntu"},
},
{
name: "amazon linux with team",
team: true,
host: test.NewHost(t, ds, "host_amzn2023", "", "hostkey_amzn2023", "hostuuid_amzn2023", time.Now(), test.WithPlatform("fedora")),
software: []fleet.Software{{Name: "kernel", Version: "6.1.144", Arch: "x86_64", Source: "rpm_packages", IsKernel: true}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0006"}},
vulnsByKernelVersion: map[string][]string{
"6.1.144": {"CVE-2025-0006"},
},
os: fleet.OperatingSystem{Name: "Amazon Linux", Version: "2023.0.0", Arch: "x86_64", KernelVersion: "6.1.144-170.251.amzn2023.x86_64", Platform: "amzn"},
},
{
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}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0007"}},
vulnsByKernelVersion: map[string][]string{
"6.11.4": {"CVE-2025-0007"},
},
os: fleet.OperatingSystem{Name: "Fedora Linux", Version: "41.0.0", Arch: "aarch64", KernelVersion: "6.11.4-301.fc41.aarch64", Platform: "rhel"},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var teamID uint
if tt.team {
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1_" + tt.name})
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{tt.host.ID})))
teamID = team1.ID
}
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, tt.host.ID, tt.os))
os, err := ds.GetHostOperatingSystem(ctx, tt.host.ID)
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(ctx, tt.host.ID, tt.software)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, tt.host, false))
// Sort the host software by name to enforce a deterministic order
sort.Slice(tt.host.Software, func(i, j int) bool {
return tt.host.Software[i].Name < tt.host.Software[j].Name
})
softwareIDByVersion := make(map[string]uint)
for _, s := range tt.host.Software {
softwareIDByVersion[s.Version] = s.ID
}
cpes := []fleet.SoftwareCPE{
{SoftwareID: tt.host.Software[0].ID, CPE: "somecpe"},
}
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, tt.host, false))
var vulnsToInsert []fleet.SoftwareVulnerability
for k, v := range tt.vulnsByKernelVersion {
for _, s := range v {
vulnsToInsert = append(vulnsToInsert, fleet.SoftwareVulnerability{
SoftwareID: softwareIDByVersion[k],
CVE: s,
})
}
}
for _, v := range vulnsToInsert {
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
require.NoError(t, ds.LoadHostSoftware(ctx, tt.host, false))
require.NoError(t, ds.UpdateOSVersions(ctx))
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
require.NoError(t, ds.InsertKernelSoftwareMapping(ctx))
kernels, err := ds.ListKernelsByOS(ctx, os.OSVersionID, &teamID)
require.NoError(t, err)
require.Len(t, kernels, len(tt.software))
for _, kernel := range kernels {
expectedVulns, ok := tt.vulnsByKernelVersion[kernel.Version]
require.True(t, ok)
require.ElementsMatchf(t, expectedVulns, kernel.Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernel.Version)
require.Equal(t, kernel.HostsCount, uint(1))
}
expectedSet := make(map[string]struct{})
for _, v := range tt.vulns {
expectedSet[v.CVE] = struct{}{}
}
cves, err := ds.ListVulnsByOsNameAndVersion(ctx, os.Name, os.Version, false, &teamID)
require.NoError(t, err)
for _, g := range cves {
_, ok := expectedSet[g.CVE]
assert.Truef(t, ok, "got unexpected CVE: %s", g.CVE)
}
assert.Len(t, cves, len(tt.vulns))
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, os.Name, "not_found", false, nil)
require.NoError(t, err)
require.Empty(t, cves)
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, os.Name, os.Version, true, nil)
require.NoError(t, err)
require.Len(t, cves, len(tt.vulns))
for _, g := range cves {
_, ok := expectedSet[g.CVE]
assert.True(t, ok)
}
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, os.Name, "not_found", true, nil)
require.NoError(t, err)
require.Empty(t, cves)
})
}
}
func testKernelVulnsHostCount(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host_ubuntu2410", "", "hostkey_ubuntu2410", "hostuuid_ubuntu2410", time.Now(), test.WithPlatform("ubuntu"))
host2 := test.NewHost(t, ds, "host_ubuntu2404", "", "hostkey_ubuntu2404", "hostuuid_ubuntu2404", time.Now(), test.WithPlatform("ubuntu"))
host3 := test.NewHost(t, ds, "host_ubuntu2404_2", "", "hostkey_ubuntu2404_2", "hostuuid_ubuntu2404_2", time.Now(), test.WithPlatform("ubuntu"))
// Same as host 2 and 3, but on a different team
host4 := test.NewHost(t, ds, "host_ubuntu2404_3", "", "hostkey_ubuntu2404_3", "hostuuid_ubuntu2404_3", time.Now(), test.WithPlatform("ubuntu"))
os1 := &fleet.OperatingSystem{Name: "Ubuntu", Version: "24.10", Arch: "x86_64", KernelVersion: "6.11.0-9-generic", Platform: "ubuntu"}
os2 := &fleet.OperatingSystem{Name: "Ubuntu", Version: "24.04", Arch: "x86_64", KernelVersion: "6.11.0-9-generic", Platform: "ubuntu"}
kernel := fleet.Software{Name: "linux-image-6.11.0-9-generic", Version: "6.11.0-9.9", Source: "deb_packages", IsKernel: true}
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1_" + t.Name()})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2_" + t.Name()})
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID, host2.ID, host3.ID})))
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team2.ID, []uint{host4.ID})))
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, host1.ID, *os1))
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, host2.ID, *os2))
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, host3.ID, *os2))
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, host4.ID, *os2))
os1, err = ds.GetHostOperatingSystem(ctx, host1.ID)
require.NoError(t, err)
os2, err = ds.GetHostOperatingSystem(ctx, host2.ID)
require.NoError(t, err)
addKernelToHost := func(h *fleet.Host) {
var vulnsToInsert []fleet.SoftwareVulnerability
_, err = ds.UpdateHostSoftware(ctx, h.ID, []fleet.Software{kernel})
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, h, false))
_, err = ds.UpsertSoftwareCPEs(ctx, []fleet.SoftwareCPE{{SoftwareID: h.Software[0].ID, CPE: "somecpe"}})
require.NoError(t, err)
for _, cve := range []string{"CVE-2025-0001", "CVE-2025-0002"} {
vulnsToInsert = append(vulnsToInsert, fleet.SoftwareVulnerability{
SoftwareID: h.Software[0].ID,
CVE: cve,
})
}
for _, v := range vulnsToInsert {
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
}
for _, h := range []*fleet.Host{host1, host2, host3, host4} {
addKernelToHost(h)
}
for _, h := range []*fleet.Host{host1, host2, host3, host4} {
require.NoError(t, ds.LoadHostSoftware(ctx, h, false))
}
require.NoError(t, ds.UpdateOSVersions(ctx))
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
require.NoError(t, ds.InsertKernelSoftwareMapping(ctx))
expectedCVEs := []string{"CVE-2025-0001", "CVE-2025-0002"}
kernels, err := ds.ListKernelsByOS(ctx, os1.OSVersionID, &team1.ID)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(1), kernels[0].HostsCount) // host1
kernels, err = ds.ListKernelsByOS(ctx, os2.OSVersionID, &team1.ID)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
require.Equal(t, uint(2), kernels[0].HostsCount) // host2, host3
kernels, err = ds.ListKernelsByOS(ctx, os2.OSVersionID, &team2.ID)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(1), kernels[0].HostsCount) // host4
// "All teams" (aka team ID is nil)
// For os2, should be 3 since it's on host2, host3, and host4
kernels, err = ds.ListKernelsByOS(ctx, os2.OSVersionID, nil)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(3), kernels[0].HostsCount)
// For os1, should be 1 since it's on host1
kernels, err = ds.ListKernelsByOS(ctx, os1.OSVersionID, nil)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(1), kernels[0].HostsCount)
// Add another host to team1, counts should update
host5 := test.NewHost(t, ds, "host_ubuntu2404_4", "", "hostkey_ubuntu2404_4", "hostuuid_ubuntu2404_4", time.Now(), test.WithPlatform("ubuntu"))
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host5.ID})))
require.NoError(t, ds.UpdateHostOperatingSystem(ctx, host5.ID, *os2))
addKernelToHost(host5)
require.NoError(t, ds.UpdateOSVersions(ctx))
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
require.NoError(t, ds.InsertKernelSoftwareMapping(ctx))
kernels, err = ds.ListKernelsByOS(ctx, os2.OSVersionID, &team1.ID)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(3), kernels[0].HostsCount) // host2, host3, host5
// "All teams" (aka team ID is nil)
// For os2, should be 4 since it's on host2, host3, host4, and now host5
kernels, err = ds.ListKernelsByOS(ctx, os2.OSVersionID, nil)
require.NoError(t, err)
require.Len(t, kernels, 1)
assert.ElementsMatchf(t, expectedCVEs, kernels[0].Vulnerabilities, "unexpected vulnerabilities for kernel %s", kernels[0].Version)
assert.Equal(t, uint(4), kernels[0].HostsCount)
}
File diff suppressed because one or more lines are too long
+13 -12
View File
@@ -424,7 +424,7 @@ func updateExistingBundleIDs(ctx context.Context, tx sqlx.ExtContext, hostID uin
updateSoftwareStmt := `UPDATE software SET software.name = ?, software.name_source = 'bundle_4.67' WHERE software.bundle_identifier = ?`
hostSoftwareStmt := `
INSERT IGNORE INTO host_software
INSERT IGNORE INTO host_software
(host_id, software_id, last_opened_at)
VALUES
(?, (SELECT id FROM software WHERE bundle_identifier = ? AND name_source = 'bundle_4.67' ORDER BY id DESC LIMIT 1), ?)`
@@ -820,9 +820,10 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
titleID = &title.ID
} else if _, ok := newTitlesNeeded[checksum]; !ok {
st := fleet.SoftwareTitle{
Name: sw.Name,
Source: sw.Source,
Browser: sw.Browser,
Name: sw.Name,
Source: sw.Source,
Browser: sw.Browser,
IsKernel: sw.IsKernel,
}
if sw.BundleIdentifier != "" {
@@ -843,14 +844,14 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
// Insert into software_titles
totalTitlesToProcess := len(newTitlesNeeded)
if totalTitlesToProcess > 0 {
const numberOfArgsPerSoftwareTitles = 4 // number of ? in each VALUES clause
titlesValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?),", totalTitlesToProcess), ",")
const numberOfArgsPerSoftwareTitles = 5 // number of ? in each VALUES clause
titlesValues := strings.TrimSuffix(strings.Repeat("(?,?,?,?,?),", totalTitlesToProcess), ",")
// INSERT IGNORE is used to avoid duplicate key errors, which may occur since our previous read came from the replica.
titlesStmt := fmt.Sprintf("INSERT IGNORE INTO software_titles (name, source, browser, bundle_identifier) VALUES %s", titlesValues)
titlesStmt := fmt.Sprintf("INSERT IGNORE INTO software_titles (name, source, browser, bundle_identifier, is_kernel) VALUES %s", titlesValues)
titlesArgs := make([]interface{}, 0, totalTitlesToProcess*numberOfArgsPerSoftwareTitles)
titleChecksums := make([]string, 0, totalTitlesToProcess)
for checksum, title := range newTitlesNeeded {
titlesArgs = append(titlesArgs, title.Name, title.Source, title.Browser, title.BundleIdentifier)
titlesArgs = append(titlesArgs, title.Name, title.Source, title.Browser, title.BundleIdentifier, title.IsKernel)
titleChecksums = append(titleChecksums, checksum)
}
if _, err := tx.ExecContext(ctx, titlesStmt, titlesArgs...); err != nil {
@@ -2061,8 +2062,8 @@ DELETE st FROM software_titles st
id DESC
LIMIT 1
)
WHERE
st.bundle_identifier IS NOT NULL AND
WHERE
st.bundle_identifier IS NOT NULL AND
st.bundle_identifier != '' AND
s.name_source = 'bundle_4.67'
`
@@ -2424,7 +2425,7 @@ func hostInstalledSoftware(ds *Datastore, ctx context.Context, hostID uint) ([]*
software.source AS software_source,
software.version AS version,
software.bundle_identifier AS bundle_identifier
FROM
FROM
host_software
INNER JOIN
software ON host_software.software_id = software.id
@@ -3910,7 +3911,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt
FROM
software_titles
LEFT JOIN
software_installers ON software_titles.id = software_installers.title_id
software_installers ON software_titles.id = software_installers.title_id
AND software_installers.global_or_team_id = :global_or_team_id
LEFT JOIN
software ON software_titles.id = software.title_id ` + installedSoftwareJoinsCondition + `
+5 -1
View File
@@ -1095,7 +1095,7 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// OperatingSystemVulnerabilities Store
ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) ([]OSVulnerability, error)
ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool) (Vulnerabilities, error)
ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool, teamID *uint) (Vulnerabilities, error)
InsertOSVulnerabilities(ctx context.Context, vulnerabilities []OSVulnerability, source VulnerabilitySource) (int64, error)
DeleteOSVulnerabilities(ctx context.Context, vulnerabilities []OSVulnerability) error
// InsertOSVulnerability will either insert a new vulnerability in the datastore (in which
@@ -1106,6 +1106,10 @@ type Datastore interface {
// the updated_at timestamp is older than the supplied timestamp
DeleteOutOfDateOSVulnerabilities(ctx context.Context, source VulnerabilitySource, olderThan time.Time) error
ListKernelsByOS(ctx context.Context, osID uint, teamID *uint) ([]*Kernel, error)
InsertKernelSoftwareMapping(ctx context.Context) error
///////////////////////////////////////////////////////////////////////////////
// Vulnerabilities
+18 -13
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"
@@ -1018,23 +1019,13 @@ var HostRpmPackageOSs = map[string]struct{}{
}
func IsLinux(hostPlatform string) bool {
for _, linuxPlatform := range HostLinuxOSs {
if linuxPlatform == hostPlatform {
return true
}
}
return false
return slices.Contains(HostLinuxOSs, hostPlatform)
}
func IsUnixLike(hostPlatform string) bool {
unixLikeOSs := HostLinuxOSs
unixLikeOSs = append(unixLikeOSs, "darwin")
for _, p := range unixLikeOSs {
if p == hostPlatform {
return true
}
}
return false
return slices.Contains(unixLikeOSs, hostPlatform)
}
// PlatformFromHost converts the given host platform into
@@ -1317,10 +1308,18 @@ type VulnerableOS struct {
ResolvedInVersion *string `json:"resolved_in_version"`
}
// Kernel represents a Linux kernel found on a host.
type Kernel struct {
ID uint `json:"id"`
Version string `json:"version"`
Vulnerabilities []string `json:"vulnerabilities"`
HostsCount uint `json:"hosts_count"`
}
type OSVersion struct {
// ID is the unique id of the operating system.
ID uint `json:"id,omitempty"`
// OSVersionID is a uniqe NameOnly/Version combination for the operating system.
// OSVersionID is a unique NameOnly/Version combination for the operating system.
OSVersionID uint `json:"os_version_id"`
// HostsCount is the number of hosts that have reported the operating system.
HostsCount int `json:"hosts_count"`
@@ -1339,7 +1338,13 @@ type OSVersion struct {
// in NVD (macOS only)
GeneratedCPEs []string `json:"generated_cpes,omitempty"`
// Vulnerabilities are the vulnerabilities associated with the operating system.
// For Linux-based operating systems, these are vulnerabilities associated with the Linux kernel.
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
// Kernels is a list of Linux kernels found on this operating system.
// This list is only populated for Linux-based operating systems.
// Vulnerabilities are pulled based on the software entries for the kernels.
// Kernels are associated based on enrolled hosts with the selected OS version.
Kernels []*Kernel `json:"kernels"`
}
type HostDetailOptions struct {
+4
View File
@@ -94,6 +94,7 @@ type Software struct {
// TODO: should we create a separate type? Feels like this field shouldn't be here since it's
// just used for VPP install verification.
Installed bool `json:"-"`
IsKernel bool `json:"-"`
}
func (Software) AuthzType() string {
@@ -209,6 +210,8 @@ type SoftwareTitle struct {
// the software installed. It's surfaced in software_titles to match
// with existing software entries.
BundleIdentifier *string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
// IsKernel indicates if the software title is a Linux kernel.
IsKernel bool `json:"-" db:"is_kernel"`
}
// This type is essentially the same as the above SoftwareTitle type. The only difference is that
@@ -469,6 +472,7 @@ func SoftwareFromOsqueryRow(
if !lastOpenedAtTime.IsZero() {
software.LastOpenedAt = &lastOpenedAtTime
}
return &software, nil
}
+27 -3
View File
@@ -783,7 +783,7 @@ type InsertWindowsUpdatesFunc func(ctx context.Context, hostID uint, updates []f
type ListOSVulnerabilitiesByOSFunc func(ctx context.Context, osID uint) ([]fleet.OSVulnerability, error)
type ListVulnsByOsNameAndVersionFunc func(ctx context.Context, name string, version string, includeCVSS bool) (fleet.Vulnerabilities, error)
type ListVulnsByOsNameAndVersionFunc func(ctx context.Context, name string, version string, includeCVSS bool, teamID *uint) (fleet.Vulnerabilities, error)
type InsertOSVulnerabilitiesFunc func(ctx context.Context, vulnerabilities []fleet.OSVulnerability, source fleet.VulnerabilitySource) (int64, error)
@@ -793,6 +793,10 @@ type InsertOSVulnerabilityFunc func(ctx context.Context, vuln fleet.OSVulnerabil
type DeleteOutOfDateOSVulnerabilitiesFunc func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error
type ListKernelsByOSFunc func(ctx context.Context, osID uint, teamID *uint) ([]*fleet.Kernel, error)
type InsertKernelSoftwareMappingFunc func(ctx context.Context) error
type ListVulnerabilitiesFunc func(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error)
type VulnerabilityFunc func(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error)
@@ -2607,6 +2611,12 @@ type DataStore struct {
DeleteOutOfDateOSVulnerabilitiesFunc DeleteOutOfDateOSVulnerabilitiesFunc
DeleteOutOfDateOSVulnerabilitiesFuncInvoked bool
ListKernelsByOSFunc ListKernelsByOSFunc
ListKernelsByOSFuncInvoked bool
InsertKernelSoftwareMappingFunc InsertKernelSoftwareMappingFunc
InsertKernelSoftwareMappingFuncInvoked bool
ListVulnerabilitiesFunc ListVulnerabilitiesFunc
ListVulnerabilitiesFuncInvoked bool
@@ -6257,11 +6267,11 @@ func (s *DataStore) ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) ([
return s.ListOSVulnerabilitiesByOSFunc(ctx, osID)
}
func (s *DataStore) ListVulnsByOsNameAndVersion(ctx context.Context, name string, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
func (s *DataStore) ListVulnsByOsNameAndVersion(ctx context.Context, name string, version string, includeCVSS bool, teamID *uint) (fleet.Vulnerabilities, error) {
s.mu.Lock()
s.ListVulnsByOsNameAndVersionFuncInvoked = true
s.mu.Unlock()
return s.ListVulnsByOsNameAndVersionFunc(ctx, name, version, includeCVSS)
return s.ListVulnsByOsNameAndVersionFunc(ctx, name, version, includeCVSS, teamID)
}
func (s *DataStore) InsertOSVulnerabilities(ctx context.Context, vulnerabilities []fleet.OSVulnerability, source fleet.VulnerabilitySource) (int64, error) {
@@ -6292,6 +6302,20 @@ func (s *DataStore) DeleteOutOfDateOSVulnerabilities(ctx context.Context, source
return s.DeleteOutOfDateOSVulnerabilitiesFunc(ctx, source, olderThan)
}
func (s *DataStore) ListKernelsByOS(ctx context.Context, osID uint, teamID *uint) ([]*fleet.Kernel, error) {
s.mu.Lock()
s.ListKernelsByOSFuncInvoked = true
s.mu.Unlock()
return s.ListKernelsByOSFunc(ctx, osID, teamID)
}
func (s *DataStore) InsertKernelSoftwareMapping(ctx context.Context) error {
s.mu.Lock()
s.InsertKernelSoftwareMappingFuncInvoked = true
s.mu.Unlock()
return s.InsertKernelSoftwareMappingFunc(ctx)
}
func (s *DataStore) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
s.mu.Lock()
s.ListVulnerabilitiesFuncInvoked = true
+15 -4
View File
@@ -2155,7 +2155,7 @@ func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *stri
}
for i := range osVersions.OSVersions {
if err := svc.populateOSVersionDetails(ctx, &osVersions.OSVersions[i], includeCVSS); err != nil {
if err := svc.populateOSVersionDetails(ctx, &osVersions.OSVersions[i], includeCVSS, teamID, false); err != nil {
return nil, count, nil, err
}
}
@@ -2269,7 +2269,7 @@ func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, incl
}
if osVersion != nil {
if err = svc.populateOSVersionDetails(ctx, osVersion, includeCVSS); err != nil {
if err = svc.populateOSVersionDetails(ctx, osVersion, includeCVSS, teamID, true); err != nil {
return nil, nil, err
}
}
@@ -2278,7 +2278,7 @@ func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, incl
}
// PopulateOSVersionDetails populates the GeneratedCPEs and Vulnerabilities for an OSVersion.
func (svc *Service) populateOSVersionDetails(ctx context.Context, osVersion *fleet.OSVersion, includeCVSS bool) error {
func (svc *Service) populateOSVersionDetails(ctx context.Context, osVersion *fleet.OSVersion, includeCVSS bool, teamID *uint, includeKernels bool) error {
// Populate GeneratedCPEs
if osVersion.Platform == "darwin" {
osVersion.GeneratedCPEs = []string{
@@ -2288,16 +2288,27 @@ func (svc *Service) populateOSVersionDetails(ctx context.Context, osVersion *fle
}
// Populate Vulnerabilities
vulns, err := svc.ds.ListVulnsByOsNameAndVersion(ctx, osVersion.NameOnly, osVersion.Version, includeCVSS)
vulns, err := svc.ds.ListVulnsByOsNameAndVersion(ctx, osVersion.NameOnly, osVersion.Version, includeCVSS, teamID)
if err != nil {
return err
}
osVersion.Vulnerabilities = make(fleet.Vulnerabilities, 0) // avoid null in JSON
osVersion.Kernels = make([]*fleet.Kernel, 0) // avoid null in JSON
for _, vuln := range vulns {
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE)
osVersion.Vulnerabilities = append(osVersion.Vulnerabilities, vuln)
}
if fleet.IsLinux(osVersion.Platform) && includeKernels {
kernels, err := svc.ds.ListKernelsByOS(ctx, osVersion.OSVersionID, teamID)
if err != nil {
return err
}
osVersion.Kernels = kernels
}
return nil
}
+2 -2
View File
@@ -1295,7 +1295,7 @@ func TestEmptyTeamOSVersions(t *testing.T) {
return nil, newNotFoundError()
}
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool, teamID *uint) (fleet.Vulnerabilities, error) {
return fleet.Vulnerabilities{}, nil
}
@@ -1339,7 +1339,7 @@ func TestOSVersionsListOptions(t *testing.T) {
return &fleet.OSVersions{CountsUpdatedAt: time.Now(), OSVersions: testVersions}, nil
}
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool, teamID *uint) (fleet.Vulnerabilities, error) {
return fleet.Vulnerabilities{}, nil
}
@@ -0,0 +1,166 @@
package service
import (
"context"
"fmt"
"net/http"
"sort"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func (s *integrationEnterpriseTestSuite) TestLinuxOSVulns() {
t := s.T()
ctx := context.Background()
kernel1 := fleet.Software{Name: "linux-image-6.11.0-9-generic", Version: "6.11.0-9.9", Source: "deb_packages", IsKernel: true}
kernel2 := fleet.Software{Name: "linux-image-7.11.0-10-generic", Version: "7.11.0-10.10", Source: "deb_packages", IsKernel: true}
kernel3 := fleet.Software{Name: "linux-image-8.11.0-11-generic", Version: "8.11.0-11.11", Source: "deb_packages", IsKernel: true}
software := []fleet.Software{
kernel1,
kernel2,
kernel3, // this one will have 0 vulns
}
cases := []struct {
name string
host *fleet.Host
software []fleet.Software
vulns []fleet.SoftwareVulnerability
vulnsByKernelVersion map[string][]string
os fleet.OperatingSystem
}{
{
name: "ubuntu",
host: test.NewHost(t, s.ds, "host_ubuntu2410", "", "hostkey_ubuntu2410", "hostuuid_ubuntu2410", time.Now(), test.WithPlatform("ubuntu")),
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0001"}, {CVE: "CVE-2025-0002"}, {CVE: "CVE-2025-0003"}},
vulnsByKernelVersion: map[string][]string{
kernel1.Version: {"CVE-2025-0001", "CVE-2025-0002"},
kernel2.Version: {"CVE-2025-0003"},
kernel3.Version: nil,
},
software: software,
os: fleet.OperatingSystem{Name: "Ubuntu", Version: "24.10", Arch: "x86_64", KernelVersion: "6.11.0-9-generic", Platform: "ubuntu"},
},
{
name: "amazon linux",
host: test.NewHost(t, s.ds, "host_amzn2023", "", "hostkey_amzn2023", "hostuuid_amzn2023", time.Now(), test.WithPlatform("fedora")),
software: []fleet.Software{{Name: "kernel", Version: "6.1.144", Arch: "x86_64", Source: "rpm_packages", IsKernel: true}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0006"}},
vulnsByKernelVersion: map[string][]string{
"6.1.144": {"CVE-2025-0006"},
},
os: fleet.OperatingSystem{Name: "Amazon Linux", Version: "2023.0.0", Arch: "x86_64", KernelVersion: "6.1.144-170.251.amzn2023.x86_64", Platform: "amzn"},
},
{
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}},
vulns: []fleet.SoftwareVulnerability{{CVE: "CVE-2025-0007"}},
vulnsByKernelVersion: map[string][]string{
"6.11.4": {"CVE-2025-0007"},
},
os: fleet.OperatingSystem{Name: "Fedora Linux", Version: "41.0.0", Arch: "aarch64", KernelVersion: "6.11.4-301.fc41.aarch64", Platform: "rhel"},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
require.NoError(t, s.ds.UpdateHostOperatingSystem(ctx, tt.host.ID, tt.os))
var osinfo struct {
ID uint `db:"id"`
OSVersionID uint `db:"os_version_id"`
}
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &osinfo,
`SELECT id, os_version_id FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ? AND platform = ?`,
tt.os.Name, tt.os.Version, tt.os.Arch, tt.os.KernelVersion, tt.os.Platform)
})
require.Greater(t, osinfo.ID, uint(0))
require.Greater(t, osinfo.OSVersionID, uint(0))
_, err := s.ds.UpdateHostSoftware(ctx, tt.host.ID, tt.software)
require.NoError(t, err)
require.NoError(t, s.ds.LoadHostSoftware(ctx, tt.host, false))
softwareIDByVersion := make(map[string]uint)
for _, s := range tt.host.Software {
softwareIDByVersion[s.Version] = s.ID
}
cpes := []fleet.SoftwareCPE{{SoftwareID: tt.host.Software[0].ID, CPE: "somecpe"}}
_, err = s.ds.UpsertSoftwareCPEs(ctx, cpes)
require.NoError(t, err)
// Reload software so that GeneratedCPEID is set.
require.NoError(t, s.ds.LoadHostSoftware(ctx, tt.host, false))
var vulnsToInsert []fleet.SoftwareVulnerability
for k, v := range tt.vulnsByKernelVersion {
for _, s := range v {
vulnsToInsert = append(vulnsToInsert, fleet.SoftwareVulnerability{
SoftwareID: softwareIDByVersion[k],
CVE: s,
})
}
}
for _, v := range vulnsToInsert {
_, err = s.ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
// Aggregate OS versions
require.NoError(t, s.ds.UpdateOSVersions(ctx))
require.NoError(t, s.ds.UpdateOSVersions(ctx))
require.NoError(t, s.ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, s.ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, s.ds.SyncHostsSoftwareTitles(ctx, time.Now()))
require.NoError(t, s.ds.InsertKernelSoftwareMapping(ctx))
var osVersionsResp osVersionsResponse
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp)
var osVersion *fleet.OSVersion
for _, os := range osVersionsResp.OSVersions {
if os.Version == tt.os.Version {
osVersion = &os
break
}
}
assert.Equal(t, 1, osVersion.HostsCount)
assert.Equal(t, fmt.Sprintf("%s %s", tt.os.Name, tt.os.Version), osVersion.Name)
assert.Equal(t, tt.os.Name, osVersion.NameOnly)
assert.Equal(t, tt.os.Version, osVersion.Version)
assert.Equal(t, tt.os.Platform, osVersion.Platform)
assert.Len(t, osVersion.Vulnerabilities, len(tt.vulns))
// Test entity endpoint
var osVersionResp getOSVersionResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/os_versions/%d", osVersion.OSVersionID), nil, http.StatusOK, &osVersionResp, "team_id", fmt.Sprintf("%d", 0))
assert.Len(t, osVersionResp.OSVersion.Kernels, len(tt.software))
// Make sure the ordering is the same
sort.Slice(osVersionResp.OSVersion.Kernels, func(i, j int) bool {
return osVersionResp.OSVersion.Kernels[i].Version < osVersionResp.OSVersion.Kernels[j].Version
})
sort.Slice(tt.software, func(i, j int) bool {
return tt.software[i].Version < tt.software[j].Version
})
for i, k := range osVersionResp.OSVersion.Kernels {
assert.Equal(t, tt.software[i].Version, k.Version)
assert.Equal(t, uint(1), k.HostsCount)
assert.ElementsMatch(t, tt.vulnsByKernelVersion[k.Version], k.Vulnerabilities)
}
})
}
}
+10
View File
@@ -1789,6 +1789,12 @@ func directIngestScheduledQueryStats(ctx context.Context, logger log.Logger, hos
return nil
}
const linuxImageRegex = `^linux-image-[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-[[:digit:]]+-[[:alnum:]]+`
const amazonLinuxKernelName = "kernel"
const rhelKernelName = "kernel-core"
var kernelRegex = regexp.MustCompile(linuxImageRegex)
func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
var software []fleet.Software
sPaths := map[string]struct{}{}
@@ -1826,6 +1832,10 @@ 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) {
s.IsKernel = true
}
MutateSoftwareOnIngestion(s, logger)
if shouldRemoveSoftware(host, s) {