Add more test coverage for Fleet-maintained apps (#27722)

For #27377.

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Added/updated automated tests
This commit is contained in:
Ian Littman
2025-04-03 09:18:27 -05:00
committed by GitHub
parent 5385662098
commit 0f2f085812
6 changed files with 332 additions and 27 deletions
+84
View File
@@ -2,7 +2,10 @@ package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
@@ -11,6 +14,7 @@ import (
ma "github.com/fleetdm/fleet/v4/ee/maintained-apps"
"github.com/fleetdm/fleet/v4/server/authz"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
@@ -253,3 +257,83 @@ func TestGetMaintainedAppAuth(t *testing.T) {
})
}
}
func TestAddFleetMaintainedApp(t *testing.T) {
installerBytes := []byte("abc")
// this is the hash we expect to get in the DB
h := sha256.New()
_, err := h.Write(installerBytes)
require.NoError(t, err)
spoofedSHA := hex.EncodeToString(h.Sum(nil))
ds := new(mock.Store)
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.GetMaintainedAppByIDFunc = func(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
return &fleet.MaintainedApp{
ID: 1,
Name: "Internet Exploder",
Slug: "iexplode/windows",
Platform: "windows",
TitleID: nil,
UniqueIdentifier: "Internet Exploder",
}, nil
}
ds.MatchOrCreateSoftwareInstallerFunc = func(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, uint, error) {
require.Equal(t, spoofedSHA, payload.StorageID)
require.Empty(t, payload.BundleIdentifier)
require.Equal(t, "Internet Exploder", payload.Title)
require.Equal(t, "programs", payload.Source)
require.Equal(t, "Hello World!", payload.InstallScript)
require.Equal(t, "Hello World!", payload.UninstallScript)
// Can't easily inject a proper fleet.service so we bail early before NewActivity gets called and panics
return 0, 0, errors.New("forced error to short-circuit storage and activity creation")
}
// Mock server to serve the "installer"
installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(installerBytes)
}))
defer installerServer.Close()
// Mock server to serve the manifest
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var versions []*ma.FMAManifestApp
versions = append(versions, &ma.FMAManifestApp{
Version: "6.0",
Queries: ma.FMAQueries{
Exists: "SELECT 1 FROM osquery_info;",
},
InstallerURL: installerServer.URL + "/iexplode.exe",
InstallScriptRef: "foobaz",
UninstallScriptRef: "foobaz",
SHA256: noCheckHash,
})
manifest := ma.FMAManifestFile{
Versions: versions,
Refs: map[string]string{
"foobaz": "Hello World!",
},
}
err := json.NewEncoder(w).Encode(manifest)
require.NoError(t, err)
}))
t.Cleanup(manifestServer.Close)
os.Setenv("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL)
defer os.Unsetenv("FLEET_DEV_MAINTAINED_APPS_BASE_URL")
svc := newTestService(t, ds)
authCtx := authz_ctx.AuthorizationContext{}
ctx := authz_ctx.NewContext(context.Background(), &authCtx)
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
_, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil)
require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation")
require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked)
}
@@ -63,6 +63,12 @@ func TestGenerateErrors(t *testing.T) {
PackageIDs: []string{""},
})
require.ErrorIs(t, err, ErrMissingTitle)
_, err = Generate(FMAInstallerMetadata{})
require.ErrorIs(t, err, ErrMissingTitle)
_, err = FMAInstallerMetadata{}.PolicyDescription()
require.ErrorIs(t, err, ErrMissingTitle)
}
func TestGenerate(t *testing.T) {
@@ -99,6 +99,10 @@ func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 2"})
require.NoError(t, err)
// Testing search that returns no results; nothing inserted yet case
_, _, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
require.ErrorIs(t, err, &fleet.NoMaintainedAppsInDatabaseError{})
maintained1, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Maintained1",
Slug: "maintained1",
@@ -203,6 +207,23 @@ func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
require.False(t, meta.HasNextResults)
require.True(t, meta.HasPreviousResults)
// Testing search
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{MatchQuery: "Maintained4", IncludeMetadata: true})
require.NoError(t, err)
require.Len(t, apps, 1)
require.EqualValues(t, 1, meta.TotalResults)
require.Equal(t, expectedApps[3:], apps)
require.False(t, meta.HasNextResults)
require.False(t, meta.HasPreviousResults)
// Testing search that returns no results; non-error case
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{MatchQuery: "Maintained5", IncludeMetadata: true})
require.NoError(t, err)
require.Len(t, apps, 0)
require.EqualValues(t, 0, meta.TotalResults)
require.False(t, meta.HasNextResults)
require.False(t, meta.HasPreviousResults)
//
// Test including software title ID for existing apps (installers)
@@ -13,33 +13,7 @@ func init() {
}
func Up_20250320200000(tx *sql.Tx) error {
// Clean up Fleet Library App associated scripts before we drop the columns on the table
_, err := tx.Exec(`DELETE FROM
script_contents
WHERE
NOT EXISTS (
SELECT 1 FROM host_script_results WHERE script_content_id = script_contents.id)
AND NOT EXISTS (
SELECT 1 FROM scripts WHERE script_content_id = script_contents.id)
AND NOT EXISTS (
SELECT 1 FROM software_installers si
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id, si.uninstall_script_content_id)
)
AND NOT EXISTS (
SELECT 1 FROM fleet_library_apps fla
WHERE script_contents.id IN (fla.install_script_content_id, fla.uninstall_script_content_id)
)
AND NOT EXISTS (
SELECT 1 FROM setup_experience_scripts WHERE script_content_id = script_contents.id
)
AND NOT EXISTS (
SELECT 1 FROM script_upcoming_activities WHERE script_content_id = script_contents.id
)`)
if err != nil {
return fmt.Errorf("failed to clean up unused scripts: %w", err)
}
_, err = tx.Exec(`
_, err := tx.Exec(`
ALTER TABLE software_installers
CHANGE COLUMN fleet_library_app_id fleet_maintained_app_id INT unsigned DEFAULT NULL
`)
@@ -84,6 +58,28 @@ ALTER TABLE fleet_maintained_apps
return fmt.Errorf("failed to alter fleet_maintained_apps: %w", err)
}
// Clean up scripts that were only associated with FMAs
_, err = tx.Exec(`DELETE FROM
script_contents
WHERE
NOT EXISTS (
SELECT 1 FROM host_script_results WHERE script_content_id = script_contents.id)
AND NOT EXISTS (
SELECT 1 FROM scripts WHERE script_content_id = script_contents.id)
AND NOT EXISTS (
SELECT 1 FROM software_installers si
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id, si.uninstall_script_content_id)
)
AND NOT EXISTS (
SELECT 1 FROM setup_experience_scripts WHERE script_content_id = script_contents.id
)
AND NOT EXISTS (
SELECT 1 FROM script_upcoming_activities WHERE script_content_id = script_contents.id
)`)
if err != nil {
return fmt.Errorf("failed to clean up unused scripts: %w", err)
}
_, err = tx.Exec(`UPDATE fleet_maintained_apps SET slug = concat(slug, '/', platform)`)
if err != nil {
return fmt.Errorf("failed to rename FMA slugs: %w", err)
@@ -0,0 +1,149 @@
package tables
import (
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"github.com/stretchr/testify/require"
)
func TestUp_20250320200000(t *testing.T) {
db := applyUpToPrev(t)
// Insert a scheduled and a triggered job run for maintained_apps
execNoErr(t, db, `INSERT INTO cron_stats (name, instance, stats_type, status) VALUES (?, 'foo', ?, ?)`, fleet.CronMaintainedApps, fleet.CronStatsTypeScheduled, fleet.CronStatsStatusCompleted)
execNoErr(t, db, `INSERT INTO cron_stats (name, instance, stats_type, status) VALUES (?, 'foo', ?, ?)`, fleet.CronMaintainedApps, fleet.CronStatsTypeTriggered, fleet.CronStatsStatusCompleted)
// Add the old Zoom, Zoom for IT Admins, and Box Drive FMAs
tx, err := db.Begin()
require.NoError(t, err)
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
installScriptID, err := getOrInsertScript(txx, "echo install")
require.NoError(t, err)
uninstallScriptID, err := getOrInsertScript(txx, "echo uninstall")
require.NoError(t, err)
installScriptID2, err := getOrInsertScript(txx, "echo install2")
require.NoError(t, err)
uninstallScriptID2, err := getOrInsertScript(txx, "echo uninstall2")
require.NoError(t, err)
installScriptID3, err := getOrInsertScript(txx, "echo install different")
require.NoError(t, err)
otherScriptID, err := getOrInsertScript(txx, "just a lil scripty boi")
require.NoError(t, err)
err = tx.Commit()
require.NoError(t, err)
execNoErr(
t,
db,
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"Zoom",
"zoom",
"6.2.11.43613",
"darwin",
"https://cdn.zoom.us/prod/6.2.11.43613/arm64/zoomusInstallerFull.pkg",
"dd6d28853eb6be7eaf7731aae1855c68cd6411ef6847158e6af18fffed5f8597",
"us.zoom.xos",
installScriptID,
uninstallScriptID,
)
execNoErr(
t,
db,
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"Zoom for IT Admins",
"zoom-for-it-admins",
"6.2.11.43613",
"darwin",
"https://cdn.zoom.us/prod/6.2.11.43613/arm64/zoomusInstallerFull.pkg",
"dd6d28853eb6be7eaf7731aae1855c68cd6411ef6847158e6af18fffed5f8597",
"us.zoom.xos",
installScriptID,
uninstallScriptID,
)
boxFMAID := execNoErrLastID(
t,
db,
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
"Box Drive",
"box-drive",
"2.42.212",
"darwin",
"https://e3.boxcdn.net/desktop/releases/mac/BoxDrive-2.42.212.pkg",
"93550756150c434bc058c30b82352c294a21e978caf436ac99e0a5f431adfb6e",
"com.box.desktop",
installScriptID2,
uninstallScriptID2,
)
// add a software installer for Box to No team, same install scripts
noTeamBox := execNoErrLastID(t, db, `
INSERT INTO software_installers
(filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id, fleet_library_app_id)
VALUES
(?,?,?,?,?,?,?,?)`, "box.pkg", "2.42.212", "darwin", installScriptID2, "sha-is-not-president", "", uninstallScriptID2, boxFMAID)
// add a software installer for Box to another team, different install script
teamID := execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ("Foo")`)
otherTeamBox := execNoErrLastID(t, db, `
INSERT INTO software_installers
(team_id, global_or_team_id, filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id)
VALUES
(?,?,?,?,?,?,?,?,?)`, teamID, teamID, "box.pkg", "2.42.212", "darwin", installScriptID3, "sha-is-not-president", "", uninstallScriptID2)
// add a separate script to No team
execNoErr(t, db, `INSERT INTO scripts (
team_id, global_or_team_id, name, script_content_id
) VALUES (?, ?, ?, ?)`, nil, 0, "myscript.sh", otherScriptID)
// Apply current migration.
applyNext(t, db)
// install/uninstall scripts for Zoom should be gone
// Box script should remain, other script should remain
var scriptContentsIDs []int64
err = db.Select(&scriptContentsIDs, `SELECT id FROM script_contents ORDER BY id`)
require.NoError(t, err)
require.Equal(t, []int64{installScriptID2, uninstallScriptID2, installScriptID3, otherScriptID}, scriptContentsIDs)
// Should only have one Zoom plus Box
var fmas []fleet.MaintainedApp
err = db.Select(&fmas, `SELECT id, name, slug, unique_identifier FROM fleet_maintained_apps ORDER BY name`)
require.NoError(t, err)
require.Len(t, fmas, 2)
require.Equal(t, "Box Drive", fmas[0].Name)
require.Equal(t, "box-drive/darwin", fmas[0].Slug)
require.Equal(t, "com.box.desktop", fmas[0].UniqueIdentifier)
require.Equal(t, "Zoom", fmas[1].Name)
require.Equal(t, "zoom/darwin", fmas[1].Slug)
require.Equal(t, "us.zoom.xos", fmas[1].UniqueIdentifier)
var linkedFMAID *int64
// FMA ID for Box software installer on No team should match ID of Box FMA
err = db.Get(&linkedFMAID, `SELECT fleet_maintained_app_id FROM software_installers WHERE id = ?`, noTeamBox)
require.NoError(t, err)
require.Equal(t, boxFMAID, *linkedFMAID)
// FMA ID for Box software installer on other team should be null
err = db.Get(&linkedFMAID, `SELECT fleet_maintained_app_id FROM software_installers WHERE id = ?`, otherTeamBox)
require.NoError(t, err)
require.Nil(t, linkedFMAID)
// Only the triggered job record should remain in the cron_stats table
var stats []fleet.CronStats
err = db.Select(&stats, `SELECT name, instance, stats_type, status FROM cron_stats`)
require.NoError(t, err)
require.Len(t, stats, 1)
require.Equal(t, string(fleet.CronMaintainedApps), stats[0].Name)
require.Equal(t, fleet.CronStatsTypeTriggered, stats[0].StatsType)
require.Equal(t, fleet.CronStatsStatusCompleted, stats[0].Status)
}
@@ -0,0 +1,49 @@
package maintained_apps
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/stretchr/testify/require"
)
func TestInstallerFilenameExtraction(t *testing.T) {
// Mock server to serve the "installers"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/redirect":
w.Header().Set("Location", "/redirected%20package.exe")
w.WriteHeader(302)
_, _ = w.Write([]byte("redirecting"))
case "/redirected%20package.exe":
_, _ = w.Write([]byte("redirected fallback"))
case "/compliant":
w.Header().Set("Content-Disposition", `attachment; filename="compliant.msi"`)
_, _ = w.Write([]byte("compliant"))
case "/not_compliant":
w.Header().Set("Content-Disposition", `attachment; filename=not_compliant.pkg`)
_, _ = w.Write([]byte("not_compliant"))
}
}))
defer srv.Close()
// follow redirect and fall back to URL, after sanitization, when we don't have a content-disposition header
client := fleethttp.NewClient(fleethttp.WithTimeout(time.Second))
_, filename, err := DownloadInstaller(context.Background(), srv.URL+"/redirect", client)
require.NoError(t, err)
require.Equal(t, "redirected package.exe", filename)
// handle properly formatted content-disposition header
_, filename, err = DownloadInstaller(context.Background(), srv.URL+"/compliant", client)
require.NoError(t, err)
require.Equal(t, "compliant.msi", filename)
// handle non-compliant content-disposition header
_, filename, err = DownloadInstaller(context.Background(), srv.URL+"/not_compliant", client)
require.NoError(t, err)
require.Equal(t, "not_compliant.pkg", filename)
}