mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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:
parent
a1860a9185
commit
0d15fd6cd6
25 changed files with 487 additions and 123 deletions
1
changes/41815-override-patch-policy-query
Normal file
1
changes/41815-override-patch-policy-query
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added ability to specify custom patch policy query in an FMA manifest
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const OutputPath = "ee/maintained-apps/outputs"
|
|||
|
||||
type FMAQueries struct {
|
||||
Exists string `json:"exists"`
|
||||
Patch string `json:"patch"`
|
||||
}
|
||||
|
||||
type FMAManifestApp struct {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
87
pkg/patch_policy/patch_policy.go
Normal file
87
pkg/patch_policy/patch_policy.go
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(×tamp, `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(×tamp2, `SELECT updated_at FROM software_installers WHERE id = ?`, installer1))
|
||||
require.Equal(t, timestamp, timestamp2)
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue