Override patch policy query (#42322)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41815
### Changes
- Extracted patch policy creation to `pkg/patch_policy`
- Added a `patch_query` column to the `software_installers` table
- By default that column is empty, and patch policies will generate with
the default query if so
- On app manifest ingestion, the appropriate entry in
`software_installers` will save the override "patch" query from the
manifest in patch_query

# 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.

- [ ] 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.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] 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)

- [ ] QA'd all new/changed functionality manually
- Relied on integration test for FMA version pinning

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] 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:
Jonathan Katz 2026-03-25 10:32:41 -04:00 committed by GitHub
parent a1860a9185
commit 0d15fd6cd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 487 additions and 123 deletions

View file

@ -0,0 +1 @@
- Added ability to specify custom patch policy query in an FMA manifest

View file

@ -17,7 +17,9 @@ import (
external_refs "github.com/fleetdm/fleet/v4/ee/maintained-apps/ingesters/homebrew/external_refs"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/pkg/patch_policy"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/ghodss/yaml"
)
func IngestApps(ctx context.Context, logger *slog.Logger, inputsPath, slugFilter string) ([]*maintained_apps.FMAManifestApp, error) {
@ -213,6 +215,26 @@ func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintain
external_refs.EnrichManifest(out)
// create patch policy
if input.PatchPolicyPath != "" {
policyBytes, err := os.ReadFile(input.PatchPolicyPath)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "reading provided patch policy path")
}
p := patch_policy.PolicyData{}
if err := yaml.Unmarshal(policyBytes, &p); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshaling patch policy")
}
p.Platform = "darwin"
p.Version = out.Version
out.Queries.Patch, err = patch_policy.GenerateFromManifest(p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating patch policy")
}
}
return out, nil
}
@ -233,6 +255,7 @@ type inputApp struct {
Frozen bool `json:"frozen"`
InstallScriptPath string `json:"install_script_path"`
UninstallScriptPath string `json:"uninstall_script_path"`
PatchPolicyPath string `json:"patch_policy_path"`
}
type brewCask struct {

View file

@ -25,6 +25,9 @@ func TestIngestValidations(t *testing.T) {
testUninstallScriptContents := "this is a test uninstall script"
require.NoError(t, os.WriteFile(path.Join(tempDir, "uninstall_script.sh"), []byte(testUninstallScriptContents), 0644))
testPatchPolicyContents := `query: "SELECT 1;"`
require.NoError(t, os.WriteFile(path.Join(tempDir, "policy.yml"), []byte(testPatchPolicyContents), 0644))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var cask brewCask
@ -86,7 +89,7 @@ func TestIngestValidations(t *testing.T) {
Version: "1.0",
}
case "ok", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post":
case "ok", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post", "patch_policy_path":
cask = brewCask{
Token: appToken,
Name: []string{appToken},
@ -123,6 +126,7 @@ func TestIngestValidations(t *testing.T) {
{"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}},
{"cannot provide pre-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_pre", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PreUninstallScripts: []string{"foo", "bar"}}},
{"cannot provide post-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_post", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PostUninstallScripts: []string{"foo", "bar"}}},
{"", inputApp{Token: "patch_policy_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", PatchPolicyPath: path.Join(tempDir, "policy.yml")}},
}
for _, c := range cases {
t.Run(c.inputApp.Token, func(t *testing.T) {
@ -148,6 +152,10 @@ func TestIngestValidations(t *testing.T) {
require.Equal(t, testUninstallScriptContents, out.UninstallScript)
}
if c.inputApp.PatchPolicyPath != "" {
require.Equal(t, "SELECT 1;", out.Queries.Patch)
}
})
}
}

View file

@ -16,6 +16,7 @@ import (
external_refs "github.com/fleetdm/fleet/v4/ee/maintained-apps/ingesters/winget/external_refs"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/patch_policy"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
feednvd "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd"
"github.com/google/go-github/v37/github"
@ -366,6 +367,26 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta
external_refs.EnrichManifest(&out)
// create patch policy
if input.PatchPolicyPath != "" {
policyBytes, err := os.ReadFile(input.PatchPolicyPath)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "reading provided patch policy path")
}
p := patch_policy.PolicyData{}
if err := yaml.Unmarshal(policyBytes, &p); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshaling patch policy")
}
p.Platform = "windows"
p.Version = out.Version
out.Queries.Patch, err = patch_policy.GenerateFromManifest(p)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creating patch policy")
}
}
return &out, nil
}
@ -433,6 +454,7 @@ type inputApp struct {
IgnoreHash bool `json:"ignore_hash"`
DefaultCategories []string `json:"default_categories"`
Frozen bool `json:"frozen"`
PatchPolicyPath string `json:"patch_policy_path"`
}
type installerManifest struct {

View file

@ -18,6 +18,7 @@ const OutputPath = "ee/maintained-apps/outputs"
type FMAQueries struct {
Exists string `json:"exists"`
Patch string `json:"patch"`
}
type FMAManifestApp struct {

View file

@ -159,6 +159,7 @@ func (svc *Service) AddFleetMaintainedApp(
AutomaticInstallQuery: app.AutomaticInstallQuery,
Categories: app.Categories,
URL: app.InstallerURL,
PatchQuery: app.PatchQuery,
}
payload.Categories = server.RemoveDuplicatesFromSlice(payload.Categories)

View file

@ -2144,7 +2144,7 @@ func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payloa
}
return err
}
_, err = maintained_apps.Hydrate(ctx, app, payload.RollbackVersion, teamID, svc.ds)
fma, err := maintained_apps.Hydrate(ctx, app, payload.RollbackVersion, teamID, svc.ds)
if err != nil {
return err
}
@ -2164,6 +2164,7 @@ func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payloa
if len(payload.Categories) == 0 {
payload.Categories = app.Categories
}
payload.MaintainedApp.PatchQuery = fma.PatchQuery
return nil
}
@ -2600,6 +2601,7 @@ func (svc *Service) softwareBatchUpload(
installer.BundleIdentifier = p.MaintainedApp.BundleIdentifier()
installer.StorageID = p.MaintainedApp.SHA256
installer.FleetMaintainedAppID = &p.MaintainedApp.ID
installer.PatchQuery = p.MaintainedApp.PatchQuery
}
var ext string

