fleet/cmd/fleet/cron_test.go
Konstantin Sykulev d7b6b3c018
Use OSV for ubuntu vulnerability scanning (#42063)
**Related issue:** Resolves #40057

# Checklist for submitter

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

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* OSV (Open Source Vulnerabilities) added as an optional Ubuntu
vulnerability data source and enabled by default.

* **Features**
* Integrated OSV into the vulnerability scanning pipeline, artifact
sync/refresh, detection, and cleanup flows.
* Improved Ubuntu package/kernel version matching for more accurate OSV
detections.

* **Chores**
  * Added configuration flag and updated expected config fixtures.

* **Tests**
* Added extensive tests for OSV sync, artifact handling, analyzer logic,
and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-03 15:59:32 -05:00

250 lines
8.7 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
"github.com/fleetdm/fleet/v4/server/test"
)
func TestNewAppleMDMProfileManagerWithoutConfig(t *testing.T) {
ctx := context.Background()
mdmStorage := &mdmmock.MDMAppleStore{}
ds := new(mock.Store)
kv := new(mock.AdvancedKVStore)
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, nil)
logger := slog.New(slog.DiscardHandler)
sch, err := newAppleMDMProfileManagerSchedule(ctx, "foo", ds, cmdr, kv, logger, 0)
require.NotNil(t, sch)
require.NoError(t, err)
}
func TestNewWindowsMDMProfileManagerWithoutConfig(t *testing.T) {
ctx := context.Background()
ds := new(mock.Store)
logger := slog.New(slog.DiscardHandler)
sch, err := newWindowsMDMProfileManagerSchedule(ctx, "foo", ds, logger)
require.NotNil(t, sch)
require.NoError(t, err)
}
func TestMigrateABMTokenDuringDEPCronJob(t *testing.T) {
ctx := context.Background()
ds := mysql.CreateMySQLDS(t)
depStorage, err := ds.NewMDMAppleDEPStorage()
require.NoError(t, err)
// to avoid issues with syncer, use that constant as org name for now
const tokenOrgName = "fleet"
// insert an ABM token as if it had been migrated by the DB migration script
tok := mysql.SetTestABMAssets(t, ds, "")
// tok, err := ds.InsertABMToken(ctx, &fleet.ABMToken{EncryptedToken: abmToken, RenewAt: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)})
// require.NoError(t, err)
require.Empty(t, tok.OrganizationName)
// start a server that will mock the Apple DEP API
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
switch r.URL.Path {
case "/session":
_, _ = w.Write([]byte(`{"auth_session_token": "session123"}`))
case "/account":
_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "admin123", "org_name": "%s"}`, tokenOrgName)))
case "/profile":
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"})
require.NoError(t, err)
case "/server/devices":
err := encoder.Encode(godep.DeviceResponse{Devices: nil})
require.NoError(t, err)
case "/devices/sync":
err := encoder.Encode(godep.DeviceResponse{Devices: nil})
require.NoError(t, err)
default:
t.Errorf("unexpected request to %s", r.URL.Path)
}
}))
t.Cleanup(srv.Close)
err = depStorage.StoreConfig(ctx, tokenOrgName, &nanodep_client.Config{BaseURL: srv.URL})
require.NoError(t, err)
err = depStorage.StoreConfig(ctx, apple_mdm.UnsavedABMTokenOrgName, &nanodep_client.Config{BaseURL: srv.URL})
require.NoError(t, err)
logger := slog.New(slog.DiscardHandler)
syncFn := appleMDMDEPSyncerJob(ds, depStorage, logger)
err = syncFn(ctx)
require.NoError(t, err)
// token has been updated with its org name/apple id
tok, err = ds.GetABMTokenByOrgName(ctx, tokenOrgName)
require.NoError(t, err)
require.Equal(t, tokenOrgName, tok.OrganizationName)
require.Equal(t, "admin123", tok.AppleID)
require.Nil(t, tok.MacOSDefaultTeamID)
require.Nil(t, tok.IOSDefaultTeamID)
require.Nil(t, tok.IPadOSDefaultTeamID)
// empty-name token does not exist anymore
_, err = ds.GetABMTokenByOrgName(ctx, "")
require.Error(t, err)
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
// the default profile was created
defProf, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
require.NoError(t, err)
require.NotNil(t, defProf)
require.NotEmpty(t, defProf.Token)
// no profile UUID was assigned for no-team (because there are no hosts right now)
_, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, nil, "")
require.Error(t, err)
require.ErrorAs(t, err, &nfe)
// no teams, so no team-specific custom setup assistants
teams, err := ds.ListTeams(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.ListOptions{})
require.NoError(t, err)
require.Empty(t, teams)
// no no-team custom setup assistant
_, err = ds.GetMDMAppleSetupAssistant(ctx, nil)
require.ErrorIs(t, err, sql.ErrNoRows)
// no host got created
hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{})
require.NoError(t, err)
require.Empty(t, hosts)
}
func TestCleanupStaleOSVVulnerabilities(t *testing.T) {
ctx := t.Context()
logger := slog.New(slog.DiscardHandler)
ds := mysql.CreateMySQLDS(t)
// Create test software
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
software := []fleet.Software{
{Name: "pkg1", Version: "1.0", Source: "apps"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
require.Len(t, host.Software, 1)
// Insert vulnerabilities from both sources
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID,
CVE: "CVE-2024-0001",
}, fleet.UbuntuOSVSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID,
CVE: "CVE-2024-0002",
}, fleet.UbuntuOVALSource)
require.NoError(t, err)
t.Run("OSV enabled - does not delete OSV vulnerabilities", func(t *testing.T) {
cleanupStaleOSVVulnerabilities(ctx, ds, logger, true)
// Both vulnerabilities should still exist
osvVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOSVSource)
require.NoError(t, err)
require.Len(t, osvVulns[host.ID], 1)
ovalVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
require.NoError(t, err)
require.Len(t, ovalVulns[host.ID], 1)
})
t.Run("OSV disabled - deletes only UbuntuOSVSource vulnerabilities", func(t *testing.T) {
cleanupStaleOSVVulnerabilities(ctx, ds, logger, false)
// OSV vulnerabilities should be deleted
osvVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOSVSource)
require.NoError(t, err)
require.Empty(t, osvVulns[host.ID])
// OVAL vulnerability should remain
ovalVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
require.NoError(t, err)
require.Len(t, ovalVulns[host.ID], 1)
require.Equal(t, "CVE-2024-0002", ovalVulns[host.ID][0].CVE)
})
}
func TestCleanupStaleOVALVulnerabilities(t *testing.T) {
ctx := context.Background()
logger := slog.New(slog.DiscardHandler)
ds := mysql.CreateMySQLDS(t)
// Create test software
host := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
software := []fleet.Software{
{Name: "pkg2", Version: "2.0", Source: "apps"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
require.Len(t, host.Software, 1)
// Insert vulnerabilities from multiple sources
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID,
CVE: "CVE-2024-0003",
}, fleet.UbuntuOSVSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID,
CVE: "CVE-2024-0004",
}, fleet.UbuntuOVALSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID,
CVE: "CVE-2024-0005",
}, fleet.RHELOVALSource)
require.NoError(t, err)
t.Run("deletes only UbuntuOVALSource vulnerabilities, preserves others", func(t *testing.T) {
cleanupStaleOVALVulnerabilities(ctx, ds, logger)
// Ubuntu OVAL vulnerabilities should be deleted
ovalVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
require.NoError(t, err)
require.Empty(t, ovalVulns[host.ID])
// OSV vulnerabilities should remain
osvVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOSVSource)
require.NoError(t, err)
require.Len(t, osvVulns[host.ID], 1)
require.Equal(t, "CVE-2024-0003", osvVulns[host.ID][0].CVE)
// RHEL OVAL vulnerabilities should remain
rhelVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.RHELOVALSource)
require.NoError(t, err)
require.Len(t, rhelVulns[host.ID], 1)
require.Equal(t, "CVE-2024-0005", rhelVulns[host.ID][0].CVE)
})
}