-
Notifications
You must be signed in to change notification settings - Fork 450
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automatic install custom packages (#25021)
#24385 Some docs change here: #25026. - [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/Committing-Changes.md#changes-files) for more information. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality
- Loading branch information
Showing
18 changed files
with
1,389 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Added capability to automatically generate "trigger policies" for custom software packages. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// 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 struct { | ||
// Title is the software title extracted from a software package. | ||
Title string | ||
// Extension is the extension of the software package. | ||
Extension string | ||
// BundleIdentifier contains the bundle identifier for 'pkg' packages. | ||
BundleIdentifier string | ||
// PackageIDs contains the product code for 'msi' packages. | ||
PackageIDs []string | ||
} | ||
|
||
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") | ||
// ErrMissingProductCode is returned if the software extension is "msi" and a product code was not extracted from the installer. | ||
ErrMissingProductCode = errors.New("missing product 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) { | ||
switch { | ||
case metadata.Title == "": | ||
return nil, ErrMissingTitle | ||
case metadata.Extension != "pkg" && metadata.Extension != "msi" && metadata.Extension != "deb" && metadata.Extension != "rpm": | ||
return nil, ErrExtensionNotSupported | ||
case metadata.Extension == "pkg" && metadata.BundleIdentifier == "": | ||
return nil, ErrMissingBundleIdentifier | ||
case metadata.Extension == "msi" && (len(metadata.PackageIDs) == 0 || metadata.PackageIDs[0] == ""): | ||
return nil, ErrMissingProductCode | ||
} | ||
|
||
name := fmt.Sprintf("[Install software] %s (%s)", metadata.Title, metadata.Extension) | ||
|
||
description := fmt.Sprintf("Policy triggers automatic install of %s on each host that's missing this software.", metadata.Title) | ||
if metadata.Extension == "deb" || metadata.Extension == "rpm" { | ||
basedPrefix := "RPM" | ||
if metadata.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, | ||
) | ||
} | ||
|
||
switch metadata.Extension { | ||
case "pkg": | ||
return &PolicyData{ | ||
Name: name, | ||
Query: fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", metadata.BundleIdentifier), | ||
Platform: "darwin", | ||
Description: description, | ||
}, nil | ||
case "msi": | ||
return &PolicyData{ | ||
Name: name, | ||
Query: fmt.Sprintf("SELECT 1 FROM programs WHERE identifying_number = '%s';", metadata.PackageIDs[0]), | ||
Platform: "windows", | ||
Description: description, | ||
}, nil | ||
case "deb": | ||
return &PolicyData{ | ||
Name: name, | ||
Query: 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' | ||
);`, metadata.Title, | ||
), | ||
Platform: "linux", | ||
Description: description, | ||
}, nil | ||
case "rpm": | ||
return &PolicyData{ | ||
Name: name, | ||
Query: 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' | ||
);`, metadata.Title), | ||
Platform: "linux", | ||
Description: description, | ||
}, nil | ||
default: | ||
return nil, ErrExtensionNotSupported | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package automatic_policy | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestGenerateErrors(t *testing.T) { | ||
_, err := Generate(InstallerMetadata{ | ||
Title: "Foobar", | ||
Extension: "exe", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{"Foobar"}, | ||
}) | ||
require.ErrorIs(t, err, ErrExtensionNotSupported) | ||
|
||
_, err = Generate(InstallerMetadata{ | ||
Title: "Foobar", | ||
Extension: "msi", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{""}, | ||
}) | ||
require.ErrorIs(t, err, ErrMissingProductCode) | ||
_, err = Generate(InstallerMetadata{ | ||
Title: "Foobar", | ||
Extension: "msi", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{}, | ||
}) | ||
require.ErrorIs(t, err, ErrMissingProductCode) | ||
|
||
_, err = Generate(InstallerMetadata{ | ||
Title: "Foobar", | ||
Extension: "pkg", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{""}, | ||
}) | ||
require.ErrorIs(t, err, ErrMissingBundleIdentifier) | ||
|
||
_, err = Generate(InstallerMetadata{ | ||
Title: "", | ||
Extension: "deb", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{""}, | ||
}) | ||
require.ErrorIs(t, err, ErrMissingTitle) | ||
} | ||
|
||
func TestGenerate(t *testing.T) { | ||
policyData, err := Generate(InstallerMetadata{ | ||
Title: "Foobar", | ||
Extension: "pkg", | ||
BundleIdentifier: "com.foo.bar", | ||
PackageIDs: []string{"com.foo.bar"}, | ||
}) | ||
require.NoError(t, err) | ||
require.Equal(t, "[Install software] Foobar (pkg)", policyData.Name) | ||
require.Equal(t, "Policy triggers automatic install of Foobar on each host that's missing this software.", policyData.Description) | ||
require.Equal(t, "darwin", policyData.Platform) | ||
require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'com.foo.bar';", policyData.Query) | ||
|
||
policyData, err = Generate(InstallerMetadata{ | ||
Title: "Barfoo", | ||
Extension: "msi", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{"foo"}, | ||
}) | ||
require.NoError(t, err) | ||
require.Equal(t, "[Install software] Barfoo (msi)", policyData.Name) | ||
require.Equal(t, "Policy triggers automatic install of Barfoo on each host that's missing this software.", policyData.Description) | ||
require.Equal(t, "windows", policyData.Platform) | ||
require.Equal(t, "SELECT 1 FROM programs WHERE identifying_number = 'foo';", policyData.Query) | ||
|
||
policyData, err = Generate(InstallerMetadata{ | ||
Title: "Zoobar", | ||
Extension: "deb", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{"Zoobar"}, | ||
}) | ||
require.NoError(t, err) | ||
require.Equal(t, "[Install software] Zoobar (deb)", policyData.Name) | ||
require.Equal(t, `Policy triggers automatic install of Zoobar on each host that's missing this software. | ||
Software won't be installed on Linux hosts with RPM-based distributions because this policy's query is written to always pass on these hosts.`, policyData.Description) | ||
require.Equal(t, "linux", policyData.Platform) | ||
require.Equal(t, `SELECT 1 WHERE EXISTS ( | ||
SELECT 1 WHERE (SELECT COUNT(*) FROM deb_packages) = 0 | ||
) OR EXISTS ( | ||
SELECT 1 FROM deb_packages WHERE name = 'Zoobar' | ||
);`, policyData.Query) | ||
|
||
policyData, err = Generate(InstallerMetadata{ | ||
Title: "Barzoo", | ||
Extension: "rpm", | ||
BundleIdentifier: "", | ||
PackageIDs: []string{"Barzoo"}, | ||
}) | ||
require.NoError(t, err) | ||
require.Equal(t, "[Install software] Barzoo (rpm)", policyData.Name) | ||
require.Equal(t, `Policy triggers automatic install of Barzoo on each host that's missing this software. | ||
Software won't be installed on Linux hosts with Debian-based distributions because this policy's query is written to always pass on these hosts.`, policyData.Description) | ||
require.Equal(t, "linux", policyData.Platform) | ||
require.Equal(t, `SELECT 1 WHERE EXISTS ( | ||
SELECT 1 WHERE (SELECT COUNT(*) FROM rpm_packages) = 0 | ||
) OR EXISTS ( | ||
SELECT 1 FROM rpm_packages WHERE name = 'Barzoo' | ||
);`, policyData.Query) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.