View file

@ -0,0 +1,87 @@
package patch_policy
import (
"errors"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
)
type PolicyData struct {
Name string
Query string
Platform string
Description string
Resolution string
Version string
}
const versionVariable = "$FMA_VERSION"
var (
ErrEmptyQuery = errors.New("query should not be empty")
ErrWrongPlatform = errors.New("platform should be darwin or windows")
)
// GenerateFromManifest replaces the $FMA_VERSION variable and checks platform
func GenerateFromManifest(p PolicyData) (string, error) {
if p.Query == "" {
return "", ErrEmptyQuery
}
// Version is extracted from the manifest so this should be safe to run as an osquery query
query := strings.ReplaceAll(p.Query, versionVariable, p.Version)
switch p.Platform {
case "darwin":
case "windows":
default:
return "", ErrWrongPlatform
}
return query, nil
}
// GenerateFromInstaller creates a patch policy with all fields from an installer
func GenerateFromInstaller(p PolicyData, installer *fleet.SoftwareInstaller) (*PolicyData, error) {
// use the patch policy query from the app manifest if available
query := installer.PatchQuery
if p.Description == "" {
p.Description = "Outdated software might introduce security vulnerabilities or compatibility issues."
}
if p.Resolution == "" {
p.Resolution = "Install the latest version from self-service."
}
switch installer.Platform {
case "darwin":
if p.Name == "" {
p.Name = fmt.Sprintf("macOS - %s up to date", installer.SoftwareTitle)
}
if installer.PatchQuery == "" {
query = fmt.Sprintf(
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_short_version, '%s') < 0);",
installer.BundleIdentifier,
installer.Version,
)
}
case "windows":
if p.Name == "" {
p.Name = fmt.Sprintf("Windows - %s up to date", installer.SoftwareTitle)
}
if installer.PatchQuery == "" {
// TODO: use upgrade code to improve accuracy?
query = fmt.Sprintf(
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = '%s' AND version_compare(version, '%s') < 0);",
installer.SoftwareTitle,
installer.Version,
)
}
default:
return nil, ErrWrongPlatform
}
return &PolicyData{Query: query, Platform: installer.Platform, Name: p.Name, Description: p.Description, Resolution: p.Resolution}, nil
}

View file

