mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- 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`).
161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
package homebrew
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIngestValidations(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
testInstallScriptContents := "this is a test install script"
|
|
require.NoError(t, os.WriteFile(path.Join(tempDir, "install_script.sh"), []byte(testInstallScriptContents), 0644))
|
|
|
|
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
|
|
|
|
appToken := strings.TrimSuffix(path.Base(r.URL.Path), ".json")
|
|
switch appToken {
|
|
case "fail":
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
|
|
case "notfound":
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
|
|
case "noname":
|
|
cask = brewCask{
|
|
Token: appToken,
|
|
Name: nil,
|
|
URL: "https://example.com",
|
|
Version: "1.0",
|
|
}
|
|
|
|
case "emptyname":
|
|
cask = brewCask{
|
|
Token: appToken,
|
|
Name: []string{""},
|
|
URL: "https://example.com",
|
|
Version: "1.0",
|
|
}
|
|
|
|
case "notoken":
|
|
cask = brewCask{
|
|
Token: "",
|
|
Name: []string{appToken},
|
|
URL: "https://example.com",
|
|
Version: "1.0",
|
|
}
|
|
|
|
case "noversion":
|
|
cask = brewCask{
|
|
Token: appToken,
|
|
Name: []string{appToken},
|
|
URL: "https://example.com",
|
|
Version: "",
|
|
}
|
|
|
|
case "nourl":
|
|
cask = brewCask{
|
|
Token: appToken,
|
|
Name: []string{appToken},
|
|
URL: "",
|
|
Version: "1.0",
|
|
}
|
|
|
|
case "invalidurl":
|
|
cask = brewCask{
|
|
Token: appToken,
|
|
Name: []string{appToken},
|
|
URL: "https://\x00\x01\x02",
|
|
Version: "1.0",
|
|
}
|
|
|
|
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},
|
|
URL: "https://example.com",
|
|
Version: "1.0",
|
|
}
|
|
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
t.Fatalf("unexpected app token %s", appToken)
|
|
}
|
|
|
|
err := json.NewEncoder(w).Encode(cask)
|
|
require.NoError(t, err)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
ctx := context.Background()
|
|
|
|
cases := []struct {
|
|
wantErr string
|
|
inputApp inputApp
|
|
}{
|
|
{"brew API returned status 500", inputApp{Token: "fail", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"app not found in brew API", inputApp{Token: "notfound", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"missing name for cask noname", inputApp{Token: "noname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"missing name for cask emptyname", inputApp{Token: "emptyname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"missing token for cask notoken", inputApp{Token: "notoken", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"missing version for cask noversion", inputApp{Token: "noversion", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"missing URL for cask nourl", inputApp{Token: "nourl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"parse URL for cask invalidurl", inputApp{Token: "invalidurl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"", inputApp{Token: "ok", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
|
{"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}},
|
|
{"", 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) {
|
|
i := &brewIngester{
|
|
logger: slog.New(slog.DiscardHandler),
|
|
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
|
|
baseURL: srv.URL + "/",
|
|
}
|
|
|
|
out, err := i.ingestOne(ctx, c.inputApp)
|
|
if c.wantErr != "" {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
if c.inputApp.InstallScriptPath != "" {
|
|
require.Equal(t, testInstallScriptContents, out.InstallScript)
|
|
}
|
|
|
|
if c.inputApp.UninstallScriptPath != "" {
|
|
require.Equal(t, testUninstallScriptContents, out.UninstallScript)
|
|
}
|
|
|
|
if c.inputApp.PatchPolicyPath != "" {
|
|
require.Equal(t, "SELECT 1;", out.Queries.Patch)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|