fleet/pkg/automatic_policy/automatic_policy.go
Ian Littman c461e097a8
Don't pass the default deb auto-install policy if install status is e.g. uninstalled (#32005)
Fixes #29894 and probably #31980.

# Checklist for submitter

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

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

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests

- [x] QA'd all new/changed functionality manually
2025-08-18 17:37:06 -05:00

218 lines
6.5 KiB
Go

// Package automatic_policy generates "trigger policies" from metadata of software packages.
package automatic_policy
import (
"errors"
"fmt"
)
// PolicyData contains generated data for a policy to trigger installation of a software package.
type PolicyData struct {
// Name is the generated name of the policy.
Name string
// Query is the generated SQL/sqlite of the policy.
Query string
// Description is the generated description for the policy.
Description string
// Platform is the target platform for the policy.
Platform string
}
// InstallerMetadata contains the metadata of a software package used to generate the policies.
type InstallerMetadata interface {
PolicyName() (string, error)
PolicyDescription() (string, error)
PolicyQuery() (string, error)
PolicyPlatform() (string, error)
}
type FMAInstallerMetadata struct {
Title string
Platform string
Query string
}
func (m FMAInstallerMetadata) PolicyName() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("[Install software] %s", m.Title), nil
}
func (m FMAInstallerMetadata) PolicyDescription() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", m.Title), nil
}
func (m FMAInstallerMetadata) PolicyQuery() (string, error) {
return m.Query, nil
}
func (m FMAInstallerMetadata) PolicyPlatform() (string, error) {
return m.Platform, nil
}
type MacInstallerMetadata struct {
BundleIdentifier string
// Title is the software title extracted from a software package.
Title string
}
func (m MacInstallerMetadata) PolicyName() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("[Install software] %s", m.Title), nil
}
func (m MacInstallerMetadata) PolicyDescription() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
return fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", m.Title), nil
}
func (m MacInstallerMetadata) PolicyQuery() (string, error) {
if m.BundleIdentifier == "" {
return "", ErrMissingBundleIdentifier
}
return fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", m.BundleIdentifier), nil
}
func (m MacInstallerMetadata) PolicyPlatform() (string, error) {
return "darwin", nil
}
type FullInstallerMetadata struct {
// BundleIdentifier is the bundle identifier for 'pkg' packages
BundleIdentifier string
// Title is the software title extracted from a software package.
Title string
// Extension is the extension of the software package.
Extension string
// PackageIDs contains the product code for 'msi' packages.
PackageIDs []string
// UpgradeCode is the upgrade code for 'msi' packages.
UpgradeCode string
}
func (m FullInstallerMetadata) PolicyName() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
if m.Extension == "" {
return "", ErrExtensionNotSupported
}
return fmt.Sprintf("[Install software] %s (%s)", m.Title, m.Extension), nil
}
func (m FullInstallerMetadata) PolicyDescription() (string, error) {
if m.Title == "" {
return "", ErrMissingTitle
}
description := fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", m.Title)
if m.Extension == "deb" || m.Extension == "rpm" {
basedPrefix := "RPM"
if m.Extension == "rpm" {
basedPrefix = "Debian"
}
description += fmt.Sprintf(
"\nSoftware won't be installed on Linux hosts with %s-based distributions because this policy's query is written to always pass on these hosts.",
basedPrefix,
)
}
return description, nil
}
func (m FullInstallerMetadata) PolicyQuery() (string, error) {
switch m.Extension {
case "pkg":
if m.BundleIdentifier == "" {
return "", ErrMissingBundleIdentifier
}
return fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", m.BundleIdentifier), nil
case "msi":
// Use the upgrade code if we have it. Otherwise, fall back to the product code.
if m.UpgradeCode != "" {
return fmt.Sprintf("SELECT 1 FROM programs WHERE upgrade_code = '%s';", m.UpgradeCode), nil
}
if len(m.PackageIDs) == 0 || m.PackageIDs[0] == "" {
return "", ErrMissingProductAndUpgradeCode
}
return fmt.Sprintf("SELECT 1 FROM programs WHERE identifying_number = '%s';", m.PackageIDs[0]), nil
case "deb":
return fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-DEB-based hosts.
`SELECT 1 WHERE EXISTS (
SELECT 1 WHERE (SELECT COUNT(*) FROM deb_packages) = 0
) OR EXISTS (
SELECT 1 FROM deb_packages WHERE name = '%s' AND status = 'install ok installed'
);`, m.Title,
), nil
case "rpm":
return fmt.Sprintf(
// First inner SELECT will mark the policies as successful on non-RPM-based hosts.
`SELECT 1 WHERE EXISTS (
SELECT 1 WHERE (SELECT COUNT(*) FROM rpm_packages) = 0
) OR EXISTS (
SELECT 1 FROM rpm_packages WHERE name = '%s'
);`, m.Title), nil
default:
return "", ErrExtensionNotSupported
}
}
func (m FullInstallerMetadata) PolicyPlatform() (string, error) {
switch m.Extension {
case "pkg":
return "darwin", nil
case "msi":
return "windows", nil
case "deb":
return "linux", nil
case "rpm":
return "linux", nil
default:
return "", ErrExtensionNotSupported
}
}
var (
// ErrExtensionNotSupported is returned if the extension is not supported to generate automatic policies.
ErrExtensionNotSupported = errors.New("extension not supported")
// ErrMissingBundleIdentifier is returned if the software extension is "pkg" and a bundle identifier was not extracted from the installer.
ErrMissingBundleIdentifier = errors.New("missing bundle identifier")
// ErrMissingProductAndUpgradeCode is returned if the software extension is "msi" and a product code was not extracted from the installer.
ErrMissingProductAndUpgradeCode = errors.New("missing product and upgrade code")
// ErrMissingTitle is returned if a title was not extracted from the installer.
ErrMissingTitle = errors.New("missing title")
)
// Generate generates the "trigger policy" from the metadata of a software package.
func Generate(metadata InstallerMetadata) (*PolicyData, error) {
name, err := metadata.PolicyName()
if err != nil {
return nil, err
}
query, err := metadata.PolicyQuery()
if err != nil {
return nil, err
}
platform, err := metadata.PolicyPlatform()
if err != nil {
return nil, err
}
description, err := metadata.PolicyDescription()
if err != nil {
return nil, err
}
return &PolicyData{Name: name, Query: query, Description: description, Platform: platform}, nil
}