@ -2001,13 +2001,13 @@ func testActivateScriptPackageInstallWithCorruptPayload(t *testing.T, ds *Datast
extension, version, platform, install_script_content_id,
pre_install_query, post_install_script_content_id, uninstall_script_content_id,
self_service, user_id, user_name, user_email, package_ids,
fleet_maintained_app_id, url, upgrade_code
fleet_maintained_app_id, url, upgrade_code, patch_query
)
VALUES (NULL, 0, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
VALUES (NULL, 0, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
`
res, err = ds.writer(ctx).ExecContext(ctx, installerStmt,
titleID, "storage-123", "test-script.sh", "sh", "", "linux", scriptContentID,
"", scriptContentID, 0, u.ID, u.Name, u.Email, "", "", "")
"", scriptContentID, 0, u.ID, u.Name, u.Email, "", "", "", "")
require.NoError(t, err)
installerID, _ := res.LastInsertId()
@ -2176,13 +2176,13 @@ func testActivateScriptPackageUninstallWithCorruptPayload(t *testing.T, ds *Data
extension, version, platform, install_script_content_id,
pre_install_query, post_install_script_content_id, uninstall_script_content_id,
self_service, user_id, user_name, user_email, package_ids,
fleet_maintained_app_id, url, upgrade_code
fleet_maintained_app_id, url, upgrade_code, patch_query
)
VALUES (NULL, 0, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
VALUES (NULL, 0, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
`
res, err = ds.writer(ctx).ExecContext(ctx, installerStmt,
titleID, "storage-id-uninstall", "test-uninstall.sh", "sh", "", "linux", scriptContentID,
"", scriptContentID, 0, u.ID, u.Name, u.Email, "", "", "")
"", scriptContentID, 0, u.ID, u.Name, u.Email, "", "", "", "")
require.NoError(t, err)
installerID, _ := res.LastInsertId()

View file

@ -0,0 +1,26 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20260324161944, Down_20260324161944)
}
func Up_20260324161944(tx *sql.Tx) error {
stmt := `
ALTER TABLE software_installers
ADD COLUMN patch_query TEXT COLLATE utf8mb4_unicode_ci NOT NULL;
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("add patch_query to software_installers: %w", err)
}
return nil
}
func Down_20260324161944(tx *sql.Tx) error {
return nil
}

View file

@ -0,0 +1,55 @@
package tables
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestUp_20260324161944(t *testing.T) {
db := applyUpToPrev(t)
insertInstallerStmt := `
INSERT INTO software_installers (
team_id,
global_or_team_id,
title_id,
storage_id,
filename,
extension,
version,
install_script_content_id,
uninstall_script_content_id,
platform,
package_ids
) VALUES (NULL, 0, ?, "storage_id", ?, "pkg", "1.0", ?, ?, "darwin", "")
`
insertTitleStmt := `
INSERT INTO software_titles (name, source, bundle_identifier)
VALUES (?, 'apps', ?)
`
scriptID := execNoErrLastID(t, db, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(MD5('echo hello')), 'echo hello')`)
title1 := execNoErrLastID(t, db, insertTitleStmt, "App 1", "com.app1")
title2 := execNoErrLastID(t, db, insertTitleStmt, "App 2", "com.app2")
installer1 := execNoErrLastID(t, db, insertInstallerStmt, title1, "app1.pkg", scriptID, scriptID)
installer2 := execNoErrLastID(t, db, insertInstallerStmt, title2, "app2.pkg", scriptID, scriptID)
var timestamp, timestamp2 time.Time
require.NoError(t, db.Get(&timestamp, `SELECT updated_at FROM software_installers WHERE id = ?`, installer1))
// Apply current migration.
applyNext(t, db)
var patchQuery string
require.NoError(t, db.Get(&patchQuery, `SELECT patch_query FROM software_installers WHERE id = ?`, installer1))
require.Equal(t, "", patchQuery)
require.NoError(t, db.Get(&patchQuery, `SELECT patch_query FROM software_installers WHERE id = ?`, installer2))
require.Equal(t, "", patchQuery)
require.NoError(t, db.Get(&timestamp2, `SELECT updated_at FROM software_installers WHERE id = ?`, installer1))
require.Equal(t, timestamp, timestamp2)
}

View file

