fleet/ee/fleetctl/updates_test.go
Lucas Manuel Rodriguez ecdfd627b6
Fleet Desktop MVP (#4530)
* WIP

* WIP2

* Fix orbit and fleetctl tests

* Amend macos-app default

* Add some fixes

* Use fleetctl updates roots command

* Add more fixes to Updater

* Fixes to app publishing and downloading

* Add more changes to support fleetctl cross generation

* Amend comment

* Add pkg generation to ease testing

* Make more fixes

* Add changes entry

* Add legacy targets (until our TUF system exposes the new app)

* Fix fleetctl preview

* Fix bool flag

* Fix orbit logic for disabled-updates and dev-mode

* Fix TestPreview

* Remove constant and fix zip-slip attack (codeql)

* Return unknown error

* Fix updater's checkExec

* Add support for executable signing in init_tuf.sh

* Try only signing orbit

* Fix init_tuf.sh targets, macos-app only for osqueryd

* Specify GOARCH to support M1s

* Add workflow to generate osqueryd.app.tar.gz

* Use 5.2.2 on init_tuf.sh

* Add unit test for tar.gz target

* Use artifacts instead of releases

* Remove copy paste residue

* Fleet Desktop Packaging WIP

* Ignore gosec warning

* Trigger on PR too

* Install Go in workflow

* Pass url parameter to desktop app

* Fix fleetctl package

* Final set of changes for v1 of Fleet Desktop

* Add changes

* PR fixes

* Fix CI build

* add larger menu bar icon

* Add transparency item

* Delete host_device_auth entry on host deletion

* Add SetTargetChannel

* Update white logo and add desktop to update runner

* Add fleet-desktop monitoring to orbit

* Define fleet-desktop app exec name

* Fix update runner creation

* Add API test before enabling the My device menu item

Co-authored-by: Zach Wasserman <zach@fleetdm.com>
2022-03-21 14:53:53 -03:00

441 lines
14 KiB
Go

//go:build darwin || linux
// +build darwin linux
package eefleetctl
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore"
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/theupdateframework/go-tuf/data"
"github.com/urfave/cli/v2"
)
func TestPassphraseHandlerEnvironment(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
testCases := []struct {
role string
passphrase string
}{
{role: "root", passphrase: "rootpassphrase"},
{role: "timestamp", passphrase: "timestamp5#$#@"},
{role: "snapshot", passphrase: "snapshot$#@"},
{role: "targets", passphrase: "$#^#$@targets"},
}
for _, tt := range testCases {
t.Run(tt.role, func(t *testing.T) {
tt := tt
t.Parallel()
handler := newPassphraseHandler()
envKey := fmt.Sprintf("FLEET_%s_PASSPHRASE", strings.ToUpper(tt.role))
require.NoError(t, os.Setenv(envKey, tt.passphrase))
passphrase, err := handler.getPassphrase(tt.role, false, false)
require.NoError(t, err)
assert.Equal(t, tt.passphrase, string(passphrase))
// Should work second time with cache
passphrase, err = handler.getPassphrase(tt.role, false, false)
require.NoError(t, err)
assert.Equal(t, tt.passphrase, string(passphrase))
})
}
}
func TestPassphraseHandlerEmpty(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
handler := newPassphraseHandler()
require.NoError(t, os.Setenv("FLEET_ROOT_PASSPHRASE", ""))
_, err := handler.getPassphrase("root", false, false)
require.Error(t, err)
}
func setPassphrases(t *testing.T) {
t.Helper()
require.NoError(t, os.Setenv("FLEET_ROOT_PASSPHRASE", "root"))
require.NoError(t, os.Setenv("FLEET_TIMESTAMP_PASSPHRASE", "timestamp"))
require.NoError(t, os.Setenv("FLEET_TARGETS_PASSPHRASE", "targets"))
require.NoError(t, os.Setenv("FLEET_SNAPSHOT_PASSPHRASE", "snapshot"))
}
func runUpdatesCommand(args ...string) error {
app := cli.NewApp()
app.Commands = []*cli.Command{UpdatesCommand()}
return app.Run(append([]string{os.Args[0], "updates"}, args...))
}
func TestUpdatesInit(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Should fail with already initialized
require.Error(t, runUpdatesCommand("init", "--path", tmpDir))
}
func TestUpdatesErrorInvalidPassphrase(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Should not be able to add with invalid passphrase
require.NoError(t, os.Setenv("FLEET_SNAPSHOT_PASSPHRASE", "invalid"))
// Reset the cache that already has correct passwords stored
passHandler = newPassphraseHandler()
require.Error(t, runUpdatesCommand("add", "--path", tmpDir, "--target", "anything", "--platform", "windows", "--name", "test", "--version", "1.3.4.7"))
}
func TestUpdatesInitKeysInitializedError(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
// Create an empty "keys" directory
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "keys"), os.ModePerm|os.ModeDir))
// Should fail with already initialized
require.Error(t, runUpdatesCommand("init", "--path", tmpDir))
}
func assertFileExists(t *testing.T, path string) {
t.Helper()
st, err := os.Stat(path)
require.NoError(t, err, "stat should succeed")
assert.True(t, st.Mode().IsRegular(), "should be regular file: %s", path)
}
func assertVersion(t *testing.T, expected int, versionFunc func() (int, error)) {
t.Helper()
actual, err := versionFunc()
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
// Capture stdout while running the updates roots command
func getRoots(t *testing.T, tmpDir string) string {
t.Helper()
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
require.NoError(t, runUpdatesCommand("roots", "--path", tmpDir))
require.NoError(t, w.Close())
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
// Check output
var keys []data.PublicKey
require.NoError(t, json.Unmarshal(out, &keys))
assert.Greater(t, len(keys[0].IDs()), 0)
assert.Equal(t, "ed25519", keys[0].Type)
return string(out)
}
func compressSingleFile(t *testing.T, filePath, outFilePath string) {
outf, err := secure.OpenFile(outFilePath, os.O_CREATE|os.O_WRONLY, constant.DefaultFileMode)
require.NoError(t, err)
defer outf.Close()
gw := gzip.NewWriter(outf)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
inf, err := os.Open(filePath)
require.NoError(t, err)
infi, err := inf.Stat()
require.NoError(t, err)
err = tw.WriteHeader(
&tar.Header{
Name: filepath.Base(filePath),
Size: infi.Size(),
Mode: 0o777,
},
)
require.NoError(t, err)
_, err = io.Copy(tw, inf)
require.NoError(t, err)
err = tw.Close()
require.NoError(t, err)
err = gw.Close()
require.NoError(t, err)
err = outf.Close()
require.NoError(t, err)
}
func TestUpdatesIntegration(t *testing.T) {
// Not t.Parallel() due to modifications to environment.
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
// Run an HTTP server to serve the update metadata
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join(tmpDir, "repository"))))
t.Cleanup(server.Close)
roots := getRoots(t, tmpDir)
// Use the current binary as target for this test so that it is a binary that
// is valid for execution on the current system.
testPath, err := os.Executable()
require.NoError(t, err)
tarGzFilePath := filepath.Join(filepath.Dir(testPath), "other.app.tar.gz")
compressSingleFile(t, testPath, tarGzFilePath)
// Initialize an update client
localStore, err := filestore.New(filepath.Join(tmpDir, "tuf-metadata.json"))
require.NoError(t, err)
updater, err := update.New(update.Options{
RootDirectory: tmpDir,
ServerURL: server.URL,
RootKeys: roots,
LocalStore: localStore,
Targets: update.Targets{
"test": update.TargetInfo{
Platform: "macos",
Channel: "1.3.3.7",
TargetFile: "test",
},
"other": update.TargetInfo{
Platform: "macos-app",
Channel: "1.3.3.8",
TargetFile: "other.app.tar.gz",
ExtractedExecSubPath: []string{filepath.Base(testPath)},
},
},
})
require.NoError(t, err)
require.NoError(t, updater.UpdateMetadata())
_, err = updater.Lookup("any")
require.Error(t, err, "lookup should fail before targets added")
repo, err := openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 1, repo.TargetsVersion)
assertVersion(t, 1, repo.SnapshotVersion)
assertVersion(t, 1, repo.TimestampVersion)
// Add some targets
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "linux", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "macos", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "windows", "--name", "test", "--version", "1.3.3.7"))
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", tarGzFilePath, "--platform", "macos-app", "--name", "other", "--version", "1.3.3.8"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "linux", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "macos", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "test", "windows", "1.3.3.7", "test"))
assertFileExists(t, filepath.Join(tmpDir, "repository", "targets", "other", "macos-app", "1.3.3.8", "other.app.tar.gz"))
// Verify the client can look up and download the updates
require.NoError(t, updater.UpdateMetadata())
targets, err := updater.Targets()
require.NoError(t, err)
assert.Len(t, targets, 4)
_, err = updater.Get("test")
require.NoError(t, err)
other, err := updater.Get("other")
require.NoError(t, err)
require.Equal(t, filepath.Base(other.ExecPath), filepath.Base(testPath))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 5, repo.TargetsVersion)
assertVersion(t, 5, repo.SnapshotVersion)
assertVersion(t, 5, repo.TimestampVersion)
require.NoError(t, runUpdatesCommand("timestamp", "--path", tmpDir))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 1, repo.RootVersion)
assertVersion(t, 5, repo.TargetsVersion)
assertVersion(t, 5, repo.SnapshotVersion)
assertVersion(t, 6, repo.TimestampVersion)
// Rotate root
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "root"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 2, repo.RootVersion)
assertVersion(t, 6, repo.TargetsVersion)
assertVersion(t, 6, repo.SnapshotVersion)
assertVersion(t, 7, repo.TimestampVersion)
// Rotate targets
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "targets"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 3, repo.RootVersion)
assertVersion(t, 7, repo.TargetsVersion)
assertVersion(t, 7, repo.SnapshotVersion)
assertVersion(t, 8, repo.TimestampVersion)
// Rotate snapshot
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "snapshot"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 4, repo.RootVersion)
assertVersion(t, 8, repo.TargetsVersion)
assertVersion(t, 8, repo.SnapshotVersion)
assertVersion(t, 9, repo.TimestampVersion)
// Rotate timestamp
require.NoError(t, runUpdatesCommand("rotate", "--path", tmpDir, "timestamp"))
repo, err = openRepo(tmpDir)
require.NoError(t, err)
assertVersion(t, 5, repo.RootVersion)
assertVersion(t, 9, repo.TargetsVersion)
assertVersion(t, 9, repo.SnapshotVersion)
assertVersion(t, 10, repo.TimestampVersion)
// Should still be able to add after rotations
require.NoError(t, runUpdatesCommand("add", "--path", tmpDir, "--target", testPath, "--platform", "windows", "--name", "test", "--version", "1.3.3.7"))
// Root key should have changed
newRoots := getRoots(t, tmpDir)
assert.NotEqual(t, roots, newRoots)
// Should still be able to retrieve an update after rotations
require.NoError(t, updater.UpdateMetadata())
targets, err = updater.Targets()
require.NoError(t, err)
assert.Len(t, targets, 4)
// Remove the old test copy first
p, err := updater.ExecutableLocalPath("test")
require.NoError(t, err)
require.NoError(t, os.RemoveAll(p))
_, err = updater.Get("test")
require.NoError(t, err)
// Remove the old other copy first
o, err := updater.Get("other")
require.NoError(t, err)
require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), "other.app.tar.gz")))
require.NoError(t, os.RemoveAll(filepath.Join(filepath.Dir(o.ExecPath), filepath.Base(testPath))))
o2, err := updater.Get("other")
require.NoError(t, err)
require.Equal(t, o, o2)
_, err = os.Stat(o2.ExecPath)
require.NoError(t, err)
// Update client should be able to initialize with new root
tmpDir = t.TempDir()
localStore, err = filestore.New(filepath.Join(tmpDir, "tuf-metadata.json"))
require.NoError(t, err)
updater, err = update.New(update.Options{RootDirectory: tmpDir, ServerURL: server.URL, RootKeys: roots, LocalStore: localStore})
require.NoError(t, err)
require.NoError(t, updater.UpdateMetadata())
}
func TestCommit(t *testing.T) {
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
initialEntries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
initialRoots := getRoots(t, tmpDir)
commit, _, err := startRotatePseudoTx(tmpDir)
require.NoError(t, err)
repo, err := openRepo(tmpDir)
require.NoError(t, err)
// Make rotations that change repo
require.NoError(t, updatesGenKey(repo, "root"))
require.NoError(t, repo.Sign("root.json"))
require.NoError(t, repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)))
require.NoError(t, repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)))
require.NoError(t, repo.Commit())
// Assert directory has changed after commit.
require.NoError(t, commit())
entries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
assert.NotEqual(t, initialEntries, entries)
_, err = os.Stat(filepath.Join(tmpDir, "repository", ".backup"))
assert.True(t, os.IsNotExist(err))
// Roots should have changed.
roots := getRoots(t, tmpDir)
assert.NotEqual(t, initialRoots, roots)
}
func TestRollback(t *testing.T) {
tmpDir := t.TempDir()
setPassphrases(t)
require.NoError(t, runUpdatesCommand("init", "--path", tmpDir))
initialEntries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
initialRoots := getRoots(t, tmpDir)
_, rollback, err := startRotatePseudoTx(tmpDir)
require.NoError(t, err)
repo, err := openRepo(tmpDir)
require.NoError(t, err)
// Make rotations that change repo
require.NoError(t, updatesGenKey(repo, "root"))
require.NoError(t, repo.Sign("root.json"))
require.NoError(t, repo.SnapshotWithExpires(time.Now().Add(snapshotExpirationDuration)))
require.NoError(t, repo.TimestampWithExpires(time.Now().Add(timestampExpirationDuration)))
require.NoError(t, repo.Commit())
// Assert directory has NOT changed after rollback.
require.NoError(t, rollback())
entries, err := os.ReadDir(filepath.Join(tmpDir, "repository"))
require.NoError(t, err)
assert.Equal(t, initialEntries, entries)
_, err = os.Stat(filepath.Join(tmpDir, "repository", ".backup"))
assert.True(t, os.IsNotExist(err))
// Roots should NOT have changed.
repo, err = openRepo(tmpDir)
require.NoError(t, err)
roots := getRoots(t, tmpDir)
assert.Equal(t, initialRoots, roots)
}