This pull request refactors how patch policy SQL queries are generated and validated, with the main goal of simplifying and correcting the construction of `NOT EXISTS` queries for version checks. The changes ensure that the generated queries are more accurate, especially in cases involving SQL `OR` conditions and platform-specific version columns. The update also adapts related test cases to match the new query structure. **Patch policy query generation improvements:** * Refactored the SQL generation logic in `GenerateQueryForManifest` to append the `version_compare` clause directly inside the original `WHERE` clause, rather than wrapping the entire query in extra parentheses. This results in simpler, more standard SQL queries. * Added logic to detect `OR` conditions in the `WHERE` clause and wrap them in parentheses to ensure correct SQL precedence when appending the `AND version_compare(...)` clause. * Improved selection of the version column (e.g., `bundle_short_version`, `version`, or `file_version`) based on platform and table name, ensuring correct queries for both macOS and Windows policies. **Test updates:** * Updated all relevant test cases in `patch_policy_test.go` to expect the new, simplified query format, removing the extra parentheses and validating correct handling of SQL with `OR` and platform-specific columns. [[1]](diffhunk://#diff-a770c8e2c3066123079c660322e318014a7c4870429e091a6e48d4acb222c340L23-R23) [[2]](diffhunk://#diff-a770c8e2c3066123079c660322e318014a7c4870429e091a6e48d4acb222c340L32-R32) [[3]](diffhunk://#diff-a770c8e2c3066123079c660322e318014a7c4870429e091a6e48d4acb222c340L41-R41) [[4]](diffhunk://#diff-a770c8e2c3066123079c660322e318014a7c4870429e091a6e48d4acb222c340L50-R61) * Adjusted a Homebrew ingester test to match the new query formatting, ensuring consistency across the codebase.
152 lines
4.5 KiB
Go
152 lines
4.5 KiB
Go
package patch_policy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
type PolicyData struct {
|
|
Name string
|
|
Platform string
|
|
Description string
|
|
Resolution string
|
|
Query string
|
|
ExistsQuery string
|
|
Version string
|
|
}
|
|
|
|
const (
|
|
// notExistsStart is prepended to the exists query body; version_compare is appended
|
|
// to the same WHERE clause (inside NOT EXISTS), matching the pre-#45647 generator.
|
|
notExistsStart = "SELECT 1 WHERE NOT EXISTS ("
|
|
existsPrefix = "SELECT 1 FROM "
|
|
)
|
|
|
|
var (
|
|
ErrWrongPlatform = errors.New("platform should be darwin or windows")
|
|
ErrNoExistsQuery = errors.New("exists query was not provided")
|
|
)
|
|
|
|
// GenerateQueryForManifest wraps the "exists" query to create a patch policy query.
|
|
func GenerateQueryForManifest(p PolicyData) (string, error) {
|
|
if p.ExistsQuery == "" {
|
|
return "", ErrNoExistsQuery
|
|
}
|
|
|
|
suffix, err := versionCompareSuffix(p.Platform, p.ExistsQuery, p.Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
before, _ := strings.CutSuffix(p.ExistsQuery, ";")
|
|
before = strings.TrimSpace(before)
|
|
if strings.Contains(before, " OR ") {
|
|
before = parenthesizeWhereClause(before)
|
|
}
|
|
|
|
return notExistsStart + before + suffix, nil
|
|
}
|
|
|
|
// parenthesizeWhereClause wraps the WHERE body in parens when it contains OR so that
|
|
// the trailing AND version_compare(...) binds to the full predicate, not just the
|
|
// right-hand side of OR (SQL precedence: AND > OR).
|
|
func parenthesizeWhereClause(existsQuery string) string {
|
|
if !strings.HasPrefix(existsQuery, existsPrefix) {
|
|
return existsQuery
|
|
}
|
|
rest := strings.TrimPrefix(existsQuery, existsPrefix)
|
|
table, conditions, found := strings.Cut(rest, " WHERE ")
|
|
if !found {
|
|
return existsQuery
|
|
}
|
|
if !strings.Contains(conditions, " OR ") {
|
|
return existsQuery
|
|
}
|
|
return existsPrefix + table + " WHERE (" + conditions + ")"
|
|
}
|
|
|
|
func versionCompareSuffix(platform, existsQuery, version string) (string, error) {
|
|
column, err := versionCompareColumn(platform, existsQuery)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf(" AND version_compare(%s, '%s') < 0);", column, version), nil
|
|
}
|
|
|
|
func versionCompareColumn(platform, existsQuery string) (string, error) {
|
|
switch platform {
|
|
case "darwin":
|
|
return "bundle_short_version", nil
|
|
case "windows":
|
|
if tableFromExistsQuery(existsQuery) == "file" {
|
|
return "file_version", nil
|
|
}
|
|
return "version", nil
|
|
default:
|
|
return "", ErrWrongPlatform
|
|
}
|
|
}
|
|
|
|
func tableFromExistsQuery(existsQuery string) string {
|
|
trimmed, _ := strings.CutSuffix(strings.TrimSpace(existsQuery), ";")
|
|
if !strings.HasPrefix(trimmed, existsPrefix) {
|
|
return ""
|
|
}
|
|
rest := strings.TrimPrefix(trimmed, existsPrefix)
|
|
if table, _, found := strings.Cut(rest, " WHERE "); found {
|
|
return table
|
|
}
|
|
if table, _, found := strings.Cut(rest, " "); found {
|
|
return table
|
|
}
|
|
return strings.TrimSpace(rest)
|
|
}
|
|
|
|
// 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 = defaultMacOSQuery(installer.BundleIdentifier, installer.Version)
|
|
}
|
|
case "windows":
|
|
if p.Name == "" {
|
|
p.Name = fmt.Sprintf("Windows - %s up to date", installer.SoftwareTitle)
|
|
}
|
|
if installer.PatchQuery == "" {
|
|
query = defaultWindowsQuery(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
|
|
}
|
|
|
|
func defaultMacOSQuery(bundleIdentifier string, version string) string {
|
|
patchTemplate := "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = '%s' AND version_compare(bundle_short_version, '%s') < 0);"
|
|
return fmt.Sprintf(patchTemplate, bundleIdentifier, version)
|
|
}
|
|
|
|
func defaultWindowsQuery(softwareTitle string, version string) string {
|
|
patchTemplate := "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = '%s' AND version_compare(version, '%s') < 0);"
|
|
return fmt.Sprintf(patchTemplate, softwareTitle, version)
|
|
}
|