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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+76
@@ -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
@@ -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 + `
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user