@ -14,6 +14,7 @@ import (
"golang.org/x/text/unicode/norm"
"github.com/fleetdm/fleet/v4/pkg/patch_policy"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
@ -1064,20 +1065,22 @@ func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *u
args.Type = fleet.PolicyTypeDynamic
}
if args.Type == fleet.PolicyTypePatch {
generated, err := ds.generatePatchPolicy(ctx, teamID, *args.PatchSoftwareTitleID)
installer, err := ds.getPatchPolicyInstaller(ctx, teamID, *args.PatchSoftwareTitleID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting patch policy installer")
}
generated, err := patch_policy.GenerateFromInstaller(patch_policy.PolicyData{
Name: args.Name,
Description: args.Description,
Resolution: args.Resolution,
}, installer)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "generating patch policy fields")
}
if args.Name == "" {
args.Name = generated.Name
}
if args.Description == "" {
args.Description = generated.Description
}
if args.Resolution == "" {
args.Resolution = generated.Resolution
}
args.Name = generated.Name
args.Description = generated.Description
args.Resolution = generated.Resolution
args.Platform = generated.Platform
args.Query = generated.Query
}
@ -1470,23 +1473,26 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
fmaTitleID := fmaTitleIDs[teamNameToID[spec.Team]][spec.FleetMaintainedAppSlug]
// generate new up-to-date query and other fields for patch policy
// generate new up-to-date patch policy
if spec.Type == fleet.PolicyTypePatch {
patch, err := ds.generatePatchPolicy(ctx, ptr.ValOrZero(teamID), *fmaTitleID)
installer, err := ds.getPatchPolicyInstaller(ctx, ptr.ValOrZero(teamID), *fmaTitleID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting patch policy installer")
}
generated, err := patch_policy.GenerateFromInstaller(patch_policy.PolicyData{
Name: spec.Name,
Description: spec.Description,
Resolution: spec.Resolution,
}, installer)
if err != nil {
return ctxerr.Wrap(ctx, err, "generating patch policy fields")
}
if spec.Name == "" {
spec.Name = patch.Name
}
if spec.Description == "" {
spec.Description = patch.Description
}
if spec.Resolution == "" {
spec.Resolution = patch.Resolution
}
spec.Platform = patch.Platform
spec.Query = patch.Query
spec.Name = generated.Name
spec.Description = generated.Description
spec.Resolution = generated.Resolution
spec.Platform = generated.Platform
spec.Query = generated.Query
}
res, err := tx.ExecContext(
@ -2624,15 +2630,7 @@ func (ds *Datastore) getPoliciesBySoftwareTitleIDs(
return policies, nil
}
type patchPolicy struct {
Name string
Query string
Platform string
Description string
Resolution string
}
func (ds *Datastore) generatePatchPolicy(ctx context.Context, teamID uint, titleID uint) (*patchPolicy, error) {
func (ds *Datastore) getPatchPolicyInstaller(ctx context.Context, teamID uint, titleID uint) (*fleet.SoftwareInstaller, error) {
installer, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &teamID, titleID, false)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software installer")
@ -2642,32 +2640,7 @@ func (ds *Datastore) generatePatchPolicy(ctx context.Context, teamID uint, title
Message: fmt.Sprintf("Software installer for Fleet maintained app with title ID %d does not exist for team ID %d", titleID, teamID),
})
}
var policy patchPolicy
switch {
case installer.Platform == string(fleet.MacOSPlatform):
policy.Query = fmt.Sprintf(
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_short_version, '%s') < 0);",
installer.BundleIdentifier,
installer.Version,
)
policy.Platform = string(fleet.MacOSPlatform)
policy.Name = fmt.Sprintf("macOS - %s up to date", installer.SoftwareTitle)
case installer.Platform == "windows":
// TODO: use upgrade code to improve accuracy?
policy.Query = fmt.Sprintf(
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = '%s' AND version_compare(bundle_short_version, '%s') < 0);",
installer.SoftwareTitle,
installer.Version,
)
policy.Platform = "windows"
policy.Name = fmt.Sprintf("Windows - %s up to date", installer.SoftwareTitle)
default:
}
policy.Description = "Outdated software might introduce security vulnerabilities or compatibility issues."
policy.Resolution = "Install the latest version from self-service."
return &policy, nil
return installer, nil
}
func (ds *Datastore) GetPatchPolicy(ctx context.Context, teamID *uint, titleID uint) (*fleet.PatchPolicyData, error) {

View file

@ -7006,8 +7006,8 @@ func testPolicyModificationResetsAttemptNumber(t *testing.T, ds *Datastore) {
installerID := int64(0)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
res, err := q.ExecContext(ctx, `
INSERT INTO software_installers (team_id, global_or_team_id, title_id, storage_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, platform, package_ids)
VALUES (?, ?, ?, 'storage', 'test.pkg', 'pkg', '1.0', ?, ?, 'darwin', '')
INSERT INTO software_installers (team_id, global_or_team_id, title_id, storage_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, platform, package_ids, patch_query)
VALUES (?, ?, ?, 'storage', 'test.pkg', 'pkg', '1.0', ?, ?, 'darwin', '', '')
`, team.ID, team.ID, titleID, scriptContentID, scriptContentID)
if err != nil {
return err
@ -7638,6 +7638,42 @@ func testTeamPatchPolicy(t *testing.T, ds *Datastore) {
_, err = ds.GetPatchPolicy(ctx, &team1.ID, titleID2)
require.True(t, fleet.IsNotFound(err))
maintainedApp2, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Maintained2",
Slug: "maintained2",
Platform: "windows",
UniqueIdentifier: "fleet.maintained2",
})
require.NoError(t, err)
payload3 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT 1",
PostInstallScript: "world",
StorageID: "storage2",
Filename: "maintained2",
Title: "Maintained2",
Version: "1.0",
Source: "programs",
Platform: "windows",
BundleIdentifier: "fleet.maintained2",
UserID: user1.ID,
TeamID: &team1.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
FleetMaintainedAppID: &maintainedApp2.ID,
}
_, titleID3, err := ds.MatchOrCreateSoftwareInstaller(context.Background(), payload3)
require.NoError(t, err)
p5, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
Type: fleet.PolicyTypePatch,
PatchSoftwareTitleID: &titleID3,
})
require.NoError(t, err)
require.Equal(t, "Windows - Maintained2 up to date", p5.Name)
require.Equal(t, "windows", p5.Platform)
require.Equal(t, "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Maintained2' AND version_compare(version, '1.0') < 0);", p5.Query)
}
func testTeamPolicyAutomationFilter(t *testing.T, ds *Datastore) {

File diff suppressed because one or more lines are too long

View file

@ -323,8 +323,9 @@ INSERT INTO software_installers (
fleet_maintained_app_id,
url,
upgrade_code,
is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, ?, ?)`
is_active,
patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, ?, ?, ?)`
args := []interface{}{
tid,
@ -348,6 +349,7 @@ INSERT INTO software_installers (
payload.URL,
payload.UpgradeCode,
true,
payload.PatchQuery,
}
res, err := tx.ExecContext(ctx, stmt, args...)
@ -1009,7 +1011,8 @@ SELECT
si.self_service,
si.url,
COALESCE(st.name, '') AS software_title,
COALESCE(st.bundle_identifier, '') AS bundle_identifier
COALESCE(st.bundle_identifier, '') AS bundle_identifier,
si.patch_query
%s
FROM
software_installers si
@ -2303,10 +2306,11 @@ INSERT INTO software_installers (
package_ids,
install_during_setup,
fleet_maintained_app_id,
is_active
is_active,
patch_query
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
(SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false), ?, ?
(SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false), ?, ?, ?
)
ON DUPLICATE KEY UPDATE
install_script_content_id = VALUES(install_script_content_id),
@ -2325,7 +2329,8 @@ ON DUPLICATE KEY UPDATE
user_email = VALUES(user_email),
url = VALUES(url),
install_during_setup = COALESCE(?, install_during_setup),
is_active = VALUES(is_active)
is_active = VALUES(is_active),
patch_query = VALUES(patch_query)
`
const updateInstaller = `
@ -2337,7 +2342,8 @@ SET
install_script_content_id = ?,
uninstall_script_content_id = ?,
post_install_script_content_id = ?,
pre_install_query = ?
pre_install_query = ?,
patch_query = ?
WHERE id = ?
`
@ -2759,6 +2765,7 @@ WHERE
installer.InstallDuringSetup,
installer.FleetMaintainedAppID,
isActive,
installer.PatchQuery,
installer.InstallDuringSetup,
}
// For FMA installers, skip the insert if this exact version is already cached
@ -2788,6 +2795,7 @@ WHERE
uninstallScriptID,
postInstallScriptID,
installer.PreInstallQuery,
installer.PatchQuery,
existingID,
}
if _, err := tx.ExecContext(ctx, updateInstaller, args...); err != nil {

View file

@ -3458,9 +3458,9 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) {
_, err := q.ExecContext(ctx, `
INSERT INTO software_installers
(team_id, global_or_team_id, storage_id, filename, extension, version, platform, title_id,
install_script_content_id, uninstall_script_content_id, is_active, url, package_ids)
install_script_content_id, uninstall_script_content_id, is_active, url, package_ids, patch_query)
SELECT team_id, global_or_team_id, storage_id, filename, extension, 'old_version', platform, title_id,
install_script_content_id, uninstall_script_content_id, 0, url, package_ids
install_script_content_id, uninstall_script_content_id, 0, url, package_ids, patch_query
FROM software_installers WHERE id = ?
`, installer1NoTeam)
return err

View file

@ -4436,13 +4436,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
platform,
self_service,
package_ids,
is_active
is_active,
patch_query
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)`,
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?)`,
teamID, globalOrTeamID, titleID, fmt.Sprintf("installer-%d.pkg", i), "pkg", fmt.Sprintf("v%d.0.0", i), scriptContentID,
uninstallScriptContentID,
[]byte("test"), "darwin", i < 2, "[]")
[]byte("test"), "darwin", i < 2, "[]", "")
if err != nil {
return err
}
@ -5326,13 +5327,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
platform,
self_service,
package_ids,
is_active
is_active,
patch_query
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)`,
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?)`,
darwinHost.TeamID, 0, softwareAlreadyInstalled.TitleID, "DummyApp.pkg", "pkg", "2.0.0",
scriptContentID, uninstallScriptContentID,
[]byte("test"), "darwin", true, "[]")
[]byte("test"), "darwin", true, "[]", "")
if err != nil {
return err
}
@ -5463,13 +5465,14 @@ func testListLinuxHostSoftware(t *testing.T, ds *Datastore) {
platform,
self_service,
package_ids,
is_active
is_active,
patch_query
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)`,
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?)`,
nil, 0, titleID, installer.Filename, installer.Extension, "2.0.0",
scriptContentID, scriptContentID,
[]byte("test"), "linux", true, "[]")
[]byte("test"), "linux", true, "[]", "")
require.NoError(t, err)
}
@ -6367,10 +6370,10 @@ func testSetHostSoftwareInstallResult(t *testing.T, ds *Datastore) {
res, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(title_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, package_ids)
(title_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, package_ids, patch_query)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
titleID, "installer.pkg", "pkg", "v1.0.0", scriptContentID, uninstallScriptContentID, []byte("test"), "darwin", "[]")
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
titleID, "installer.pkg", "pkg", "v1.0.0", scriptContentID, uninstallScriptContentID, []byte("test"), "darwin", "[]", "")
if err != nil {
return err
}
@ -11806,8 +11809,8 @@ func testListHostSoftwarePaginationWithMultipleInstallers(t *testing.T, ds *Data
if _, err := q.ExecContext(ctx, `
INSERT INTO software_installers
(team_id, global_or_team_id, title_id, filename, extension, version,
install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service, package_ids)
VALUES (NULL, 0, ?, ?, 'pkg', ?, ?, ?, ?, 'darwin', 0, '[]')`,
install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service, package_ids, patch_query)
VALUES (NULL, 0, ?, ?, 'pkg', ?, ?, ?, ?, 'darwin', 0, '[]', '')`,
titleID, fmt.Sprintf("installer-%s.pkg", version), version,
installScriptID, uninstallScriptID, fmt.Appendf(nil, "storage-%s", version),
); err != nil {

View file

@ -1036,7 +1036,8 @@ func (ds *Datastore) GetCachedFMAInstallerMetadata(ctx context.Context, teamID *
COALESCE(isc.contents, '') AS install_script,
COALESCE(usc.contents, '') AS uninstall_script,
COALESCE(si.pre_install_query, '') AS pre_install_query,
si.upgrade_code
si.upgrade_code,
si.patch_query
FROM software_installers si
LEFT JOIN script_contents isc ON isc.id = si.install_script_content_id
LEFT JOIN script_contents usc ON usc.id = si.uninstall_script_content_id

View file

@ -686,9 +686,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage1", "[]", appDarwin1.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage1", "[]", appDarwin1.ID, "")
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -696,9 +696,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "slack.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage2", "[]", appDarwin2.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "slack.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage2", "[]", appDarwin2.ID, "")
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -706,9 +706,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "teams.exe", "1.0", "windows", installScriptID, uninstallScriptID, "storage3", "[]", appWindows1.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "teams.exe", "1.0", "windows", installScriptID, uninstallScriptID, "storage3", "[]", appWindows1.ID, "")
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -716,9 +716,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom.exe", "1.0", "windows", installScriptID, uninstallScriptID, "storage4", "[]", appWindows2.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom.exe", "1.0", "windows", installScriptID, uninstallScriptID, "storage4", "[]", appWindows2.ID, "")
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@ -726,9 +726,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "linux.deb", "1.0", "linux", installScriptID, uninstallScriptID, "storage5", "[]", appLinux.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "linux.deb", "1.0", "linux", installScriptID, uninstallScriptID, "storage5", "[]", appLinux.ID, "")
return err
})
@ -744,9 +744,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom-v2.pkg", "2.0", "darwin", installScriptID, uninstallScriptID, "storage6", "[]", appDarwin1.ID)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "zoom-v2.pkg", "2.0", "darwin", installScriptID, uninstallScriptID, "storage6", "[]", appDarwin1.ID, "")
return err
})
macOSApps, windowsApps, err = fleetMaintainedAppsInUseDB(ctx, ds.reader(ctx))
@ -760,9 +760,9 @@ func testFleetMaintainedAppsInUse(t *testing.T, ds *Datastore) {
INSERT INTO software_installers (
team_id, global_or_team_id, filename, version, platform,
install_script_content_id, uninstall_script_content_id,
storage_id, package_ids, fleet_maintained_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "custom.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage7", "[]", nil)
storage_id, package_ids, fleet_maintained_app_id, patch_query
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, nil, 0, "custom.pkg", "1.0", "darwin", installScriptID, uninstallScriptID, "storage7", "[]", nil, "")
return err
})

