mirror of
https://github.com/fleetdm/fleet
synced 2026-05-14 20:48:35 +00:00
Closes https://github.com/fleetdm/fleet/issues/32257 # 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 For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results
679 lines
25 KiB
Go
679 lines
25 KiB
Go
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"
|
|
)
|
|
|
|
func TestOperatingSystemVulnerabilities(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"ListOSVulnerabilitiesEmpty", testListOSVulnerabilitiesByOSEmpty},
|
|
{"ListOSVulnerabilities", testListOSVulnerabilitiesByOS},
|
|
{"ListVulnsByOsNameAndVersion", testListVulnsByOsNameAndVersion},
|
|
{"InsertOSVulnerabilities", testInsertOSVulnerabilities},
|
|
{"InsertSingleOSVulnerability", testInsertOSVulnerability},
|
|
{"DeleteOSVulnerabilitiesEmpty", testDeleteOSVulnerabilitiesEmpty},
|
|
{"DeleteOSVulnerabilities", testDeleteOSVulnerabilities},
|
|
{"DeleteOutOfDateOSVulnerabilities", testDeleteOutOfDateOSVulnerabilities},
|
|
{"TestListKernelsByOS", testListKernelsByOS},
|
|
{"TestKernelVulnsHostCount", testKernelVulnsHostCount},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testListOSVulnerabilitiesByOSEmpty(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Empty(t, actual)
|
|
}
|
|
|
|
func testListOSVulnerabilitiesByOS(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
vulns := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3")},
|
|
{CVE: "cve-3", OSID: 1, ResolvedInVersion: ptr.String("10.14.2")},
|
|
{CVE: "cve-2", OSID: 1, ResolvedInVersion: ptr.String("8.123.1")},
|
|
{CVE: "cve-1", OSID: 2, ResolvedInVersion: ptr.String("1.2.3")},
|
|
{CVE: "cve-5", OSID: 2, ResolvedInVersion: ptr.String("10.14.2")},
|
|
}
|
|
|
|
for _, v := range vulns {
|
|
_, err := ds.InsertOSVulnerability(ctx, v, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("returns matching", func(t *testing.T) {
|
|
expected := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3"), Source: fleet.MSRCSource},
|
|
{CVE: "cve-3", OSID: 1, ResolvedInVersion: ptr.String("10.14.2"), Source: fleet.MSRCSource},
|
|
{CVE: "cve-2", OSID: 1, ResolvedInVersion: ptr.String("8.123.1"), Source: fleet.MSRCSource},
|
|
}
|
|
|
|
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, expected, actual)
|
|
})
|
|
}
|
|
|
|
func testListVulnsByOsNameAndVersion(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
seedOS := []fleet.OperatingSystem{
|
|
{
|
|
Name: "Microsoft Windows 11 Pro 21H2",
|
|
Version: "10.0.22000.795",
|
|
Arch: "64-bit",
|
|
KernelVersion: "10.0.22000.795",
|
|
Platform: "windows",
|
|
DisplayVersion: "21H2",
|
|
},
|
|
{
|
|
Name: "Microsoft Windows 11 Pro 21H2",
|
|
Version: "10.0.22000.795",
|
|
Arch: "ARM 64-bit",
|
|
KernelVersion: "10.0.22000.795",
|
|
Platform: "windows",
|
|
DisplayVersion: "21H2",
|
|
},
|
|
{
|
|
Name: "Microsoft Windows 11 Pro 22H2",
|
|
Version: "10.0.22621.890",
|
|
Arch: "64-bit",
|
|
KernelVersion: "10.0.22621.890",
|
|
Platform: "windows",
|
|
DisplayVersion: "22H2",
|
|
},
|
|
}
|
|
|
|
var dbOS []fleet.OperatingSystem
|
|
for _, seed := range seedOS {
|
|
os, err := newOperatingSystemDB(context.Background(), ds.writer(context.Background()), seed)
|
|
require.NoError(t, err)
|
|
dbOS = append(dbOS, *os)
|
|
}
|
|
|
|
cves, err := ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, cves)
|
|
|
|
mockTime := time.Date(2024, time.January, 18, 10, 0, 0, 0, time.UTC)
|
|
|
|
cveMeta := []fleet.CVEMeta{
|
|
{
|
|
CVE: "CVE-2021-1234",
|
|
CVSSScore: ptr.Float64(9.7),
|
|
EPSSProbability: ptr.Float64(4.2),
|
|
CISAKnownExploit: ptr.Bool(true),
|
|
Published: ptr.Time(mockTime),
|
|
Description: "A bad vulnerability",
|
|
},
|
|
{
|
|
CVE: "CVE-2021-1235",
|
|
CVSSScore: ptr.Float64(9.8),
|
|
EPSSProbability: ptr.Float64(0.1),
|
|
CISAKnownExploit: ptr.Bool(false),
|
|
Published: ptr.Time(mockTime),
|
|
Description: "A worse vulnerability",
|
|
},
|
|
{
|
|
CVE: "CVE-2021-1236",
|
|
CVSSScore: ptr.Float64(9.8),
|
|
EPSSProbability: ptr.Float64(0.1),
|
|
CISAKnownExploit: ptr.Bool(false),
|
|
Published: ptr.Time(mockTime),
|
|
Description: "A terrible vulnerability",
|
|
},
|
|
}
|
|
|
|
err = ds.InsertCVEMeta(ctx, cveMeta)
|
|
require.NoError(t, err)
|
|
|
|
// add CVEs for each OS with different architectures
|
|
vulns := []fleet.OSVulnerability{
|
|
{CVE: "CVE-2021-1234", OSID: dbOS[0].ID, ResolvedInVersion: ptr.String("1.2.3")},
|
|
{CVE: "CVE-2021-1235", OSID: dbOS[1].ID, ResolvedInVersion: ptr.String("10.14.2")},
|
|
{CVE: "CVE-2021-1236", OSID: dbOS[2].ID, ResolvedInVersion: ptr.String("103.2.1")},
|
|
}
|
|
|
|
_, err = ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
|
|
// push other vulns into the past to ensure "SELECT DISTINCT" wouldn't deduplicate properly
|
|
_, err = ds.writer(ctx).ExecContext(ctx, "UPDATE operating_system_vulnerabilities SET created_at = NOW() - INTERVAL 5 SECOND")
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.InsertOSVulnerabilities(ctx, []fleet.OSVulnerability{
|
|
{CVE: "CVE-2021-1234", OSID: dbOS[1].ID, ResolvedInVersion: ptr.String("1.2.3")}, // same OS, different arch
|
|
}, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
|
|
// test without CVS meta
|
|
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"}
|
|
require.Len(t, cves, 2)
|
|
for _, cve := range cves {
|
|
require.Contains(t, expected, cve.CVE)
|
|
require.Greater(t, cve.CreatedAt, time.Now().Add(-time.Hour)) // assert non-zero time
|
|
}
|
|
|
|
// test with CVS meta
|
|
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)
|
|
|
|
require.Equal(t, cveMeta[0].CVE, cves[0].CVE)
|
|
require.Equal(t, &cveMeta[0].CVSSScore, cves[0].CVSSScore)
|
|
require.Equal(t, &cveMeta[0].EPSSProbability, cves[0].EPSSProbability)
|
|
require.Equal(t, &cveMeta[0].CISAKnownExploit, cves[0].CISAKnownExploit)
|
|
require.Equal(t, cveMeta[0].Published, *cves[0].CVEPublished)
|
|
require.Equal(t, cveMeta[0].Description, **cves[0].Description)
|
|
require.Equal(t, cveMeta[1].CVE, cves[1].CVE)
|
|
require.Equal(t, &cveMeta[1].CVSSScore, cves[1].CVSSScore)
|
|
require.Equal(t, &cveMeta[1].EPSSProbability, cves[1].EPSSProbability)
|
|
require.Equal(t, &cveMeta[1].CISAKnownExploit, cves[1].CISAKnownExploit)
|
|
require.Equal(t, cveMeta[1].Published, *cves[1].CVEPublished)
|
|
require.Equal(t, cveMeta[1].Description, **cves[1].Description)
|
|
}
|
|
|
|
func testInsertOSVulnerabilities(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
vulns := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1},
|
|
{CVE: "cve-3", OSID: 1},
|
|
{CVE: "cve-2", OSID: 1},
|
|
}
|
|
|
|
c, err := ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(3), c)
|
|
|
|
expected := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1, Source: fleet.MSRCSource},
|
|
{CVE: "cve-3", OSID: 1, Source: fleet.MSRCSource},
|
|
{CVE: "cve-2", OSID: 1, Source: fleet.MSRCSource},
|
|
}
|
|
|
|
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func testInsertOSVulnerability(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
vulns := fleet.OSVulnerability{
|
|
CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3"),
|
|
}
|
|
|
|
vulnsUpdate := fleet.OSVulnerability{
|
|
CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.4"),
|
|
}
|
|
|
|
vulnNoCVE := fleet.OSVulnerability{
|
|
OSID: 1, ResolvedInVersion: ptr.String("1.2.4"),
|
|
}
|
|
|
|
// Inserting a vulnerability with no CVE should not insert anything
|
|
didInsert, err := ds.InsertOSVulnerability(ctx, vulnNoCVE, fleet.MSRCSource)
|
|
require.Error(t, err)
|
|
require.False(t, didInsert)
|
|
|
|
// Inserting a vulnerability with a CVE should insert
|
|
didInsert, err = ds.InsertOSVulnerability(ctx, vulns, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
require.True(t, didInsert)
|
|
|
|
// Inserting the same vulnerability should not insert, but update
|
|
didInsert, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
assert.False(t, didInsert)
|
|
|
|
// Inserting the exact same vulnerability again may or may not change updated_at, but qualifies as an update
|
|
didInsert, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
assert.False(t, didInsert)
|
|
|
|
// simulate vuln in the past to make sure updated_at gets set
|
|
_, err = ds.writer(ctx).ExecContext(ctx, "UPDATE operating_system_vulnerabilities SET updated_at = NOW() - INTERVAL 5 MINUTE WHERE operating_system_id = 1")
|
|
require.NoError(t, err)
|
|
|
|
// Inserting the exact same vulnerability again will update again, as we need to bump updated_at
|
|
didInsert, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
assert.False(t, didInsert)
|
|
|
|
// make sure the update happened
|
|
var recentRows uint
|
|
require.NoError(t, sqlx.Get(ds.writer(ctx), &recentRows, "SELECT COUNT(*) FROM operating_system_vulnerabilities WHERE operating_system_id = 1 AND updated_at > NOW() - INTERVAL 5 SECOND"))
|
|
require.Equal(t, uint(1), recentRows)
|
|
|
|
expected := vulnsUpdate
|
|
expected.Source = fleet.MSRCSource
|
|
|
|
list1, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.Len(t, list1, 1)
|
|
require.Equal(t, expected, list1[0])
|
|
}
|
|
|
|
func testDeleteOSVulnerabilitiesEmpty(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
vulns := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1},
|
|
{CVE: "cve-1", OSID: 1},
|
|
{CVE: "cve-3", OSID: 1},
|
|
{CVE: "cve-2", OSID: 1},
|
|
}
|
|
|
|
err := ds.DeleteOSVulnerabilities(ctx, vulns)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testDeleteOSVulnerabilities(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
vulns := []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1},
|
|
{CVE: "cve-2", OSID: 1},
|
|
{CVE: "cve-3", OSID: 1},
|
|
}
|
|
|
|
c, err := ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(3), c)
|
|
|
|
toDelete := []fleet.OSVulnerability{
|
|
{CVE: "cve-2", OSID: 1},
|
|
}
|
|
|
|
err = ds.DeleteOSVulnerabilities(ctx, toDelete)
|
|
require.NoError(t, err)
|
|
|
|
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []fleet.OSVulnerability{
|
|
{CVE: "cve-1", OSID: 1, Source: fleet.MSRCSource},
|
|
{CVE: "cve-3", OSID: 1, Source: fleet.MSRCSource},
|
|
}, actual)
|
|
}
|
|
|
|
func testDeleteOutOfDateOSVulnerabilities(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
yesterday := time.Now().Add(-3 * time.Hour).Format("2006-01-02 15:04:05")
|
|
|
|
oldVuln := fleet.OSVulnerability{
|
|
CVE: "cve-1", OSID: 1,
|
|
}
|
|
|
|
newVuln := fleet.OSVulnerability{
|
|
CVE: "cve-2", OSID: 1,
|
|
}
|
|
|
|
_, err := ds.InsertOSVulnerability(ctx, oldVuln, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx, "UPDATE operating_system_vulnerabilities SET updated_at = ?", yesterday)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.InsertOSVulnerability(ctx, newVuln, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
|
|
// Delete out of date vulns
|
|
err = ds.DeleteOutOfDateOSVulnerabilities(ctx, fleet.NVDSource, time.Now().UTC().Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
|
|
require.NoError(t, err)
|
|
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))
|
|
}
|
|
|
|
updateMappings := func() {
|
|
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))
|
|
}
|
|
|
|
updateMappings()
|
|
|
|
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)
|
|
|
|
updateMappings()
|
|
|
|
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)
|
|
|
|
// Delete host 1. We should see the count for the kernel go down to 0.
|
|
require.NoError(t, ds.DeleteHost(ctx, host1.ID))
|
|
|
|
updateMappings()
|
|
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
var count uint
|
|
err := sqlx.GetContext(ctx, q, &count, "SELECT hosts_count FROM kernel_host_counts WHERE os_version_id = ?", os1.OSVersionID)
|
|
require.NoError(t, err)
|
|
assert.Zero(t, count)
|
|
return nil
|
|
})
|
|
|
|
kernels, err = ds.ListKernelsByOS(ctx, os1.OSVersionID, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, kernels)
|
|
|
|
}
|