mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
b65739f035
This should fix #16006. On Windows when a process is killed by the Task Manager, it is killed without any signaling, thus the osqueryd processes are left orphaned. Executing osqueryd (which we do to get host information) was failing because the lingering processes had a lock on the database file. The solution implemented in this PR is to kill any pre-existing osqueryd processes before running osqueryd. - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [X] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). PS: I added a log of the stdout+stderr of osqueryd execution when such command fails to execute. (This helped me find the root cause.) ``` 2024-01-25T11:57:56-08:00 ERR getHostInfo via osquery output= stderr="E0125 11:57:56.744843 7860 shutdown.cpp:79] IO error: Failed to create lock file: C:\\Program Files\\Orbit\\osquery.db/LOCK: The process cannot access the file because it is being used by another process.\r\r\n" ``` PPS: I removed some unused exported methods in the `orbit/pkg/platform` package.
220 lines
7.2 KiB
Go
220 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
// Generated using this command in `go env GOROOT`/src/crypto/tls:
|
|
// go run generate_cert.go --rsa-bits 1024 --host example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
|
// Certificate is only valid for example.com, and so should fail validation
|
|
// with a localhost-running httptest.NewTLSServer.
|
|
exampleDotComCertDotPem = `-----BEGIN CERTIFICATE-----
|
|
MIICGzCCAYSgAwIBAgIRAM596905ZjtK0p+hURZWO7IwDQYJKoZIhvcNAQELBQAw
|
|
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
|
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
|
|
gYkCgYEA57PzoKfRgAYvOte5RVKEm4g6hD6jhxeg/lyvuidbuL9XzyvWesKGqxXh
|
|
LxMTrAeH1T3LbLlU0c/OdwcPQRLErqXee0YM3OeVhlZLnnOfyywE7WRFwAtS+uSm
|
|
m61Mrx8VHLqXiN8R3yQPiHmekuHIDMvIkC793d2YpaV02grWH7ECAwEAAaNvMG0w
|
|
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
|
|
MAMBAf8wHQYDVR0OBBYEFI3hGM84qbH234gBQmbCShCq0430MBYGA1UdEQQPMA2C
|
|
C2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4GBAHqLUn9kpHdAElEwAP/7Xoth
|
|
yWkBFCfkIy2ftaWJKTB1nDfxbdEuJ1BfMDYyM5anYd+d/Id7w3fe3Wn+VkOnxxtZ
|
|
oug6edBNpdhp8r2/4t6n3AouK0/zG2naAlmXV0JoFuEvy2bX0BbbbPg+v4WNZIsC
|
|
0cUq8IOA9g0kHJar8rAI
|
|
-----END CERTIFICATE-----`
|
|
)
|
|
|
|
func TestDebugConnectionCommand(t *testing.T) {
|
|
t.Run("without certificate", func(t *testing.T) {
|
|
_, ds := runServerWithMockedDS(t)
|
|
|
|
ds.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
|
return nil, errors.New("invalid")
|
|
}
|
|
|
|
output := runAppForTest(t, []string{"debug", "connection"})
|
|
// 3 successes: resolve host, dial address, check api endpoint
|
|
require.Equal(t, 3, strings.Count(output, "Success:"))
|
|
})
|
|
|
|
t.Run("invalid certificate flag without address", func(t *testing.T) {
|
|
_, err := runAppNoChecks([]string{"debug", "connection", "--fleet-certificate", "cert.pem"})
|
|
require.Contains(t, err.Error(), "--fleet-certificate")
|
|
})
|
|
|
|
t.Run("invalid context flag with address", func(t *testing.T) {
|
|
_, err := runAppNoChecks([]string{"debug", "connection", "--context", "test", "localhost:8080"})
|
|
require.Contains(t, err.Error(), "--context")
|
|
})
|
|
|
|
t.Run("invalid config flag with address", func(t *testing.T) {
|
|
_, err := runAppNoChecks([]string{"debug", "connection", "--config", "/tmp/nosuchfile", "localhost:8080"})
|
|
require.Contains(t, err.Error(), "--config")
|
|
})
|
|
|
|
t.Run("with valid certificate", func(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprint(w, `{"error": "error", "node_invalid": true}`)
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv("FLEET_SERVER_ADDRESS", srv.URL)
|
|
|
|
// get the certificate of the TLS server
|
|
certPath := rawCertToPemFile(t, srv.Certificate().Raw)
|
|
|
|
output := runAppForTest(t, []string{"debug", "connection", "--fleet-certificate", certPath, srv.URL})
|
|
// 4 successes: resolve host, dial address, certificate, check api endpoint
|
|
t.Log(output)
|
|
require.Equal(t, 4, strings.Count(output, "Success:"))
|
|
})
|
|
|
|
t.Run("with invalid certificate", func(t *testing.T) {
|
|
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprint(w, `{"error": "error", "node_invalid": true}`)
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv("FLEET_SERVER_ADDRESS", srv.URL)
|
|
|
|
// get the invalid certificate (for example.com)
|
|
dir := t.TempDir()
|
|
certPath := filepath.Join(dir, "cert.pem")
|
|
require.NoError(t, os.WriteFile(certPath, []byte(exampleDotComCertDotPem), 0o600))
|
|
|
|
buf, err := runAppNoChecks([]string{"debug", "connection", "--fleet-certificate", certPath, srv.URL})
|
|
// 2 successes: resolve host, dial address
|
|
t.Log(buf.String())
|
|
require.Equal(t, 2, strings.Count(buf.String(), "Success:"))
|
|
// 1 failure: invalid certificate
|
|
require.Error(t, err)
|
|
require.Equal(t, 1, strings.Count(err.Error(), "Fail: certificate:"))
|
|
})
|
|
}
|
|
|
|
// encodes raw certificate bytes to a PEM-encoded temp file, returns the path.
|
|
func rawCertToPemFile(t *testing.T, raw []byte) string {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
require.NoError(t, pem.Encode(&buf, &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: raw,
|
|
}))
|
|
|
|
dir := t.TempDir()
|
|
certPath := filepath.Join(dir, "cert.pem")
|
|
require.NoError(t, os.WriteFile(certPath, buf.Bytes(), 0o600))
|
|
return certPath
|
|
}
|
|
|
|
func TestDebugCheckAPIEndpoint(t *testing.T) {
|
|
const timeout = 100 * time.Millisecond
|
|
cases := [...]struct {
|
|
code int // == 0 panics, negative value waits for timeout, sets status code to absolute value
|
|
body string
|
|
errContains string // empty if checkAPIEndpoint should not return an error
|
|
}{
|
|
{401, `{"error": "fail", "node_invalid": true}`, ""},
|
|
{-401, `{"error": "fail", "node_invalid": true}`, "deadline exceeded"},
|
|
{200, `{"error": "", "node_invalid": false}`, "unexpected 200 response"},
|
|
{0, `panic`, "EOF"},
|
|
}
|
|
|
|
var callCount int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
res := cases[atomic.LoadInt32(&callCount)]
|
|
|
|
switch {
|
|
case res.code == 0:
|
|
panic(res.body)
|
|
case res.code < 0:
|
|
time.Sleep(timeout + timeout/10)
|
|
res.code = -res.code
|
|
}
|
|
w.WriteHeader(res.code)
|
|
fmt.Fprint(w, res.body)
|
|
}))
|
|
t.Cleanup(func() {
|
|
srv.Close()
|
|
})
|
|
|
|
t.Setenv("FLEET_SERVER_ADDRESS", srv.URL)
|
|
cli, base, err := rawHTTPClientFromConfig(Context{Address: srv.URL, TLSSkipVerify: true})
|
|
require.NoError(t, err)
|
|
for i, c := range cases {
|
|
atomic.StoreInt32(&callCount, int32(i))
|
|
t.Run(fmt.Sprint(c.code), func(t *testing.T) {
|
|
err := checkAPIEndpoint(context.Background(), timeout, base, cli)
|
|
if c.errContains == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebugResolveHostname(t *testing.T) {
|
|
const timeout = 100 * time.Millisecond
|
|
|
|
// resolves host name
|
|
err := resolveHostname(context.Background(), timeout, "localhost")
|
|
require.NoError(t, err)
|
|
|
|
// resolves ip4 address
|
|
err = resolveHostname(context.Background(), timeout, "127.0.0.1")
|
|
require.NoError(t, err)
|
|
|
|
// resolves ip6 address
|
|
err = resolveHostname(context.Background(), timeout, "::1")
|
|
require.NoError(t, err)
|
|
|
|
// fails on invalid host
|
|
randBytes := make([]byte, 8)
|
|
_, err = rand.Read(randBytes)
|
|
require.NoError(t, err)
|
|
noSuchHost := "no_such_host" + hex.EncodeToString(randBytes)
|
|
|
|
err = resolveHostname(context.Background(), timeout, noSuchHost)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFilenameFunctions(t *testing.T) {
|
|
nowFn = func() time.Time {
|
|
now, _ := time.Parse(time.RFC3339, "1969-06-19T21:44:05Z")
|
|
return now
|
|
}
|
|
defer func() { nowFn = time.Now }()
|
|
|
|
t.Run("outfileName builds a file name using the name provided + current time ", func(t *testing.T) {
|
|
name := outfileName("test")
|
|
assert.Equal(t, "fleet-test-19690619214405Z", name)
|
|
})
|
|
|
|
t.Run("outfileNameWithExt builds a file name using the name and extension provided + current time ", func(t *testing.T) {
|
|
name := outfileNameWithExt("test", "go")
|
|
assert.Equal(t, "fleet-test-19690619214405Z.go", name)
|
|
})
|
|
}
|