View file

@ -18,6 +18,7 @@ type MaintainedApp struct {
AutomaticInstallQuery string `json:"-" db:"pre_install_query"`
Categories []string `json:"categories"`
UpgradeCode string `json:"upgrade_code,omitempty" db:"upgrade_code"`
PatchQuery string `json:"-" db:"patch_query"`
}
func (s *MaintainedApp) Source() string {

View file

@ -138,6 +138,8 @@ type SoftwareInstaller struct {
// PatchPolicy is present for Fleet maintained apps with an associated patch policy
PatchPolicy *PatchPolicyData `json:"patch_policy"`
// PatchQuery is the query to use for creating a patch policy
PatchQuery string `json:"-" db:"patch_query"`
}
// SoftwarePackageResponse is the response type used when applying software by batch.
@ -540,6 +542,7 @@ type UploadSoftwareInstallerPayload struct {
// automatically created when a software installer is added to Fleet. This field should be set
// after software installer creation if AutomaticInstall is true.
AddedAutomaticInstallPolicy *Policy
PatchQuery string
}
func (p UploadSoftwareInstallerPayload) UniqueIdentifier() string {

View file

@ -159,6 +159,7 @@ func Hydrate(ctx context.Context, app *fleet.MaintainedApp, version string, team
app.AutomaticInstallQuery = cached.AutomaticInstallQuery
app.Categories = cached.Categories
app.UpgradeCode = cached.UpgradeCode
app.PatchQuery = cached.PatchQuery
return app, nil
}
@ -211,6 +212,7 @@ func Hydrate(ctx context.Context, app *fleet.MaintainedApp, version string, team
app.AutomaticInstallQuery = manifest.Versions[0].Queries.Exists
app.Categories = manifest.Versions[0].DefaultCategories
app.UpgradeCode = manifest.Versions[0].UpgradeCode
app.PatchQuery = manifest.Versions[0].Queries.Patch
return app, nil
}

View file

@ -14557,9 +14557,9 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestP
_, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(title_id, filename, extension, version, platform, install_script_content_id, uninstall_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query, package_ids, is_active)
(title_id, filename, extension, version, platform, install_script_content_id, uninstall_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query, package_ids, is_active, patch_query)
VALUES
(?, ?, ?, ?, ?, ?, ?, unhex(?), ?, ?, ?, ?, TRUE)`,
(?, ?, ?, ?, ?, ?, ?, unhex(?), ?, ?, ?, ?, TRUE, '')`,
titleID, fmt.Sprintf("installer.%s", kind), kind, "v1.0.0", platform, scriptContentID, uninstallScriptContentID,
hex.EncodeToString([]byte("test")), tm.ID, tm.ID, "foo", "")
return err
@ -27006,6 +27006,8 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
// team tests and no team tests
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "Team 1" + t.Name()})
require.NoError(t, err)
team2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "Team 2" + t.Name()})
require.NoError(t, err)
// mock an installer being uploaded through FMA
updateInstallerFMAID := func(fmaID, teamID, titleID uint) {
@ -27017,17 +27019,18 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
}
// resetFMAState resets an fmaTestState to the given version and installer bytes.
resetFMAState := func(state *fmaTestState, version string, installerBytes []byte) {
resetFMAState := func(state *fmaTestState, version string, installerBytes []byte, patchQuery string) {
state.version = version
state.installerBytes = installerBytes
state.ComputeSHA(installerBytes)
state.patchQuery = patchQuery
}
checkPolicy := func(policy *fleet.Policy, name, version string, titleID uint) {
require.NotNil(t, policy.PatchSoftware)
require.Equal(t, titleID, policy.PatchSoftware.SoftwareTitleID)
require.Equal(t, fleet.PolicyTypePatch, policy.Type)
require.Equal(t, fmt.Sprintf(`SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Zoom Workplace (X64)' AND version_compare(bundle_short_version, '%s') < 0);`, version), policy.Query)
require.Equal(t, fmt.Sprintf(`SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Zoom Workplace (X64)' AND version_compare(version, '%s') < 0);`, version), policy.Query)
require.Equal(t, name, policy.Name)
}
@ -27372,7 +27375,7 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
require.Equal(t, uint(1), listPolResp.Policies[0].FailingHostCount)
// Test 2: FMA Version is updated (query should use new version)
resetFMAState(states["/zoom/windows.json"], "1.2", []byte("abc"))
resetFMAState(states["/zoom/windows.json"], "1.2", []byte("abc"), "")
s.DoJSON("POST", "/api/latest/fleet/software/batch",
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows")}}, TeamName: team.Name},
@ -27398,7 +27401,7 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
require.NoError(t, err)
require.Equal(t, uint(0), listPolResp.Policies[0].FailingHostCount)
// Test 3: FMA Version is pinned back after update? (query should use old version)
// Test 3: FMA Version is pinned back after update to a newer version (query should use old version)
s.DoJSON("POST", "/api/latest/fleet/software/batch",
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{
{Slug: ptr.String("zoom/windows"), SelfService: true, RollbackVersion: "1.0"},
@ -27420,6 +27423,108 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() {
require.Len(t, listPolResp.Policies, 1)
checkPolicy(listPolResp.Policies[0], spec.Name, "1.0", title.ID)
})
t.Run("override and empty queries behave the same", func(t *testing.T) {
// initialize FMA server
states := make(map[string]*fmaTestState, 2)
states["/zoom/windows.json"] = &fmaTestState{
version: "1.0",
installerBytes: []byte("xyz"),
installerPath: "/zoom.msi",
}
states["/1password/darwin.json"] = &fmaTestState{
version: "1.0",
installerBytes: []byte("xyz"),
installerPath: "/1password.pkg",
patchQuery: "SELECT 1; --custom query 1.0",
}
startFMAServers(t, s.ds, states)
// Add installers at version 1.0
var resp batchSetSoftwareInstallersResponse
s.DoJSON("POST", "/api/latest/fleet/software/batch",
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows")}, {Slug: ptr.String("1password/darwin")}}, TeamName: team2.Name},
http.StatusAccepted, &resp,
"team_name", team2.Name, "team_id", fmt.Sprint(team2.ID),
)
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, team2.Name, resp.RequestUUID)
checkPolicies := func(policies []*fleet.Policy, version string) {
require.Len(t, policies, 2)
require.Equal(t, fmt.Sprintf(`SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Zoom Workplace (X64)' AND version_compare(version, '%s') < 0);`, version), policies[0].Query)
require.Equal(t, fmt.Sprintf(`SELECT 1; --custom query %s`, version), policies[1].Query)
}
specs := []*fleet.PolicySpec{
{
Name: "team patch policy",
Query: "SELECT 1",
Team: team2.Name,
Type: fleet.PolicyTypePatch,
FleetMaintainedAppSlug: "zoom/windows",
},
{
Name: "team patch policy 2",
Team: team2.Name,
Type: fleet.PolicyTypePatch,
FleetMaintainedAppSlug: "1password/darwin",
},
}
// Apply patch policies
applyResp := applyPolicySpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/policies",
applyPolicySpecsRequest{Specs: specs},
http.StatusOK, &applyResp,
)
listPolResp := listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team2.ID), listTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0")
checkPolicies(listPolResp.Policies, "1.0")
// Pin FMA versions to 1.2, queries should update
resetFMAState(states["/zoom/windows.json"], "1.2", []byte("abc"), "")
resetFMAState(states["/1password/darwin.json"], "1.2", []byte("abc"), "SELECT 1; --custom query 1.2")
s.DoJSON("POST", "/api/latest/fleet/software/batch",
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows")}, {Slug: ptr.String("1password/darwin")}}, TeamName: team2.Name},
http.StatusAccepted, &resp,
"team_name", team2.Name, "team_id", fmt.Sprint(team2.ID),
)
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, team2.Name, resp.RequestUUID)
applyResp = applyPolicySpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/policies",
applyPolicySpecsRequest{Specs: specs},
http.StatusOK, &applyResp,
)
listPolResp = listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team2.ID), listTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0")
checkPolicies(listPolResp.Policies, "1.2")
// Rollback FMA versions to 1.0, queries should use older versions
s.DoJSON("POST", "/api/latest/fleet/software/batch",
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{
{Slug: ptr.String("zoom/windows"), SelfService: true, RollbackVersion: "1.0"},
{Slug: ptr.String("1password/darwin"), SelfService: true, RollbackVersion: "1.0"},
}, TeamName: team2.Name},
http.StatusAccepted, &resp,
"team_name", team2.Name, "team_id", fmt.Sprint(team2.ID),
)
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, team2.Name, resp.RequestUUID)
applyResp = applyPolicySpecsResponse{}
s.DoJSON("POST", "/api/latest/fleet/spec/policies",
applyPolicySpecsRequest{Specs: specs},
http.StatusOK, &applyResp,
)
listPolResp = listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team2.ID), listTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0")
checkPolicies(listPolResp.Policies, "1.0")
})
}
func (s *integrationEnterpriseTestSuite) TestConditionalAccessPlatformValidation() {

View file

@ -12272,6 +12272,7 @@ type appStoreApp interface {
func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
t := s.T()
s.setSkipWorkerJobs(t)
batchURL := "/api/latest/fleet/software/app_store_apps/batch"
// non-existent team

View file

@ -1427,6 +1427,7 @@ type fmaTestState struct {
installerBytes []byte
sha256 string
installerPath string
patchQuery string
}
func (s *fmaTestState) ComputeSHA(b []byte) {
@ -1480,8 +1481,11 @@ func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTes
versions := []*ma.FMAManifestApp{
{
Version: state.version,
Queries: ma.FMAQueries{Exists: "SELECT 1 FROM osquery_info;"},
Version: state.version,
Queries: ma.FMAQueries{
Exists: "SELECT 1 FROM osquery_info;",
Patch: state.patchQuery,
},
InstallerURL: installerServer.URL + state.installerPath,
InstallScriptRef: "foobaz",
UninstallScriptRef: "foobaz",