mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Merge branch 'main' into feature_19010-ipad-ios-lock-wipe
This commit is contained in:
commit
63a6bb8bf3
87 changed files with 814 additions and 307 deletions
1
.github/ISSUE_TEMPLATE/story.md
vendored
1
.github/ISSUE_TEMPLATE/story.md
vendored
|
|
@ -35,6 +35,7 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan
|
|||
- [ ] UI changes: TODO <!-- Insert the link to the relevant Figma cover page. Remove this checkbox if there are no changes to the user interface. -->
|
||||
- [ ] CLI usage changes: TODO <!-- Insert the link to the relevant Figma cover page. Remove this checkbox if there are no changes to the CLI. -->
|
||||
- [ ] REST API changes: TODO <!-- Specify changes as a draft PR to the REST API doc page. Remove this checkbox if there are no changes necessary. Move this item to the engineering list below if engineering will design the API changes. -->
|
||||
- [ ] Fleet's agent (fleetd) changes: TODO <!-- Specify changes to fleetd. If the change requires a new Fleet (server) version, consider specifying to only enable this change in new Fleet versions. Remove this checkbox if there are no changes necessary. -->
|
||||
- [ ] Permissions changes: TODO <!-- Specify changes as a draft PR to the Manage access doc page. If doc changes aren't necessary, explicitly mention no changes to the doc page. Remove this checkbox if there are no permissions changes. -->
|
||||
- [ ] Outdated documentation changes: TODO <!-- Specify required documentation changes (public-facing fleetdm.com/docs or contributors) & redirects to add to /website/config/routes.js. -->
|
||||
- [ ] Changes to paid features or tiers: TODO <!-- Specify "Fleet Free" or "Fleet Premium". If only certain parts of the user story involve paid features, specify which parts. Implementation of paid features should live in the `ee/` directory. -->
|
||||
|
|
|
|||
2
changes/16961-return-api-token-for-api-only-users
Normal file
2
changes/16961-return-api-token-for-api-only-users
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Endpoint `/api/latest/fleet/users/admin` to return API token when creating API-only (non-SSO) users.
|
||||
- Added API-token of the created API-only (non-SSO) user to the output of `fleetctl user create --api-only`.
|
||||
1
changes/18427-cert-names
Normal file
1
changes/18427-cert-names
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Use Fleet instead of FleetDM in MDM certificates
|
||||
1
changes/19512-mdm-migration-sonoma
Normal file
1
changes/19512-mdm-migration-sonoma
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fixed bug where MDM migration failed when attempting to renew enrollment profiles on macOS Sonoma devices.
|
||||
2
changes/19545-unlock-pin
Normal file
2
changes/19545-unlock-pin
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
* /api/latest/fleet/hosts/:id/lock returns `unlock_pin` for Apple hosts
|
||||
* UI no longer uses unlock pending state for Apple hosts
|
||||
1
changes/19600-add-config-to-set-query-report-cap
Normal file
1
changes/19600-add-config-to-set-query-report-cap
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added a server setting to configure the query repory cap size, `server_settings.query_report_cap` (default is 1000).
|
||||
1
changes/part-of-19072-use-reader-db-for-stats
Normal file
1
changes/part-of-19072-use-reader-db-for-stats
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Improved db usage when sending statistics
|
||||
|
|
@ -418,6 +418,7 @@ func TestFullGlobalGitOps(t *testing.T) {
|
|||
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
|
||||
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
|
||||
assert.Contains(t, string(*savedAppConfig.AgentOptions), "distributed_denylist_duration")
|
||||
assert.Equal(t, 2000, savedAppConfig.ServerSettings.QueryReportCap)
|
||||
assert.Len(t, enrolledSecrets, 2)
|
||||
assert.True(t, policyDeleted)
|
||||
assert.Len(t, appliedPolicySpecs, 5)
|
||||
|
|
@ -923,7 +924,6 @@ team_settings:
|
|||
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--delete-other-teams"})
|
||||
assert.True(t, ds.ListTeamsFuncInvoked)
|
||||
assert.True(t, ds.DeleteTeamFuncInvoked)
|
||||
|
||||
}
|
||||
|
||||
func TestFullGlobalAndTeamGitOps(t *testing.T) {
|
||||
|
|
@ -1059,7 +1059,6 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, **fleet.Team) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
|
@ -361,13 +362,6 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledUP := &fleet.Host{
|
||||
ID: 9,
|
||||
UUID: "mac-enrolled-up",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
|
||||
winEnrolledLP := &fleet.Host{
|
||||
ID: 10,
|
||||
|
|
@ -409,7 +403,6 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
macPending,
|
||||
winPending,
|
||||
winEnrolledUP,
|
||||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
|
|
@ -421,7 +414,6 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
|
||||
unlockPending := map[uint]*fleet.Host{
|
||||
winEnrolledUP.ID: winEnrolledUP,
|
||||
macEnrolledUP.ID: macEnrolledUP,
|
||||
}
|
||||
|
||||
lockPending := map[uint]*fleet.Host{
|
||||
|
|
@ -446,9 +438,7 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
return &status, nil
|
||||
return nil, errors.New("apple devices do not have an unlock pending state")
|
||||
}
|
||||
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
|
|
@ -542,7 +532,6 @@ fleetctl mdm unlock --host=%s
|
|||
{appCfgWinMDM, "valid windows but pending ", []string{"--host", winPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`},
|
||||
{appCfgMacMDM, "valid macos but pending", []string{"--host", macPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`},
|
||||
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
|
|
@ -603,13 +592,6 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledUP := &fleet.Host{
|
||||
ID: 9,
|
||||
UUID: "mac-enrolled-up",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledLP := &fleet.Host{
|
||||
ID: 10,
|
||||
UUID: "win-enrolled-lp",
|
||||
|
|
@ -650,7 +632,6 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
macPending,
|
||||
winPending,
|
||||
winEnrolledUP,
|
||||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
|
|
@ -667,7 +648,6 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
|
||||
unlockPending := map[uint]*fleet.Host{
|
||||
winEnrolledUP.ID: winEnrolledUP,
|
||||
macEnrolledUP.ID: macEnrolledUP,
|
||||
}
|
||||
|
||||
lockPending := map[uint]*fleet.Host{
|
||||
|
|
@ -701,9 +681,7 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
return &status, nil
|
||||
return nil, errors.New("apple devices do not have an unlock pending state")
|
||||
}
|
||||
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
|
|
@ -800,7 +778,6 @@ fleetctl get host %s
|
|||
{appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`},
|
||||
{appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`},
|
||||
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, ""},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
|
|
@ -856,13 +833,6 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
macEnrolledUP := &fleet.Host{
|
||||
ID: 9,
|
||||
UUID: "mac-enrolled-up",
|
||||
Platform: "darwin",
|
||||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
|
||||
}
|
||||
winEnrolledLP := &fleet.Host{
|
||||
ID: 10,
|
||||
UUID: "win-enrolled-lp",
|
||||
|
|
@ -950,7 +920,6 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
macPending,
|
||||
winPending,
|
||||
winEnrolledUP,
|
||||
macEnrolledUP,
|
||||
winEnrolledLP,
|
||||
macEnrolledLP,
|
||||
winEnrolledWP,
|
||||
|
|
@ -971,7 +940,6 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
|
||||
unlockPending := map[uint]*fleet.Host{
|
||||
winEnrolledUP.ID: winEnrolledUP,
|
||||
macEnrolledUP.ID: macEnrolledUP,
|
||||
}
|
||||
|
||||
lockPending := map[uint]*fleet.Host{
|
||||
|
|
@ -1010,9 +978,7 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
|
||||
if _, ok := unlockPending[host.ID]; ok {
|
||||
if fleetPlatform == "darwin" {
|
||||
status.UnlockPIN = "1234"
|
||||
status.UnlockRequestedAt = time.Now()
|
||||
return &status, nil
|
||||
return nil, errors.New("apple devices do not have an unlock pending state")
|
||||
}
|
||||
|
||||
status.UnlockScript = &fleet.HostScriptResult{}
|
||||
|
|
@ -1129,7 +1095,6 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
{appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
|
||||
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
|
||||
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_report_cap": 0,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_report_cap: 0
|
||||
query_reports_disabled: false
|
||||
server_url: ""
|
||||
scripts_disabled: false
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_report_cap": 0,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_report_cap: 0
|
||||
query_reports_disabled: false
|
||||
server_url: ""
|
||||
scripts_disabled: false
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ org_settings:
|
|||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 2000
|
||||
query_reports_disabled: false
|
||||
scripts_disabled: false
|
||||
server_url: $FLEET_SERVER_URL
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 0
|
||||
query_reports_disabled: false
|
||||
server_url: https://example.org
|
||||
scripts_disabled: false
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 0
|
||||
query_reports_disabled: false
|
||||
server_url: https://example.org
|
||||
scripts_disabled: false
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ func createUserCommand() *cli.Command {
|
|||
force_reset := !sso && !apiOnly
|
||||
|
||||
// password requirements are validated as part of `CreateUser`
|
||||
err = client.CreateUser(fleet.UserPayload{
|
||||
sessionKey, err := client.CreateUser(fleet.UserPayload{
|
||||
Password: &password,
|
||||
Email: &email,
|
||||
Name: &name,
|
||||
|
|
@ -174,6 +174,10 @@ func createUserCommand() *cli.Command {
|
|||
return fmt.Errorf("Failed to create user: %w", err)
|
||||
}
|
||||
|
||||
if apiOnly && sessionKey != nil && *sessionKey != "" {
|
||||
fmt.Fprintf(c.App.Writer, "Success! The API token for your new user is: %s\n", *sessionKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -208,7 +212,6 @@ func createBulkUsersCommand() *cli.Command {
|
|||
}
|
||||
defer csvFile.Close()
|
||||
csvLines, err := csv.NewReader(csvFile).ReadAll()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -278,7 +281,7 @@ func createBulkUsersCommand() *cli.Command {
|
|||
}
|
||||
|
||||
for _, user := range users {
|
||||
err = client.CreateUser(user)
|
||||
_, err = client.CreateUser(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create user: %w", err)
|
||||
}
|
||||
|
|
@ -351,7 +354,6 @@ func deleteBulkUsersCommand() *cli.Command {
|
|||
}
|
||||
defer csvFile.Close()
|
||||
csvLines, err := csv.NewReader(csvFile).ReadAll()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -362,10 +364,10 @@ func deleteBulkUsersCommand() *cli.Command {
|
|||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomPassword() (string, error) {
|
||||
password, err := password.Generate(20, 2, 2, false, true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -73,31 +74,57 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
|
|||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
|
||||
if email == "bar@example.com" {
|
||||
apiOnlyUser := &fleet.User{
|
||||
ID: 1,
|
||||
Email: email,
|
||||
}
|
||||
err := apiOnlyUser.SetPassword(pwd, 24, 10)
|
||||
require.NoError(t, err)
|
||||
return apiOnlyUser, nil
|
||||
}
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
var apiOnlyUserSessionKey string
|
||||
ds.NewSessionFunc = func(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
|
||||
apiOnlyUserSessionKey = sessionKey
|
||||
return &fleet.Session{
|
||||
ID: 2,
|
||||
UserID: userID,
|
||||
Key: sessionKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedAdminForcePasswordReset bool
|
||||
displaysToken bool
|
||||
}{
|
||||
{
|
||||
name: "sso",
|
||||
args: []string{"--email", "foo@example.com", "--name", "foo", "--sso"},
|
||||
expectedAdminForcePasswordReset: false,
|
||||
displaysToken: false,
|
||||
},
|
||||
{
|
||||
name: "api-only",
|
||||
args: []string{"--email", "bar@example.com", "--password", pwd, "--name", "bar", "--api-only"},
|
||||
expectedAdminForcePasswordReset: false,
|
||||
displaysToken: true,
|
||||
},
|
||||
{
|
||||
name: "api-only-sso",
|
||||
args: []string{"--email", "baz@example.com", "--name", "baz", "--api-only", "--sso"},
|
||||
expectedAdminForcePasswordReset: false,
|
||||
displaysToken: false,
|
||||
},
|
||||
{
|
||||
name: "non-sso-non-api-only",
|
||||
args: []string{"--email", "zoo@example.com", "--password", pwd, "--name", "zoo"},
|
||||
expectedAdminForcePasswordReset: true,
|
||||
displaysToken: false,
|
||||
},
|
||||
} {
|
||||
ds.NewUserFuncInvoked = false
|
||||
|
|
@ -106,10 +133,15 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
|
|||
return user, nil
|
||||
}
|
||||
|
||||
require.Equal(t, "", runAppForTest(t, append(
|
||||
stdout := runAppForTest(t, append(
|
||||
[]string{"user", "create"},
|
||||
tc.args...,
|
||||
)))
|
||||
))
|
||||
if tc.displaysToken {
|
||||
require.Equal(t, stdout, fmt.Sprintf("Success! The API token for your new user is: %s\n", apiOnlyUserSessionKey))
|
||||
} else {
|
||||
require.Empty(t, stdout)
|
||||
}
|
||||
require.True(t, ds.NewUserFuncInvoked)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,22 +38,22 @@ func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, incl
|
|||
return svc.Service.OSVersion(ctx, osID, teamID, true)
|
||||
}
|
||||
|
||||
func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||
func (svc *Service) LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error) {
|
||||
// First ensure the user has access to list hosts, then check the specific
|
||||
// host once team_id is loaded.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host lite")
|
||||
return "", ctxerr.Wrap(ctx, err, "get host lite")
|
||||
}
|
||||
|
||||
// Authorize again with team loaded now that we have the host's team_id.
|
||||
// Authorize as "execute mdm_command", which is the correct access
|
||||
// requirement and is what happens for macOS platforms.
|
||||
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// locking validations are based on the platform of the host
|
||||
|
|
@ -65,19 +65,23 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
|||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
return "", ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
|
||||
}
|
||||
|
||||
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
|
||||
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."),
|
||||
)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "get host MDM information")
|
||||
return "", ctxerr.Wrap(ctx, err, "get host MDM information")
|
||||
}
|
||||
if !hostMDM.IsFleetEnrolled() {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."),
|
||||
)
|
||||
}
|
||||
|
||||
case "windows", "linux":
|
||||
|
|
@ -86,27 +90,30 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
|||
if errors.Is(err, fleet.ErrMDMNotConfigured) {
|
||||
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
|
||||
}
|
||||
}
|
||||
// on windows and linux, a script is used to lock the host so scripts must
|
||||
// be enabled
|
||||
appCfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get app config")
|
||||
return "", ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
if appCfg.ServerSettings.ScriptsDisabled {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx,
|
||||
fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."),
|
||||
)
|
||||
}
|
||||
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
|
||||
switch {
|
||||
case err != nil:
|
||||
// If not found, then do nothing. We do not know if this host has scripts enabled or not
|
||||
if !fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx, err, "get host orbit info")
|
||||
return "", ctxerr.Wrap(ctx, err, "get host orbit info")
|
||||
}
|
||||
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
||||
return ctxerr.Wrap(
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError(
|
||||
"host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
||||
),
|
||||
|
|
@ -114,26 +121,37 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
|||
}
|
||||
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
}
|
||||
|
||||
// if there's a lock, unlock or wipe action pending, do not accept the lock
|
||||
// request.
|
||||
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
|
||||
}
|
||||
switch {
|
||||
case lockWipe.IsPendingLock():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online."),
|
||||
)
|
||||
case lockWipe.IsPendingUnlock():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError(
|
||||
"host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.",
|
||||
),
|
||||
)
|
||||
case lockWipe.IsPendingWipe():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx,
|
||||
fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."),
|
||||
)
|
||||
case lockWipe.IsWiped():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."))
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."),
|
||||
)
|
||||
case lockWipe.IsLocked():
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
|
||||
}
|
||||
|
||||
// all good, go ahead with queuing the lock request.
|
||||
|
|
@ -333,19 +351,21 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
|
|||
return svc.enqueueWipeHostRequest(ctx, host, lockWipe)
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) error {
|
||||
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (
|
||||
unlockPIN string, err error,
|
||||
) {
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return fleet.ErrNoContext
|
||||
return "", fleet.ErrNoContext
|
||||
}
|
||||
|
||||
if lockStatus.HostFleetPlatform == "darwin" {
|
||||
lockCommandUUID := uuid.NewString()
|
||||
if err := svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin")
|
||||
if unlockPIN, err = svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin")
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(
|
||||
if err = svc.NewActivity(
|
||||
ctx,
|
||||
vc.User,
|
||||
fleet.ActivityTypeLockedHost{
|
||||
|
|
@ -353,10 +373,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
|
|||
HostDisplayName: host.DisplayName(),
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for darwin lock host request")
|
||||
return "", ctxerr.Wrap(ctx, err, "create activity for darwin lock host request")
|
||||
}
|
||||
|
||||
return nil
|
||||
return unlockPIN, nil
|
||||
}
|
||||
|
||||
script := windowsLockScript
|
||||
|
|
@ -376,7 +396,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
|
|||
UserID: &vc.User.ID,
|
||||
SyncRequest: false,
|
||||
}, host.FleetPlatform()); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(
|
||||
|
|
@ -387,10 +407,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
|
|||
HostDisplayName: host.DisplayName(),
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for lock host request")
|
||||
return "", ctxerr.Wrap(ctx, err, "create activity for lock host request")
|
||||
}
|
||||
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (string, error) {
|
||||
|
|
@ -401,7 +421,9 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
|
|||
|
||||
var unlockPIN string
|
||||
if lockStatus.HostFleetPlatform == "darwin" {
|
||||
// record the unlock request if it was not already recorded
|
||||
// Record the unlock request time if it was not already recorded.
|
||||
// It should be always recorded, since the UnlockRequestedAt time is created after the lock command is acknowledged.
|
||||
// This code is left here to catch potential issues.
|
||||
if lockStatus.UnlockRequestedAt.IsZero() {
|
||||
if err := svc.ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC()); err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ func (svc *Service) MDMAppleDeviceLock(ctx context.Context, hostID uint) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String())
|
||||
_, err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = {
|
|||
},
|
||||
},
|
||||
],
|
||||
report_clipped: false,
|
||||
};
|
||||
|
||||
const createMockQueryReport = (
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export interface IQueryReportResultRow {
|
|||
export interface IQueryReport {
|
||||
query_id: number;
|
||||
results: IQueryReportResultRow[];
|
||||
report_clipped: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,27 @@ import React from "react";
|
|||
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import { IDeviceUserResponse } from "interfaces/host";
|
||||
|
||||
interface IAutoEnrollMdmModalProps {
|
||||
host: IDeviceUserResponse["host"];
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "auto-enroll-mdm-modal";
|
||||
|
||||
const AutoEnrollMdmModal = ({
|
||||
host: { platform, os_version },
|
||||
onCancel,
|
||||
}: IAutoEnrollMdmModalProps): JSX.Element => {
|
||||
let isMacOsSonomaOrLater = false;
|
||||
if (platform === "darwin" && os_version.startsWith("macOS ")) {
|
||||
const [major] = os_version
|
||||
.replace("macOS ", "")
|
||||
.split(".")
|
||||
.map((s) => parseInt(s, 10));
|
||||
isMacOsSonomaOrLater = major >= 14;
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title="Turn on MDM"
|
||||
|
|
@ -25,13 +36,21 @@ const AutoEnrollMdmModal = ({
|
|||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Open your Mac’s notification center by selecting the date and time
|
||||
in the top right corner of your screen.
|
||||
From the Apple menu in the top left corner of your screen, select{" "}
|
||||
<b>System Settings</b> or <b>System Preferences</b>.
|
||||
</li>
|
||||
<li>
|
||||
Select the <b>Device Enrollment</b> notification. This will open{" "}
|
||||
<b>System Settings</b> or <b>System Preferences</b>. Select{" "}
|
||||
<b>Allow</b>.
|
||||
{isMacOsSonomaOrLater ? (
|
||||
<>
|
||||
In the sidebar menu, select <b>Enroll in Remote Management</b>,
|
||||
and select <b>Enroll</b>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
In the search bar, type “Profiles.” Select <b>Profiles</b>, find
|
||||
and select <b>Enrollment Profile</b>, and select <b>Install</b>.
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
Enter your password, and select <b>Enroll</b>.
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ const DeviceUserPage = ({
|
|||
|
||||
const renderEnrollMdmModal = () => {
|
||||
return host?.dep_assigned_to_fleet ? (
|
||||
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
|
||||
<AutoEnrollMdmModal host={host} onCancel={toggleEnrollMdmModal} />
|
||||
) : (
|
||||
<ManualEnrollMdmModal
|
||||
onCancel={toggleEnrollMdmModal}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import NoResults from "../components/NoResults/NoResults";
|
|||
import {
|
||||
DEFAULT_SORT_HEADER,
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
QUERY_REPORT_RESULTS_LIMIT,
|
||||
} from "./QueryDetailsPageConfig";
|
||||
|
||||
interface IQueryDetailsPageProps {
|
||||
|
|
@ -199,8 +198,7 @@ const QueryDetailsPage = ({
|
|||
|
||||
const isLoading = isStoredQueryLoading || isQueryReportLoading;
|
||||
const isApiError = storedQueryError || queryReportError;
|
||||
const isClipped =
|
||||
(queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT;
|
||||
const isClipped = queryReport?.report_clipped;
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
|
||||
const renderHeader = () => {
|
||||
|
|
|
|||
|
|
@ -11,5 +11,3 @@ export type QueryDetailsPageQueryParams = Record<
|
|||
|
||||
export const DEFAULT_SORT_HEADER = "host_name";
|
||||
export const DEFAULT_SORT_DIRECTION = "asc";
|
||||
|
||||
export const QUERY_REPORT_RESULTS_LIMIT = 1000;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ describe("QueryReport", () => {
|
|||
columns: { col1: "value3", col2: "value4" },
|
||||
},
|
||||
],
|
||||
report_clipped: false,
|
||||
},
|
||||
];
|
||||
render(<QueryReport {...{ isClipped, queryReport }} />);
|
||||
|
|
@ -56,6 +57,7 @@ describe("QueryReport", () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
report_clipped: false,
|
||||
},
|
||||
];
|
||||
render(<QueryReport {...{ isClipped, queryReport }} />);
|
||||
|
|
@ -83,6 +85,7 @@ describe("QueryReport", () => {
|
|||
columns: { col1: "value1", col2: "value2" },
|
||||
},
|
||||
],
|
||||
report_clipped: true,
|
||||
},
|
||||
];
|
||||
render(<QueryReport {...{ isClipped, queryReport }} />);
|
||||
|
|
|
|||
|
|
@ -247,6 +247,6 @@ func parseEnrollmentProfileValue(line []byte, key string) (string, bool) {
|
|||
|
||||
// showEnrollmentProfileCmd is declared as a variable so it can be overwritten by tests.
|
||||
var showEnrollmentProfileCmd = func() ([]byte, error) {
|
||||
cmd := exec.Command("/usr/bin/profiles", "show", "-type", "enrollment")
|
||||
cmd := exec.Command("sh", "-c", `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles show -type enrollment`)
|
||||
return cmd.Output()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
package update
|
||||
|
||||
func runRenewEnrollmentProfile() error {
|
||||
return runCmdCollectErr("/usr/bin/profiles", "renew", "--type", "enrollment")
|
||||
cmd := `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles renew -type enrollment`
|
||||
return runCmdCollectErr("sh", "-c", cmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,14 +104,15 @@ func (h *renewEnrollmentProfileConfigReceiver) Run(config *fleet.OrbitConfig) er
|
|||
fn = runRenewEnrollmentProfile
|
||||
}
|
||||
if err := fn(); err != nil {
|
||||
// TODO: Look into whether we should increment lastRun here or implement a
|
||||
// backoff to avoid unnecessary user notification popups and mitigate rate
|
||||
// limiting by Apple.
|
||||
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
|
||||
} else {
|
||||
h.lastRun = time.Now()
|
||||
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
|
||||
// TODO: Design a better way to backoff `profiles show` so that the device doesn't get rate
|
||||
// limited by Apple. For now, wait at least 2 minutes before retrying.
|
||||
h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
|
||||
return nil
|
||||
}
|
||||
h.lastRun = time.Now()
|
||||
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
|
||||
|
||||
} else {
|
||||
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
|
@ -48,7 +50,7 @@ const mdmUnenrollmentTotalWaitTime = 90 * time.Second
|
|||
// between unenrollment checks.
|
||||
const defaultUnenrollmentRetryInterval = 5 * time.Second
|
||||
|
||||
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
|
||||
var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemplate").Parse(`
|
||||
## Migrate to Fleet
|
||||
|
||||
Select **Start** and look for this notification in your notification center:` +
|
||||
|
|
@ -56,6 +58,14 @@ Select **Start** and look for this notification in your notification center:` +
|
|||
"After you start, this window will popup every 15-20 minutes until you finish.",
|
||||
))
|
||||
|
||||
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
|
||||
## Migrate to Fleet
|
||||
|
||||
Select **Start** and Remote Management window will appear soon:` +
|
||||
"\n\n\n\n" +
|
||||
"After you start, this window will popup every 15-20 minutes until you finish.",
|
||||
))
|
||||
|
||||
var errorTemplate = template.Must(template.New("").Parse(`
|
||||
### Something's gone wrong.
|
||||
|
||||
|
|
@ -291,29 +301,9 @@ func (m *swiftDialogMDMMigrator) waitForUnenrollment() error {
|
|||
}
|
||||
|
||||
func (m *swiftDialogMDMMigrator) renderMigration() error {
|
||||
var message bytes.Buffer
|
||||
if err := mdmMigrationTemplate.Execute(
|
||||
&message,
|
||||
m.props,
|
||||
); err != nil {
|
||||
return fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
flags := []string{
|
||||
// main button
|
||||
"--button1text", "Start",
|
||||
// secondary button
|
||||
"--button2text", "Later",
|
||||
"--height", "440",
|
||||
}
|
||||
|
||||
if m.props.OrgInfo.ContactURL != "" {
|
||||
flags = append(flags,
|
||||
// info button
|
||||
"--infobuttontext", "Unsure? Contact IT",
|
||||
"--infobuttonaction", m.props.OrgInfo.ContactURL,
|
||||
"--quitoninfo",
|
||||
)
|
||||
message, flags, err := m.getMessageAndFlags()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting mdm migrator message: %w", err)
|
||||
}
|
||||
|
||||
exitCodeCh, errCh := m.render(message.String(), flags...)
|
||||
|
|
@ -419,3 +409,71 @@ func (m *swiftDialogMDMMigrator) ShowInterval() error {
|
|||
func (m *swiftDialogMDMMigrator) SetProps(props MDMMigratorProps) {
|
||||
m.props = props
|
||||
}
|
||||
|
||||
func (m *swiftDialogMDMMigrator) getMessageAndFlags() (*bytes.Buffer, []string, error) {
|
||||
vers, err := m.getMacOSMajorVersion()
|
||||
if err != nil {
|
||||
// log error for debugging and continue with default template
|
||||
log.Error().Err(err).Msg("getting macOS major version failed: using default migration template")
|
||||
}
|
||||
|
||||
tmpl := mdmMigrationTemplate
|
||||
height := "669"
|
||||
if vers != 0 && vers < 14 {
|
||||
height = "440"
|
||||
tmpl = mdmMigrationTemplatePreSonoma
|
||||
}
|
||||
|
||||
var message bytes.Buffer
|
||||
if err := tmpl.Execute(
|
||||
&message,
|
||||
m.props,
|
||||
); err != nil {
|
||||
return nil, nil, fmt.Errorf("executing migrqation template: %w", err)
|
||||
}
|
||||
|
||||
flags := []string{
|
||||
// main button
|
||||
"--button1text", "Start",
|
||||
// secondary button
|
||||
"--button2text", "Later",
|
||||
"--height", height,
|
||||
}
|
||||
|
||||
if m.props.OrgInfo.ContactURL != "" {
|
||||
flags = append(flags,
|
||||
// info button
|
||||
"--infobuttontext", "Unsure? Contact IT",
|
||||
"--infobuttonaction", m.props.OrgInfo.ContactURL,
|
||||
"--quitoninfo",
|
||||
)
|
||||
}
|
||||
|
||||
return &message, flags, nil
|
||||
}
|
||||
|
||||
// TODO: make this a variable for testing
|
||||
func (m *swiftDialogMDMMigrator) getMacOSMajorVersion() (int, error) {
|
||||
cmd := exec.Command("sw_vers", "-productVersion")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("getting macOS version: %w", err)
|
||||
}
|
||||
parts := strings.SplitN(string(out), ".", 2)
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
// this should never happen
|
||||
return 0, errors.New("getting macOS version: sw_vers command returned no output")
|
||||
case 1:
|
||||
// unexpected, so log for debugging
|
||||
log.Debug().Msgf("parsing macOS version: expected 2 parts, got 1: %s", out)
|
||||
default:
|
||||
// ok
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing macOS major version: %w", err)
|
||||
}
|
||||
return major, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
|||
serverSettings, ok := gitops.OrgSettings["server_settings"]
|
||||
assert.True(t, ok, "server_settings not found")
|
||||
assert.Equal(t, "https://fleet.example.com", serverSettings.(map[string]interface{})["server_url"])
|
||||
assert.EqualValues(t, 2000, serverSettings.(map[string]interface{})["query_report_cap"])
|
||||
assert.Contains(t, gitops.OrgSettings, "org_info")
|
||||
orgInfo, ok := gitops.OrgSettings["org_info"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
|
|
|
|||
1
pkg/spec/testdata/global_config_no_paths.yml
vendored
1
pkg/spec/testdata/global_config_no_paths.yml
vendored
|
|
@ -101,6 +101,7 @@ org_settings:
|
|||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 2000
|
||||
query_reports_disabled: false
|
||||
scripts_disabled: false
|
||||
server_url: https://fleet.example.com
|
||||
|
|
|
|||
1
pkg/spec/testdata/org-settings.yml
vendored
1
pkg/spec/testdata/org-settings.yml
vendored
|
|
@ -4,6 +4,7 @@ server_settings:
|
|||
deferred_save_host: false
|
||||
enable_analytics: true
|
||||
live_query_disabled: false
|
||||
query_report_cap: 2000
|
||||
query_reports_disabled: false
|
||||
scripts_disabled: false
|
||||
server_url: https://fleet.example.com
|
||||
|
|
|
|||
23
schema/tables/patches.yml
Normal file
23
schema/tables/patches.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: patches
|
||||
description: |- # (required) string - The description for this table. Note: this field supports Markdown
|
||||
The `patches` osquery table lists Windows security patch updates.
|
||||
examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
|
||||
Basic query:
|
||||
|
||||
```
|
||||
SELECT * FROM patches;
|
||||
```
|
||||
|
||||
This query determines if a specific hotfix patch is installed:
|
||||
|
||||
```
|
||||
SELECT * FROM patches WHERE hotfix_id='kb5037663';
|
||||
```
|
||||
notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
|
||||
Microsoft creates a support page per hotfix patch. Support pages can be discovered by doing a web browser search for the hotfix ID string (e.g., KB5037663).
|
||||
|
||||
Microsoft documentation for [KB5037663](https://support.microsoft.com/en-us/topic/may-29-2024-kb5037853-os-builds-22621-3672-and-22631-3672-preview-dcf14fd8-84d6-4234-9d5b-784c319cd7cf)
|
||||
|
||||
The `patches` table does not include updates that are applied via Windows Installer / Microsoft Standard Installer packages (.msi) or updates downloaded directly from Windows Update (e.g., Service Packs).
|
||||
|
||||
[Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)
|
||||
|
|
@ -24,6 +24,7 @@ notes: |- # (optional) string - Notes about this table. Note: This field support
|
|||
|
||||
- [Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)
|
||||
- [Chocolatey](https://chocolatey.org/)
|
||||
- The Fleet `chocolatey_packages`[table](https://fleetdm.com/tables/chocolatey_packages)
|
||||
- [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)
|
||||
- [winget.run](https://winget.run/)
|
||||
- Windows [cmd](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,25 @@
|
|||
name: safari_extensions
|
||||
description: Installed Safari browser extensions (plugins).
|
||||
description: Safari extensions add functionality to Safari.app, the native web browser in macOS. The `safari_extensions` table collects all Safari extensions installed on a Mac.
|
||||
columns:
|
||||
- name: uid
|
||||
examples: |-
|
||||
Collect Safari extensions for all Mac users:
|
||||
|
||||
```
|
||||
SELECT * FROM users CROSS JOIN safari_extensions USING (uid);
|
||||
```
|
||||
notes: |-
|
||||
- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
|
||||
- Includes installed extensions for all system users.
|
||||
Because Safari data is intentionally isolated for each macOS user to maintain privacy, this query requires a `JOIN` operation.
|
||||
|
||||
Query explanation:
|
||||
|
||||
- The `safari_extensions` table has a row for each installed extension
|
||||
- Each row has a column with the `uid` of the user who installed the extension
|
||||
- Each `uid` from the `safari_extensions` table is matched in the `users` table to collect Safari extensions in the output data for all user accounts on the Mac by the `JOIN`
|
||||
|
||||
Links:
|
||||
|
||||
- Apple dcoumentaion on Safari Extensions: https://support.apple.com/en-us/102343
|
||||
- CROSS JOIN SQLite tutorial: https://www.sqlitetutorial.net/sqlite-cross-join/
|
||||
- [Fleet documentation on joining against the `users` table](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
|
||||
- Fleet users table: https://fleetdm.com/tables/users
|
||||
|
|
|
|||
13
schema/tables/scheduled_tasks.yml
Normal file
13
schema/tables/scheduled_tasks.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: scheduled_tasks
|
||||
description: |- # (required) string - The description for this table. Note: this field supports Markdown
|
||||
The Windows Task Scheduler tracks and performs automated tasks on a Windows device. The `scheduled_tasks` table collects the data from the Windows Task Scheduler.
|
||||
examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
|
||||
This query collects all tasks that are enabled but have not run:
|
||||
|
||||
```
|
||||
SELECT * FROM scheduled_tasks WHERE enabled='1' AND last_run_message='The task has not yet run.';
|
||||
```
|
||||
notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
|
||||
Many automated tasks are added to the Task Scheduler by Windows itself, however, administrators can also customize the Task Scheduler. Scheduled tasks are analogous to Launch Daemons and Launch Agents used on Linux or macOS. Because automation is a potential vector for malicious activity, monitoring the Windows Task Scheduler may be critical in an enterprise environment.
|
||||
|
||||
[Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler)
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
name: software_update
|
||||
description: The `software_update` table displays the number of updates available from Apple's Software Update service on a Mac.
|
||||
platforms:
|
||||
- darwin
|
||||
description: Information about available Apple software updates.
|
||||
examples: |-
|
||||
Basic query:
|
||||
|
||||
```
|
||||
SELECT * FROM software_update;
|
||||
```
|
||||
columns:
|
||||
- name: software_update_required
|
||||
type: integer
|
||||
required: false
|
||||
description: |-
|
||||
If true, means one of the Apple softwares installed on this machine has a new available upgrade.
|
||||
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
|
||||
A value of 0 means no updates are available. Any other integer represents the number of updates available.
|
||||
notes: |-
|
||||
This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
|
||||
|
||||
Available updates on a Mac can be displayed in the macOS Graphical User Interface (GUI) by clicking on the Apple menu and then selecting “System Settings”. In the System Settings.app, click General > Software Update.
|
||||
|
||||
Apple Software Updates can also be listed in Terminal with the following command:
|
||||
|
||||
```
|
||||
softwareupdate --list --verbose
|
||||
```
|
||||
|
||||
[Update Your Apple Software](https://support.apple.com/guide/personal-safety/update-your-apple-software-ips4930e3486/web)
|
||||
evented: false
|
||||
|
|
|
|||
|
|
@ -4603,14 +4603,14 @@ func testLockUnlockWipeMacOS(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||
|
||||
// request an unlock, to make it pending unlock
|
||||
// request an unlock. This is a NOOP for Apple MDM.
|
||||
err = ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// it is now locked pending unlock
|
||||
// it is still locked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, host)
|
||||
require.NoError(t, err)
|
||||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
checkLockWipeState(t, status, false, true, false, false, false, false)
|
||||
|
||||
// execute CleanMacOSMDMLock to simulate successful unlock
|
||||
err = ds.CleanMacOSMDMLock(ctx, host.UUID)
|
||||
|
|
|
|||
|
|
@ -4255,7 +4255,7 @@ func testHostsIncludesScheduledQueriesInPackStats(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage(json.RawMessage(`{"foo": "baz"}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
hostResult, err = ds.Host(context.Background(), host.ID)
|
||||
|
|
@ -9067,7 +9067,7 @@ func testHostsAddToTeamCleansUpTeamQueryResults(t *testing.T, ds *Datastore) {
|
|||
h4Global0Results,
|
||||
h4Query1Results,
|
||||
} {
|
||||
err = ds.OverwriteQueryResultRows(ctx, results)
|
||||
err = ds.OverwriteQueryResultRows(ctx, results, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6122,7 +6122,7 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
|
|||
cert := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "FleetDM Identity",
|
||||
CommonName: "Fleet Identity",
|
||||
},
|
||||
NotAfter: notAfter,
|
||||
// use a random value, just to make sure they're
|
||||
|
|
|
|||
|
|
@ -359,7 +359,8 @@ ON DUPLICATE KEY UPDATE
|
|||
// if we received a Wipe command result, update the host's status
|
||||
if wipeCmdUUID != "" {
|
||||
if err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrollment.HostUUID,
|
||||
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2")); err != nil {
|
||||
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2"), false,
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
// OverwriteQueryResultRows overwrites the query result rows for a given query and host
|
||||
// in a single transaction, ensuring that the number of rows for the given query
|
||||
// does not exceed the maximum allowed
|
||||
func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) (err error) {
|
||||
func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) (err error) {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
|
|||
return ctxerr.Wrap(ctx, err, "counting existing query results")
|
||||
}
|
||||
|
||||
if countExisting >= fleet.MaxQueryReportRows {
|
||||
if countExisting >= maxQueryReportRows {
|
||||
// do not delete any rows if we are already at the limit
|
||||
return nil
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
|
|||
|
||||
// Calculate how many new rows can be added given the maximum limit
|
||||
netRowsAfterDeletion := countExisting - int(countDeleted)
|
||||
allowedNewRows := fleet.MaxQueryReportRows - netRowsAfterDeletion
|
||||
allowedNewRows := maxQueryReportRows - netRowsAfterDeletion
|
||||
if allowedNewRows == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), query1Rows)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), query1Rows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert Result Row for different Scheduled Query
|
||||
|
|
@ -76,7 +76,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
}
|
||||
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), query2Rows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), query2Rows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := ds.QueryResultRows(context.Background(), query.ID, fleet.TeamFilter{User: test.UserAdmin})
|
||||
|
|
@ -125,7 +125,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert 1 Result Row for Query1 Host2
|
||||
|
|
@ -137,7 +137,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that Query1 returns 2 results for Host1
|
||||
|
|
@ -215,7 +215,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
}
|
||||
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), globalRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), globalRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
teamRow := []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -229,7 +229,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), teamRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), teamRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
observerTeamRow := []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -243,7 +243,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
filter := fleet.TeamFilter{
|
||||
|
|
@ -286,7 +286,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert Nil Result Row for Query1, nil data rows are not counted
|
||||
|
|
@ -298,7 +298,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
|
|||
Data: nil,
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Insert 5 Result Rows for Query2
|
||||
|
|
@ -317,7 +317,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
|
|||
resultRows = append(resultRows, resultRow2)
|
||||
}
|
||||
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), resultRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), resultRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that ResultCountForQuery returns 1
|
||||
|
|
@ -366,7 +366,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
host1Query2 := []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -380,7 +380,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host1Query2)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host1Query2, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
host2ResultRow := []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -394,7 +394,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
|
|||
}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
host3ResultRow := []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -405,7 +405,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
|
|||
Data: nil,
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that Query1 returns 2
|
||||
|
|
@ -451,7 +451,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
}
|
||||
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), initialRow)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), initialRow, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite Result Rows with new data
|
||||
|
|
@ -465,7 +465,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
}
|
||||
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that we get the overwritten data (1 result with USB Mouse data)
|
||||
|
|
@ -486,7 +486,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that the data has not changed
|
||||
|
|
@ -511,7 +511,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
mockTime := time.Now().UTC().Truncate(time.Second)
|
||||
|
||||
// Generate max rows -1
|
||||
maxRows := fleet.MaxQueryReportRows - 1
|
||||
maxRows := fleet.DefaultMaxQueryReportRows - 1
|
||||
maxMinusOneRows := make([]*fleet.ScheduledQueryResultRow, maxRows)
|
||||
for i := 0; i < maxRows; i++ {
|
||||
maxMinusOneRows[i] = &fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -521,7 +521,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
}
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add an empty data rows which do not count towards the max
|
||||
|
|
@ -532,7 +532,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
LastFetched: mockTime,
|
||||
Data: nil,
|
||||
},
|
||||
})
|
||||
}, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm that we can still add a row
|
||||
|
|
@ -543,13 +543,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
LastFetched: mockTime,
|
||||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
})
|
||||
}, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that we now have max rows
|
||||
count, err := ds.ResultCountForQuery(context.Background(), query.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.MaxQueryReportRows, count)
|
||||
require.Equal(t, fleet.DefaultMaxQueryReportRows, count)
|
||||
|
||||
// Attempt to add another row
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), []*fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -559,7 +559,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
LastFetched: mockTime,
|
||||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
})
|
||||
}, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that the last row was not added
|
||||
|
|
@ -568,7 +568,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, host4result, 0)
|
||||
|
||||
// Generate more than max rows in Query 2
|
||||
rows := fleet.MaxQueryReportRows + 50
|
||||
rows := fleet.DefaultMaxQueryReportRows + 50
|
||||
largeBatchRows := make([]*fleet.ScheduledQueryResultRow, rows)
|
||||
for i := 0; i < rows; i++ {
|
||||
largeBatchRows[i] = &fleet.ScheduledQueryResultRow{
|
||||
|
|
@ -578,13 +578,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
}
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm only max rows are stored for the queryID
|
||||
allResults, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, allResults, fleet.MaxQueryReportRows)
|
||||
require.Len(t, allResults, fleet.DefaultMaxQueryReportRows)
|
||||
|
||||
// Confirm that new rows are not added when the max is reached
|
||||
newMockTime := mockTime.Add(2 * time.Minute)
|
||||
|
|
@ -597,7 +597,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
}
|
||||
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
host2Results, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host2.ID)
|
||||
|
|
@ -619,7 +619,7 @@ func testQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
|
||||
},
|
||||
}
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
|
||||
err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
filter := fleet.TeamFilter{User: user, IncludeObserver: true}
|
||||
|
|
@ -655,7 +655,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "Keyboard", "vendor": "Microsoft"}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), rows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), rows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call OverwriteQueryResultRows again with different rows
|
||||
|
|
@ -673,7 +673,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
|
|||
Data: ptr.RawMessage([]byte(`{"model": "Speakers", "vendor": "Bose"}`)),
|
||||
},
|
||||
}
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
|
||||
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cleanup query result rows
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func TestAppleMDMSCEPSerial(t *testing.T) {
|
|||
func TestAppleMDMPutAndHasCN(t *testing.T) {
|
||||
depot := setup(t)
|
||||
|
||||
name := "FleetDM Identity"
|
||||
name := "Fleet Identity"
|
||||
serial, err := depot.Serial()
|
||||
require.NoError(t, err)
|
||||
cert := x509.Certificate{
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
|
|||
UNIQUE KEY `id` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
|
||||
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `calendar_events` (
|
||||
|
|
|
|||
|
|
@ -1023,7 +1023,7 @@ func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, hostFl
|
|||
return ctxerr.Wrap(ctx, err, "record manual unlock host request")
|
||||
}
|
||||
|
||||
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string) string {
|
||||
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string, setUnlockRef bool) string {
|
||||
var alias string
|
||||
|
||||
stmt := `UPDATE host_mdm_actions `
|
||||
|
|
@ -1039,7 +1039,14 @@ func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart s
|
|||
// Note that this must not clear the unlock_pin, because recording the
|
||||
// lock request does generate the PIN and store it there to be used by an
|
||||
// eventual unlock.
|
||||
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
|
||||
if !setUnlockRef {
|
||||
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
|
||||
} else {
|
||||
// Currently only used for Apple MDM devices.
|
||||
// We set the unlock_ref to current time since the device can be unlocked any time after the lock.
|
||||
// Apple MDM does not have a concept of unlock pending.
|
||||
stmt += fmt.Sprintf("%sunlock_ref = '%s', %[1]swipe_ref = NULL", alias, time.Now().Format(time.DateTime))
|
||||
}
|
||||
case "unlock_ref":
|
||||
// a successful unlock clears itself as well as the lock ref, because
|
||||
// unlock is the default state so we don't need to keep its unlock_ref
|
||||
|
|
@ -1061,26 +1068,30 @@ func (ds *Datastore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Cont
|
|||
// a bit of MDM protocol leaking in the mysql layer, but it's either that or
|
||||
// the other way around (MDM protocol would translate to database column)
|
||||
var refCol string
|
||||
var setUnlockRef bool
|
||||
switch requestType {
|
||||
case "EraseDevice":
|
||||
refCol = "wipe_ref"
|
||||
case "DeviceLock":
|
||||
refCol = "lock_ref"
|
||||
setUnlockRef = true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded)
|
||||
return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded, setUnlockRef)
|
||||
}
|
||||
|
||||
func updateHostLockWipeStatusFromResultAndHostUUID(ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool) error {
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`)
|
||||
func updateHostLockWipeStatusFromResultAndHostUUID(
|
||||
ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool, setUnlockRef bool,
|
||||
) error {
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`, setUnlockRef)
|
||||
stmt += ` WHERE h.uuid = ? AND hma.` + refCol + ` = ?`
|
||||
_, err := tx.ExecContext(ctx, stmt, hostUUID, cmdUUID)
|
||||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result via host uuid")
|
||||
}
|
||||
|
||||
func updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, refCol string, succeeded bool) error {
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "")
|
||||
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "", false)
|
||||
stmt += ` WHERE host_id = ?`
|
||||
_, err := tx.ExecContext(ctx, stmt, hostID)
|
||||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -764,6 +765,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Equal(t, "windows", status.HostFleetPlatform)
|
||||
require.NotNil(t, status.LockScript)
|
||||
assert.Nil(t, status.UnlockScript)
|
||||
|
||||
s := status.LockScript
|
||||
require.Equal(t, script, s.ScriptContents)
|
||||
|
|
|
|||
|
|
@ -24,47 +24,47 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
lic, _ := license.FromContext(ctx)
|
||||
|
||||
computeStats := func(stats *fleet.StatisticsPayload, since time.Time) error {
|
||||
enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.writer(ctx))
|
||||
enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount enrolled hosts by os")
|
||||
}
|
||||
amountUsers, err := tableRowsCount(ctx, ds.writer(ctx), "users")
|
||||
amountUsers, err := tableRowsCount(ctx, ds.reader(ctx), "users")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount users")
|
||||
}
|
||||
amountSoftwaresVersions, err := tableRowsCount(ctx, ds.writer(ctx), "software")
|
||||
amountSoftwaresVersions, err := tableRowsCount(ctx, ds.reader(ctx), "software")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount software")
|
||||
}
|
||||
amountHostSoftwares, err := tableRowsCount(ctx, ds.writer(ctx), "host_software")
|
||||
amountHostSoftwares, err := tableRowsCount(ctx, ds.reader(ctx), "host_software")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount host_software")
|
||||
}
|
||||
amountSoftwareTitles, err := tableRowsCount(ctx, ds.writer(ctx), "software_titles")
|
||||
amountSoftwareTitles, err := tableRowsCount(ctx, ds.reader(ctx), "software_titles")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount software_titles")
|
||||
}
|
||||
amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.writer(ctx), "host_software_installed_paths")
|
||||
amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.reader(ctx), "host_software_installed_paths")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount host_software_installed_paths")
|
||||
}
|
||||
amountSoftwareCpes, err := tableRowsCount(ctx, ds.writer(ctx), "software_cpe")
|
||||
amountSoftwareCpes, err := tableRowsCount(ctx, ds.reader(ctx), "software_cpe")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount software_cpe")
|
||||
}
|
||||
amountSoftwareCves, err := tableRowsCount(ctx, ds.writer(ctx), "software_cve")
|
||||
amountSoftwareCves, err := tableRowsCount(ctx, ds.reader(ctx), "software_cve")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount software_cve")
|
||||
}
|
||||
amountTeams, err := amountTeamsDB(ctx, ds.writer(ctx))
|
||||
amountTeams, err := amountTeamsDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount teams")
|
||||
}
|
||||
amountPolicies, err := amountPoliciesDB(ctx, ds.writer(ctx))
|
||||
amountPolicies, err := amountPoliciesDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount policies")
|
||||
}
|
||||
amountLabels, err := amountLabelsDB(ctx, ds.writer(ctx))
|
||||
amountLabels, err := amountLabelsDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount labels")
|
||||
}
|
||||
|
|
@ -72,11 +72,11 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "statistics app config")
|
||||
}
|
||||
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.writer(ctx), since)
|
||||
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.reader(ctx), since)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount active users")
|
||||
}
|
||||
amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.writer(ctx))
|
||||
amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.reader(ctx))
|
||||
if err == sql.ErrNoRows {
|
||||
level.Debug(ds.logger).Log("msg", "amount policy violation days", "err", err) //nolint:errcheck
|
||||
} else if err != nil {
|
||||
|
|
@ -86,15 +86,15 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "statistics error store")
|
||||
}
|
||||
amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.writer(ctx), ds.logger, config)
|
||||
amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.reader(ctx), ds.logger, config)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount hosts not responding")
|
||||
}
|
||||
amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.writer(ctx))
|
||||
amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount hosts by orbit version")
|
||||
}
|
||||
amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.writer(ctx))
|
||||
amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount hosts by osquery version")
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
}
|
||||
|
||||
dest := statistics{}
|
||||
err := sqlx.GetContext(ctx, ds.writer(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
|
||||
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
anonIdentifier, err := server.GenerateRandomText(64)
|
||||
|
|
|
|||
|
|
@ -888,6 +888,16 @@ type ServerSettings struct {
|
|||
QueryReportsDisabled bool `json:"query_reports_disabled"`
|
||||
ScriptsDisabled bool `json:"scripts_disabled"`
|
||||
AIFeaturesDisabled bool `json:"ai_features_disabled"`
|
||||
QueryReportCap int `json:"query_report_cap"`
|
||||
}
|
||||
|
||||
const DefaultMaxQueryReportRows int = 1000
|
||||
|
||||
func (f *ServerSettings) GetQueryReportCap() int {
|
||||
if f.QueryReportCap <= 0 {
|
||||
return DefaultMaxQueryReportRows
|
||||
}
|
||||
return f.QueryReportCap
|
||||
}
|
||||
|
||||
// HostExpirySettings contains settings pertaining to automatic host expiry.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
type MDMAppleCommandIssuer interface {
|
||||
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
|
||||
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
|
||||
DeviceLock(ctx context.Context, host *Host, uuid string) error
|
||||
DeviceLock(ctx context.Context, host *Host, uuid string) (unlockPIN string, err error)
|
||||
EraseDevice(ctx context.Context, host *Host, uuid string) error
|
||||
InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ type Datastore interface {
|
|||
QueryResultRowsForHost(ctx context.Context, queryID, hostID uint) ([]*ScheduledQueryResultRow, error)
|
||||
ResultCountForQuery(ctx context.Context, queryID uint) (int, error)
|
||||
ResultCountForQueryAndHost(ctx context.Context, queryID, hostID uint) (int, error)
|
||||
OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow) error
|
||||
OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow, maxQueryReportRows int) error
|
||||
// CleanupDiscardedQueryResults deletes all query results for queries with DiscardData enabled.
|
||||
// Used in cleanups_then_aggregation cron to cleanup rows that were inserted immediately
|
||||
// after DiscardData was set to true due to query caching.
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ type Stats struct {
|
|||
|
||||
const (
|
||||
// StatusOK is the success code returned by osquery
|
||||
StatusOK OsqueryStatus = 0
|
||||
MaxQueryReportRows int = 1000
|
||||
StatusOK OsqueryStatus = 0
|
||||
)
|
||||
|
||||
// QueryContent is the format of a query stanza in an osquery configuration.
|
||||
|
|
|
|||
|
|
@ -414,8 +414,8 @@ func (s *HostLockWipeStatus) IsPendingLock() bool {
|
|||
|
||||
func (s HostLockWipeStatus) IsPendingUnlock() bool {
|
||||
if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
|
||||
// pending unlock if an unlock was requested
|
||||
return !s.UnlockRequestedAt.IsZero()
|
||||
// Apple MDM does not have a concept of pending unlock.
|
||||
return false
|
||||
}
|
||||
// pending unlock if script execution request is queued but no result yet
|
||||
return s.UnlockScript != nil && s.UnlockScript.ExitCode == nil
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ type Service interface {
|
|||
CreateUserFromInvite(ctx context.Context, p UserPayload) (user *User, err error)
|
||||
|
||||
// CreateUser allows an admin to create a new user without first creating and validating invite tokens.
|
||||
CreateUser(ctx context.Context, p UserPayload) (user *User, err error)
|
||||
// The sessionKey is only returned (not-nil) when creating API-only (non-SSO) users.
|
||||
CreateUser(ctx context.Context, p UserPayload) (user *User, sessionKey *string, err error)
|
||||
|
||||
// CreateInitialUser creates the first user, skipping authorization checks. If a user already exists this method
|
||||
// should fail.
|
||||
|
|
@ -274,12 +275,13 @@ type Service interface {
|
|||
// included in the results.
|
||||
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error)
|
||||
GetQuery(ctx context.Context, id uint) (*Query, error)
|
||||
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to
|
||||
GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error)
|
||||
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to.
|
||||
// Returns a boolean indicating whether the report is clipped.
|
||||
GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, bool, error)
|
||||
// GetHostQueryReportResults returns all stored results of a query for a specific host
|
||||
GetHostQueryReportResults(ctx context.Context, hid uint, queryID uint) (rows []HostQueryReportResult, lastFetched *time.Time, err error)
|
||||
// QueryReportIsClipped returns true if the number of query report rows exceeds the maximum
|
||||
QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error)
|
||||
QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error)
|
||||
NewQuery(ctx context.Context, p QueryPayload) (*Query, error)
|
||||
ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error)
|
||||
DeleteQuery(ctx context.Context, teamID *uint, name string) error
|
||||
|
|
@ -1040,7 +1042,7 @@ type Service interface {
|
|||
BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeTmName *string, payloads []ScriptPayload, dryRun bool) error
|
||||
|
||||
// Script-based methods (at least for some platforms, MDM-based for others)
|
||||
LockHost(ctx context.Context, hostID uint) error
|
||||
LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
|
||||
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
|
||||
WipeHost(ctx context.Context, hostID uint) error
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ type DEPService struct {
|
|||
// getDefaultProfile returns a godep.Profile with default values set.
|
||||
func (d *DEPService) getDefaultProfile() *godep.Profile {
|
||||
return &godep.Profile{
|
||||
ProfileName: "FleetDM default enrollment profile",
|
||||
ProfileName: "Fleet default enrollment profile",
|
||||
AllowPairing: true,
|
||||
AutoAdvanceSetup: false,
|
||||
IsSupervised: false,
|
||||
|
|
@ -688,8 +688,8 @@ var enrollmentProfileMobileconfigTemplate = template.Must(template.New("").Parse
|
|||
<string>{{ .SCEPURL }}</string>
|
||||
<key>Subject</key>
|
||||
<array>
|
||||
<array><array><string>O</string><string>FleetDM</string></array></array>
|
||||
<array><array><string>CN</string><string>FleetDM Identity</string></array></array>
|
||||
<array><array><string>O</string><string>Fleet</string></array></array>
|
||||
<array><array><string>CN</string><string>Fleet Identity</string></array></array>
|
||||
</array>
|
||||
</dict>
|
||||
<key>PayloadIdentifier</key>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
const (
|
||||
defaultFleetDMAPIURL = "https://fleetdm.com"
|
||||
getSignedAPNSCSRPath = "/api/v1/deliver-apple-csr"
|
||||
depCertificateCommonName = "FleetDM"
|
||||
depCertificateCommonName = "Fleet"
|
||||
depCertificateExpiryDays = 30
|
||||
)
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ func NewSCEPCACertKey() (*x509.Certificate, *rsa.PrivateKey, error) {
|
|||
|
||||
caCert := depot.NewCACert(
|
||||
depot.WithYears(10),
|
||||
depot.WithCommonName("FleetDM"),
|
||||
depot.WithCommonName("Fleet"),
|
||||
)
|
||||
|
||||
crtBytes, err := caCert.SelfSign(rand.Reader, key.Public(), key)
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []str
|
|||
return ctxerr.Wrap(ctx, err, "commander remove profile")
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) error {
|
||||
pin := GenerateRandomPin(6)
|
||||
func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) (unlockPIN string, err error) {
|
||||
unlockPIN = GenerateRandomPin(6)
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
|
|
@ -106,22 +106,23 @@ func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host,
|
|||
<string>%s</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>`, uuid, pin)
|
||||
</plist>`, uuid, unlockPIN,
|
||||
)
|
||||
|
||||
cmd, err := mdm.DecodeCommand([]byte(raw))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "decoding command")
|
||||
return "", ctxerr.Wrap(ctx, err, "decoding command")
|
||||
}
|
||||
|
||||
if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, pin); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
|
||||
if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, unlockPIN); err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
|
||||
}
|
||||
|
||||
if err := svc.sendNotifications(ctx, []string{host.UUID}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
|
||||
return "", ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
|
||||
}
|
||||
|
||||
return nil
|
||||
return unlockPIN, nil
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error {
|
||||
|
|
|
|||
|
|
@ -132,8 +132,9 @@ func TestMDMAppleCommander(t *testing.T) {
|
|||
require.Len(t, pin, 6)
|
||||
return nil
|
||||
}
|
||||
err = cmdr.DeviceLock(ctx, host, cmdUUID)
|
||||
pin, err := cmdr.DeviceLock(ctx, host, cmdUUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pin, 6)
|
||||
require.True(t, mdmStorage.EnqueueDeviceLockCommandFuncInvoked)
|
||||
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ type ResultCountForQueryFunc func(ctx context.Context, queryID uint) (int, error
|
|||
|
||||
type ResultCountForQueryAndHostFunc func(ctx context.Context, queryID uint, hostID uint) (int, error)
|
||||
|
||||
type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error
|
||||
type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error
|
||||
|
||||
type CleanupDiscardedQueryResultsFunc func(ctx context.Context) error
|
||||
|
||||
|
|
@ -3508,11 +3508,11 @@ func (s *DataStore) ResultCountForQueryAndHost(ctx context.Context, queryID uint
|
|||
return s.ResultCountForQueryAndHostFunc(ctx, queryID, hostID)
|
||||
}
|
||||
|
||||
func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
s.mu.Lock()
|
||||
s.OverwriteQueryResultRowsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.OverwriteQueryResultRowsFunc(ctx, rows)
|
||||
return s.OverwriteQueryResultRowsFunc(ctx, rows, maxQueryReportRows)
|
||||
}
|
||||
|
||||
func (s *DataStore) CleanupDiscardedQueryResults(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,16 @@ import (
|
|||
)
|
||||
|
||||
// CreateUser creates a new user, skipping the invitation process.
|
||||
func (c *Client) CreateUser(p fleet.UserPayload) error {
|
||||
//
|
||||
// The session key (aka API token) is returned only when creating
|
||||
// API only users.
|
||||
func (c *Client) CreateUser(p fleet.UserPayload) (*string, error) {
|
||||
verb, path := "POST", "/api/latest/fleet/users/admin"
|
||||
var responseBody createUserResponse
|
||||
|
||||
return c.authenticatedRequest(p, verb, path, &responseBody)
|
||||
if err := c.authenticatedRequest(p, verb, path, &responseBody); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return responseBody.Token, nil
|
||||
}
|
||||
|
||||
// ListUsers retrieves the list of users.
|
||||
|
|
|
|||
|
|
@ -1231,7 +1231,12 @@ func getHostQueryReportEndpoint(ctx context.Context, request interface{}, svc fl
|
|||
return getHostQueryReportResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID)
|
||||
appConfig, err := svc.AppConfigObfuscated(ctx)
|
||||
if err != nil {
|
||||
return getHostQueryReportResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID, appConfig.ServerSettings.GetQueryReportCap())
|
||||
if err != nil {
|
||||
return getHostQueryReportResponse{Err: err}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1648,9 +1648,9 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
|
|||
}
|
||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||
|
||||
err := svc.LockHost(ctx, globalHostID)
|
||||
_, err := svc.LockHost(ctx, globalHostID)
|
||||
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
|
||||
err = svc.LockHost(ctx, teamHostID)
|
||||
_, err = svc.LockHost(ctx, teamHostID)
|
||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||
|
||||
// Pretend we locked the host
|
||||
|
|
|
|||
|
|
@ -281,6 +281,44 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
|
|||
require.True(t, found)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestCreatingAPIOnlyUserReturnsAPIToken() {
|
||||
t := s.T()
|
||||
|
||||
defer func() {
|
||||
s.token = s.getTestAdminToken()
|
||||
}()
|
||||
|
||||
var createResp createUserResponse
|
||||
params := fleet.UserPayload{
|
||||
Name: ptr.String("someadmin"),
|
||||
Email: ptr.String("someadmin@example.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
APIOnly: ptr.Bool(false),
|
||||
}
|
||||
s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
|
||||
assert.NotZero(t, createResp.User.ID)
|
||||
assert.Nil(t, createResp.Token)
|
||||
|
||||
params = fleet.UserPayload{
|
||||
Name: ptr.String("apionly"),
|
||||
Email: ptr.String("apionly@example.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
APIOnly: ptr.Bool(true),
|
||||
// AdminForcedPasswordReset is set to false when creating api-only users via `fleetctl user create --api-only`.
|
||||
AdminForcedPasswordReset: ptr.Bool(false),
|
||||
}
|
||||
s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
|
||||
assert.NotZero(t, createResp.User.ID)
|
||||
assert.NotNil(t, createResp.Token)
|
||||
|
||||
s.token = *createResp.Token
|
||||
var chr countHostsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts/count", countHostsRequest{}, http.StatusOK, &chr)
|
||||
assert.Equal(t, 0, chr.Count)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestActivityUserEmailPersistsAfterDeletion() {
|
||||
t := s.T()
|
||||
|
||||
|
|
@ -10799,12 +10837,14 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
}, http.StatusOK, &applyResp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 0)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// Re-add results to our query and check that they're actually there
|
||||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 1)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// don't change platform or min_osquery_version and results should not be deleted
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
|
||||
|
|
@ -10812,6 +10852,7 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
}, http.StatusOK, &applyResp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 1)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// now update the platform and results should be deleted.
|
||||
osqueryInfoQuerySpec.Platform = "darwin"
|
||||
|
|
@ -10820,30 +10861,35 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
}, http.StatusOK, &applyResp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 0)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// Update logging type, which should cause results deletion
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp)
|
||||
require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 0)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// Re-add results to our query and check that they're actually there
|
||||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 1)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
discardData := true
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{DiscardData: &discardData}}, http.StatusOK, &modifyQueryResp)
|
||||
require.True(t, modifyQueryResp.Query.DiscardData)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 0)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// check that now that discardData is set, we don't add new results
|
||||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, 0)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
// Verify that we can't have more than 1k results
|
||||
|
||||
|
|
@ -10855,7 +10901,7 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
NodeKey: *host1Global.NodeKey,
|
||||
LogType: "result",
|
||||
Data: json.RawMessage(`[{
|
||||
"snapshot": [` + results(1000, host1Global.UUID) + `
|
||||
"snapshot": [` + results(fleet.DefaultMaxQueryReportRows, host1Global.UUID) + `
|
||||
],
|
||||
"action": "snapshot",
|
||||
"name": "pack/Global/` + osqueryInfoQuery.Name + `",
|
||||
|
|
@ -10878,13 +10924,14 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
|
||||
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
|
||||
require.True(t, gqrr.ReportClipped)
|
||||
|
||||
ghqrr = getHostQueryReportResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/queries/%d", host1Global.ID, osqueryInfoQuery.ID), getHostQueryReportRequest{}, http.StatusOK, &ghqrr)
|
||||
require.NoError(t, ghqrr.Err)
|
||||
require.Len(t, ghqrr.Results, fleet.DefaultMaxQueryReportRows)
|
||||
require.True(t, ghqrr.ReportClipped)
|
||||
require.Len(t, ghqrr.Results, fleet.MaxQueryReportRows)
|
||||
|
||||
slreq.Data = json.RawMessage(`[{
|
||||
"snapshot": [` + results(1, host1Global.UUID) + `
|
||||
|
|
@ -10906,7 +10953,41 @@ func (s *integrationTestSuite) TestQueryReports() {
|
|||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
|
||||
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
|
||||
require.True(t, gqrr.ReportClipped)
|
||||
|
||||
appConfigSpec := map[string]map[string]int{
|
||||
"server_settings": {"query_report_cap": fleet.DefaultMaxQueryReportRows + 1},
|
||||
}
|
||||
s.Do("PATCH", "/api/latest/fleet/config", appConfigSpec, http.StatusOK)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
|
||||
require.False(t, gqrr.ReportClipped)
|
||||
|
||||
slreq.Data = json.RawMessage(`[{
|
||||
"snapshot": [` + results(1002, host1Global.UUID) + `
|
||||
],
|
||||
"action": "snapshot",
|
||||
"name": "pack/Global/` + osqueryInfoQuery.Name + `",
|
||||
"hostIdentifier": "` + *host1Global.OsqueryHostID + `",
|
||||
"calendarTime": "Fri Oct 6 18:13:04 2023 UTC",
|
||||
"unixTime": 1696615984,
|
||||
"epoch": 0,
|
||||
"counter": 0,
|
||||
"numerics": false,
|
||||
"decorations": {
|
||||
"host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd",
|
||||
"hostname": "` + host1Global.Hostname + `"
|
||||
}
|
||||
}]`)
|
||||
|
||||
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
|
||||
require.NoError(t, slres.Err)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
|
||||
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows+1)
|
||||
require.True(t, gqrr.ReportClipped)
|
||||
|
||||
// TODO: Set global discard flag and verify that all data is gone.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -73,12 +74,15 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() {
|
|||
{
|
||||
"locked host turns on MDM",
|
||||
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
||||
s.Do(
|
||||
var resp lockHostResponse
|
||||
s.DoJSON(
|
||||
"POST",
|
||||
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
|
||||
nil,
|
||||
http.StatusNoContent,
|
||||
http.StatusOK,
|
||||
&resp,
|
||||
)
|
||||
assert.Len(t, resp.UnlockPIN, 6)
|
||||
|
||||
cmd, err := device.Idle()
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -531,9 +531,9 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
|
|||
var mdmResp getAppleMDMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/apns", nil, http.StatusOK, &mdmResp)
|
||||
// returned values are dummy, this is a test certificate
|
||||
require.Equal(t, "FleetDM", mdmResp.Issuer)
|
||||
require.Equal(t, "Fleet", mdmResp.Issuer)
|
||||
require.NotZero(t, mdmResp.SerialNumber)
|
||||
require.Equal(t, "FleetDM", mdmResp.CommonName)
|
||||
require.Equal(t, "Fleet", mdmResp.CommonName)
|
||||
require.NotZero(t, mdmResp.RenewDate)
|
||||
|
||||
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -8038,7 +8038,9 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
|
|||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusConflict, &unlockResp)
|
||||
|
||||
// lock the host
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusNoContent)
|
||||
var lockResp lockHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusOK, &lockResp)
|
||||
assert.Len(t, lockResp.UnlockPIN, 6)
|
||||
|
||||
// refresh the host's status, it is now pending lock
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
|
||||
|
|
@ -8084,12 +8086,12 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
|
|||
unlockActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeUnlockedHost{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "host_platform": %q}`, host.ID, host.DisplayName(), host.FleetPlatform()), 0)
|
||||
|
||||
// refresh the host's status, it is locked pending unlock
|
||||
// refresh the host's status, it is still locked
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
|
||||
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "unlock", *getHostResp.Host.MDM.PendingAction)
|
||||
assert.Empty(t, *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// try unlocking the host again simply returns the PIN again
|
||||
unlockResp = unlockHostResponse{}
|
||||
|
|
@ -8834,7 +8836,7 @@ func (s *integrationMDMTestSuite) appleCoreCertsSetup() {
|
|||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "FleetDM",
|
||||
CommonName: "Fleet",
|
||||
ExtraNames: []pkix.AttributeTypeAndValue{
|
||||
{
|
||||
Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1},
|
||||
|
|
|
|||
|
|
@ -1799,7 +1799,8 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
|
|||
|
||||
unmarshaledResults, queriesDBData := svc.preProcessOsqueryResults(ctx, logs, queryReportsDisabled)
|
||||
if !queryReportsDisabled {
|
||||
svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData)
|
||||
maxQueryReportRows := appConfig.ServerSettings.GetQueryReportCap()
|
||||
svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData, maxQueryReportRows)
|
||||
}
|
||||
|
||||
var filteredLogs []json.RawMessage
|
||||
|
|
@ -1861,7 +1862,12 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
|
|||
// Query Reports
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshaledResults []*fleet.ScheduledQueryResult, queriesDBData map[string]*fleet.Query) {
|
||||
func (svc *Service) saveResultLogsToQueryReports(
|
||||
ctx context.Context,
|
||||
unmarshaledResults []*fleet.ScheduledQueryResult,
|
||||
queriesDBData map[string]*fleet.Query,
|
||||
maxQueryReportRows int,
|
||||
) {
|
||||
// skipauth: Authorization is currently for user endpoints only.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
|
|
@ -1903,11 +1909,11 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
|
|||
level.Error(svc.logger).Log("msg", "get result count for query", "err", err, "query_id", dbQuery.ID)
|
||||
continue
|
||||
}
|
||||
if count >= fleet.MaxQueryReportRows {
|
||||
if count >= maxQueryReportRows {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID); err != nil {
|
||||
if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID, maxQueryReportRows); err != nil {
|
||||
level.Error(svc.logger).Log("msg", "overwrite results", "err", err, "query_id", dbQuery.ID, "host_id", host.ID)
|
||||
continue
|
||||
}
|
||||
|
|
@ -1919,7 +1925,7 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
|
|||
// The "snapshot" array in a ScheduledQueryResult can contain multiple rows.
|
||||
// Each row is saved as a separate ScheduledQueryResultRow, i.e. a result could contain
|
||||
// many USB Devices or a result could contain all user accounts on a host.
|
||||
func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint) error {
|
||||
func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint, maxQueryReportRows int) error {
|
||||
fetchTime := time.Now()
|
||||
|
||||
rows := make([]*fleet.ScheduledQueryResultRow, 0, len(result.Snapshot))
|
||||
|
|
@ -1945,7 +1951,7 @@ func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.Sched
|
|||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if err := svc.ds.OverwriteQueryResultRows(ctx, rows); err != nil {
|
||||
if err := svc.ds.OverwriteQueryResultRows(ctx, rows, maxQueryReportRows); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "overwriting query result rows")
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -614,7 +614,7 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
|
|||
return 0, nil
|
||||
}
|
||||
teamQueryResultsStored := false
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -766,7 +766,7 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
|
|||
Logging: fleet.LoggingSnapshot,
|
||||
},
|
||||
}
|
||||
serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse)
|
||||
serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse, fleet.DefaultMaxQueryReportRows)
|
||||
assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
||||
|
||||
// Happy Path: Results saved
|
||||
|
|
@ -777,13 +777,13 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
|
|||
Logging: fleet.LoggingSnapshot,
|
||||
},
|
||||
}
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
return nil
|
||||
}
|
||||
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue)
|
||||
serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue, fleet.DefaultMaxQueryReportRows)
|
||||
require.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
|
||||
}
|
||||
|
||||
|
|
@ -825,7 +825,7 @@ func TestSubmitResultLogsToQueryResultsWithEmptySnapShot(t *testing.T) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
require.Len(t, rows, 1)
|
||||
require.Equal(t, uint(999), rows[0].HostID)
|
||||
require.NotZero(t, rows[0].LastFetched)
|
||||
|
|
@ -876,7 +876,7 @@ func TestSubmitResultLogsToQueryResultsDoesNotCountNullDataRows(t *testing.T) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
require.Len(t, rows, 1)
|
||||
require.Equal(t, uint(999), rows[0].HostID)
|
||||
require.NotZero(t, rows[0].LastFetched)
|
||||
|
|
@ -933,7 +933,7 @@ func TestSubmitResultLogsFail(t *testing.T) {
|
|||
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
|
||||
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,16 +121,17 @@ type getQueryReportRequest struct {
|
|||
}
|
||||
|
||||
type getQueryReportResponse struct {
|
||||
QueryID uint `json:"query_id"`
|
||||
Results []fleet.HostQueryResultRow `json:"results"`
|
||||
Err error `json:"error,omitempty"`
|
||||
QueryID uint `json:"query_id"`
|
||||
Results []fleet.HostQueryResultRow `json:"results"`
|
||||
ReportClipped bool `json:"report_clipped"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getQueryReportResponse) error() error { return r.Err }
|
||||
|
||||
func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*getQueryReportRequest)
|
||||
queryReportResults, err := svc.GetQueryReportResults(ctx, req.ID)
|
||||
queryReportResults, reportClipped, err := svc.GetQueryReportResults(ctx, req.ID)
|
||||
if err != nil {
|
||||
return listQueriesResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -140,44 +141,53 @@ func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.
|
|||
results = queryReportResults
|
||||
}
|
||||
return getQueryReportResponse{
|
||||
QueryID: req.ID,
|
||||
Results: results,
|
||||
QueryID: req.ID,
|
||||
Results: results,
|
||||
ReportClipped: reportClipped,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, error) {
|
||||
func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, bool, error) {
|
||||
// Load query first to get its teamID.
|
||||
query, err := svc.ds.Query(ctx, id)
|
||||
if err != nil {
|
||||
setAuthCheckedOnPreAuthErr(ctx)
|
||||
return nil, ctxerr.Wrap(ctx, err, "get query from datastore")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "get query from datastore")
|
||||
}
|
||||
if err := svc.authz.Authorize(ctx, query, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if query.DiscardData {
|
||||
return nil, nil
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fleet.ErrNoContext
|
||||
return nil, false, fleet.ErrNoContext
|
||||
}
|
||||
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
|
||||
|
||||
queryReportResultRows, err := svc.ds.QueryResultRows(ctx, id, filter)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get query report results")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "get query report results")
|
||||
}
|
||||
queryReportResults, err := fleet.MapQueryReportResultsToRows(queryReportResultRows)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "map db rows to results")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "map db rows to results")
|
||||
}
|
||||
return queryReportResults, nil
|
||||
appConfig, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
reportClipped, err := svc.QueryReportIsClipped(ctx, id, appConfig.ServerSettings.GetQueryReportCap())
|
||||
if err != nil {
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "check query report is clipped")
|
||||
}
|
||||
return queryReportResults, reportClipped, nil
|
||||
}
|
||||
|
||||
func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error) {
|
||||
func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error) {
|
||||
query, err := svc.ds.Query(ctx, queryID)
|
||||
if err != nil {
|
||||
setAuthCheckedOnPreAuthErr(ctx)
|
||||
|
|
@ -191,7 +201,7 @@ func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (boo
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count >= fleet.MaxQueryReportRows, nil
|
||||
return count >= maxQueryReportRows, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -644,7 +644,7 @@ func TestQueryAuth(t *testing.T) {
|
|||
_, err = svc.GetQuery(ctx, tt.qid)
|
||||
checkAuthErr(t, tt.shouldFailRead, err)
|
||||
|
||||
_, err = svc.QueryReportIsClipped(ctx, tt.qid)
|
||||
_, err = svc.QueryReportIsClipped(ctx, tt.qid, fleet.DefaultMaxQueryReportRows)
|
||||
checkAuthErr(t, tt.shouldFailRead, err)
|
||||
|
||||
_, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false)
|
||||
|
|
@ -688,15 +688,15 @@ func TestQueryReportIsClipped(t *testing.T) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1)
|
||||
isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
require.False(t, isClipped)
|
||||
|
||||
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
|
||||
return fleet.MaxQueryReportRows, nil
|
||||
return fleet.DefaultMaxQueryReportRows, nil
|
||||
}
|
||||
|
||||
isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1)
|
||||
isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
|
||||
require.NoError(t, err)
|
||||
require.True(t, isClipped)
|
||||
}
|
||||
|
|
@ -725,9 +725,10 @@ func TestQueryReportReturnsNilIfDiscardDataIsTrue(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
results, err := svc.GetQueryReportResults(viewerCtx, 1)
|
||||
results, reportClipped, err := svc.GetQueryReportResults(viewerCtx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, results)
|
||||
require.False(t, reportClipped)
|
||||
}
|
||||
|
||||
func TestComparePlatforms(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -915,26 +915,37 @@ type lockHostRequest struct {
|
|||
}
|
||||
|
||||
type lockHostResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
UnlockPIN string `json:"unlock_pin,omitempty"`
|
||||
StatusCode int `json:"-"`
|
||||
}
|
||||
|
||||
func (r lockHostResponse) Status() int { return http.StatusNoContent }
|
||||
func (r lockHostResponse) Status() int {
|
||||
if r.StatusCode != 0 {
|
||||
return r.StatusCode
|
||||
}
|
||||
return http.StatusNoContent
|
||||
}
|
||||
func (r lockHostResponse) error() error { return r.Err }
|
||||
|
||||
func lockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*lockHostRequest)
|
||||
if err := svc.LockHost(ctx, req.HostID); err != nil {
|
||||
unlockPIN, err := svc.LockHost(ctx, req.HostID)
|
||||
if err != nil {
|
||||
return lockHostResponse{Err: err}, nil
|
||||
}
|
||||
if unlockPIN != "" {
|
||||
return lockHostResponse{UnlockPIN: unlockPIN, StatusCode: http.StatusOK}, nil
|
||||
}
|
||||
return lockHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
||||
func (svc *Service) LockHost(ctx context.Context, hostID uint) (string, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return fleet.ErrMissingLicense
|
||||
return "", fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -34,38 +34,43 @@ type createUserRequest struct {
|
|||
|
||||
type createUserResponse struct {
|
||||
User *fleet.User `json:"user,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
// Token is only returned when creating API-only (non-SSO) users.
|
||||
Token *string `json:"token,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r createUserResponse) error() error { return r.Err }
|
||||
|
||||
func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*createUserRequest)
|
||||
user, err := svc.CreateUser(ctx, req.UserPayload)
|
||||
user, sessionKey, err := svc.CreateUser(ctx, req.UserPayload)
|
||||
if err != nil {
|
||||
return createUserResponse{Err: err}, nil
|
||||
}
|
||||
return createUserResponse{User: user}, nil
|
||||
return createUserResponse{
|
||||
User: user,
|
||||
Token: sessionKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
|
||||
func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, *string, error) {
|
||||
var teams []fleet.UserTeam
|
||||
if p.Teams != nil {
|
||||
teams = *p.Teams
|
||||
}
|
||||
if err := svc.authz.Authorize(ctx, &fleet.User{Teams: teams}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := p.VerifyAdminCreate(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "verify user payload")
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "verify user payload")
|
||||
}
|
||||
|
||||
if teams != nil {
|
||||
// Validate that the teams exist
|
||||
teamsSummary, err := svc.ds.TeamsSummary(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
|
||||
}
|
||||
teamIDs := map[uint]struct{}{}
|
||||
for _, team := range teamsSummary {
|
||||
|
|
@ -74,7 +79,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
|
|||
for _, userTeam := range teams {
|
||||
_, ok := teamIDs[userTeam.Team.ID]
|
||||
if !ok {
|
||||
return nil, ctxerr.Wrap(
|
||||
return nil, nil, ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError("teams.id", fmt.Sprintf("team with id %d does not exist", userTeam.Team.ID)),
|
||||
)
|
||||
}
|
||||
|
|
@ -82,7 +87,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
|
|||
}
|
||||
|
||||
if invite, err := svc.ds.InviteByEmail(ctx, *p.Email); err == nil && invite != nil {
|
||||
return nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
|
||||
return nil, nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
|
||||
}
|
||||
|
||||
if p.AdminForcedPasswordReset == nil {
|
||||
|
|
@ -90,7 +95,28 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
|
|||
p.AdminForcedPasswordReset = ptr.Bool(true)
|
||||
}
|
||||
|
||||
return svc.NewUser(ctx, p)
|
||||
user, err := svc.NewUser(ctx, p)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "create user")
|
||||
}
|
||||
|
||||
// The sessionKey is returned for API-only non-SSO users only.
|
||||
var sessionKey *string
|
||||
if user.APIOnly && !user.SSOEnabled {
|
||||
if p.Password == nil {
|
||||
// Should not happen but let's log just in case.
|
||||
level.Error(svc.logger).Log("err", err, "msg", "password not set during admin user creation")
|
||||
} else {
|
||||
// Create a session for the API-only user by logging in.
|
||||
_, session, err := svc.Login(ctx, user.Email, *p.Password)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "create session for api-only user")
|
||||
}
|
||||
sessionKey = &session.Key
|
||||
}
|
||||
}
|
||||
|
||||
return user, sessionKey, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ func TestUserAuth(t *testing.T) {
|
|||
}
|
||||
|
||||
teams := []fleet.UserTeam{{Team: fleet.Team{ID: teamID}, Role: fleet.RoleMaintainer}}
|
||||
_, err = svc.CreateUser(ctx, fleet.UserPayload{
|
||||
_, _, err = svc.CreateUser(ctx, fleet.UserPayload{
|
||||
Name: ptr.String("Some Name"),
|
||||
Email: ptr.String("some@email.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
|
|
@ -375,7 +375,7 @@ func TestUserAuth(t *testing.T) {
|
|||
})
|
||||
checkAuthErr(t, tt.shouldFailTeamWrite, err)
|
||||
|
||||
_, err = svc.CreateUser(ctx, fleet.UserPayload{
|
||||
_, _, err = svc.CreateUser(ctx, fleet.UserPayload{
|
||||
Name: ptr.String("Some Name"),
|
||||
Email: ptr.String("some@email.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
|
|
@ -641,6 +641,7 @@ func TestUsersWithDS(t *testing.T) {
|
|||
{"CreateUserForcePasswdReset", testUsersCreateUserForcePasswdReset},
|
||||
{"ChangePassword", testUsersChangePassword},
|
||||
{"RequirePasswordReset", testUsersRequirePasswordReset},
|
||||
{"UsersCreateUserWithAPIOnly", testUsersCreateUserWithAPIOnly},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -668,13 +669,14 @@ func testUsersCreateUserForcePasswdReset(t *testing.T, ds *mysql.Datastore) {
|
|||
|
||||
// As the admin, create a new user.
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
|
||||
user, err := svc.CreateUser(ctx, fleet.UserPayload{
|
||||
user, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
|
||||
Name: ptr.String("Some Observer"),
|
||||
Email: ptr.String("some-observer@email.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, sessionKey) // only set when creating API-only users
|
||||
|
||||
user, err = ds.UserByID(context.Background(), user.ID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1319,3 +1321,50 @@ func TestTeamAdminAddRoleOtherTeam(t *testing.T) {
|
|||
require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
|
||||
require.False(t, ds.SaveUserFuncInvoked)
|
||||
}
|
||||
|
||||
func testUsersCreateUserWithAPIOnly(t *testing.T, ds *mysql.Datastore) {
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
UUID: "uuid-42",
|
||||
OsqueryHostID: ptr.String("osquery_host_id-42"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create admin user.
|
||||
admin := &fleet.User{
|
||||
Name: "Fleet Admin",
|
||||
Email: "admin@foo.com",
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
}
|
||||
err = admin.SetPassword(test.GoodPassword, 10, 10)
|
||||
require.NoError(t, err)
|
||||
admin, err = ds.NewUser(ctx, admin)
|
||||
require.NoError(t, err)
|
||||
|
||||
// As the admin, create a new API-only user.
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
|
||||
apiOnlyUser, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
|
||||
Name: ptr.String("Some Observer"),
|
||||
Email: ptr.String("some-observer@email.com"),
|
||||
Password: ptr.String(test.GoodPassword),
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
APIOnly: ptr.Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sessionKey)
|
||||
require.NotEmpty(t, *sessionKey)
|
||||
|
||||
sessions, err := svc.GetInfoAboutSessionsForUser(ctx, apiOnlyUser.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sessions, 1)
|
||||
session := sessions[0]
|
||||
require.Equal(t, *sessionKey, session.Key)
|
||||
|
||||
refreshCtx(t, ctx, apiOnlyUser, ds, session)
|
||||
|
||||
hosts, err := svc.ListHosts(ctx, fleet.HostListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
require.Equal(t, host.ID, hosts[0].ID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func TestMacosSetupAssistant(t *testing.T) {
|
|||
DEPClient: apple_mdm.NewDEPClient(depStorage, ds, logger),
|
||||
}
|
||||
|
||||
const defaultProfileName = "FleetDM default enrollment profile"
|
||||
const defaultProfileName = "Fleet default enrollment profile"
|
||||
|
||||
// track the profile assigned to each device
|
||||
serialsToProfile := map[string]string{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ github.com/fleetdm/fleet/v4/server/fleet/ServerSettings DeferredSaveHost bool
|
|||
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportsDisabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings ScriptsDisabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings AIFeaturesDisabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportCap int
|
||||
github.com/fleetdm/fleet/v4/server/fleet/AppConfig SMTPSettings *fleet.SMTPSettings
|
||||
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPEnabled bool
|
||||
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPConfigured bool
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ module.exports = {
|
|||
fleetApiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
redirectToExternalPageAfterAuthorization: {
|
||||
type: 'string',
|
||||
description: 'If provided, the user will be sent to this URL after they complete the setup of this integration'
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -59,7 +63,6 @@ module.exports = {
|
|||
},
|
||||
|
||||
fn: async function (inputs) {
|
||||
|
||||
let url = require('url');
|
||||
|
||||
// Look for any existing VantaConnection records that use this fleet instance URL.
|
||||
|
|
@ -139,17 +142,26 @@ module.exports = {
|
|||
fleetApiKey: inputs.fleetApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
let callbackUrl = `/vanta-authorization`;
|
||||
if(inputs.redirectToExternalPageAfterAuthorization){
|
||||
callbackUrl += `?redirectAfterSetup=${inputs.redirectToExternalPageAfterAuthorization}`;
|
||||
}
|
||||
// Build the authorization URL for this request.
|
||||
let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, '/vanta-authorization'))}&response_type=code`;
|
||||
let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, callbackUrl))}&response_type=code`;
|
||||
|
||||
// Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
|
||||
this.res.cookie('state', generatedStateForThisRequest, {signed: true});
|
||||
if(inputs.redirectToExternalPageAfterAuthorization){
|
||||
let internalRedirectUrl = `${sails.config.custom.baseUrl}/redirect-vanta-authorization-request?vantaSourceId=${encodeURIComponent(sourceIDForThisRequest)}&state=${encodeURIComponent(generatedStateForThisRequest)}&vantaAuthorizationRequestURL=${encodeURIComponent(vantaAuthorizationRequestURL)}&redirectAfterSetup=${encodeURIComponent(inputs.redirectToExternalPageAfterAuthorization)}`;
|
||||
|
||||
// Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
|
||||
this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
|
||||
|
||||
return vantaAuthorizationRequestURL;
|
||||
return internalRedirectUrl;
|
||||
// If the useInternalRedirect input was provided, we'll return the URL of an internal endpoiint that will set the required cookies for this request.
|
||||
} else {
|
||||
// Otherwise, if this request came from a user on the connect-vanta page, we'll set the cookies are redirect them directly to Vanta.
|
||||
// Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
|
||||
this.res.cookie('state', generatedStateForThisRequest, {signed: true});
|
||||
// Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
|
||||
this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
|
||||
return vantaAuthorizationRequestURL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
61
website/api/controllers/redirect-vanta-authorization-request.js
vendored
Normal file
61
website/api/controllers/redirect-vanta-authorization-request.js
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
module.exports = {
|
||||
|
||||
|
||||
friendlyName: 'Redirect vanta authorization request',
|
||||
|
||||
|
||||
description: 'Sets provided inputs in the user`s browser as cookies and redirects them to Vanta.',
|
||||
|
||||
|
||||
inputs: {
|
||||
vantaSourceId: {
|
||||
type: 'string',
|
||||
description: 'The generated vanta Source ID for this request.',
|
||||
required: true,
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'The state provided to Vanta when an authorization request was created',
|
||||
required: true,
|
||||
},
|
||||
vantaAuthorizationRequestURL: {
|
||||
type: 'string',
|
||||
description: 'The Vanta authorization url that the user will be directed to after they are sent to this page.',
|
||||
required: true,
|
||||
},
|
||||
redirectAfterSetup: {
|
||||
type: 'string',
|
||||
description: 'The URL that the user will be redirected to after they complete setup.',
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
exits: {
|
||||
noMatchingVantaConnection: {
|
||||
description: 'No Vanta connection could be found using the provided vantaSourceId',
|
||||
responseType: 'badRequest'
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
fn: async function ({vantaSourceId, state, vantaAuthorizationRequestURL, redirectAfterSetup}) {
|
||||
|
||||
// Find the VantaConnection record that we created when the user created this request.
|
||||
let recordOfThisAuthorization = await VantaConnection.findOne({vantaSourceId: vantaSourceId});
|
||||
|
||||
// If no record of this authorization could be found, return a noMatchingVantaConnection response.
|
||||
if(!recordOfThisAuthorization){
|
||||
throw 'noMatchingVantaConnection';
|
||||
}
|
||||
|
||||
// Set a 'state' and 'vantaSourceId' cookie on the users browser.
|
||||
this.res.cookie('redirectAfterSetup', redirectAfterSetup, {signed: true});
|
||||
this.res.cookie('state', state, {signed: true});
|
||||
this.res.cookie('vantaSourceId', vantaSourceId, {signed: true});
|
||||
// now that the user has the required cookies to complete the vanta integration setup, redirect them to the provided VantaAuthorizationUrl.
|
||||
return this.res.redirect(vantaAuthorizationRequestURL);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -89,7 +89,9 @@ module.exports = {
|
|||
if(!updatedRecord){
|
||||
throw new Error(`When trying to update a VantaConnection record (id: ${recordOfThisAuthorization.id}) with an authorization token from Vanta, the database record associated with this request has gone missing.`);
|
||||
}
|
||||
|
||||
if(this.req.signedCookies.redirectAfterSetup){
|
||||
return this.res.redirect(this.req.signedCookies.redirectAfterSetup);
|
||||
}
|
||||
return {
|
||||
showSuccessMessage: true
|
||||
};
|
||||
|
|
|
|||
2
website/assets/.eslintrc
vendored
2
website/assets/.eslintrc
vendored
|
|
@ -48,7 +48,7 @@
|
|||
"moment": true,
|
||||
"docsearch": true,
|
||||
"Chart": true,
|
||||
// "google": true,
|
||||
"gtag": true,
|
||||
// ...etc.
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
|
|
|
|||
BIN
website/assets/images/permanent/mdm-migration-sonoma-1500x938.png
vendored
Normal file
BIN
website/assets/images/permanent/mdm-migration-sonoma-1500x938.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
7
website/assets/js/pages/contact.page.js
vendored
7
website/assets/js/pages/contact.page.js
vendored
|
|
@ -80,13 +80,18 @@ parasails.registerPage('contact', {
|
|||
methods: {
|
||||
|
||||
submittedContactForm: async function() {
|
||||
|
||||
if(typeof gtag !== 'undefined'){
|
||||
gtag('event','website_contact_forms');
|
||||
}
|
||||
// Show the success message.
|
||||
this.cloudSuccess = true;
|
||||
|
||||
},
|
||||
submittedTalkToUsForm: async function() {
|
||||
this.syncing = true;
|
||||
if(typeof gtag !== 'undefined'){
|
||||
gtag('event','website_contact_forms');
|
||||
}
|
||||
if(this.formData.numberOfHosts > 700){
|
||||
this.goto(`https://calendly.com/fleetdm/talk-to-us?email=${encodeURIComponent(this.formData.emailAddress)}&name=${encodeURIComponent(this.formData.firstName+' '+this.formData.lastName)}`);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,13 @@ parasails.registerPage('basic-documentation', {
|
|||
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
methods: {
|
||||
|
||||
clickSwagRequestCTA: function () {
|
||||
if(typeof gtag !== 'undefined') {
|
||||
gtag('event','website_swag_request');
|
||||
}
|
||||
this.goto('https://kqphpqst851.typeform.com/to/ZfA3sOu0');
|
||||
},
|
||||
|
||||
clickCTA: function (slug) {
|
||||
this.goto(slug);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ parasails.registerPage('signup', {
|
|||
// redirect to the /start page.
|
||||
// > (Note that we re-enable the syncing state here. This is on purpose--
|
||||
// > to make sure the spinner stays there until the page navigation finishes.)
|
||||
if(typeof gtag !== 'undefined'){
|
||||
gtag('event','website_sign_up');
|
||||
}
|
||||
this.syncing = true;
|
||||
this.goto(this.pageToRedirectToAfterRegistration);// « / start if the user came here from the start now button, or customers/new-license if the user came here from the "Get your license" link.
|
||||
}
|
||||
|
|
|
|||
1
website/config/policies.js
vendored
1
website/config/policies.js
vendored
|
|
@ -55,4 +55,5 @@ module.exports.policies = {
|
|||
'deliver-talk-to-us-form-submission': true,
|
||||
'get-human-interpretation-from-osquery-sql': true,
|
||||
'customers/view-new-license': true,
|
||||
'redirect-vanta-authorization-request': true,
|
||||
};
|
||||
|
|
|
|||
3
website/config/routes.js
vendored
3
website/config/routes.js
vendored
|
|
@ -589,7 +589,8 @@ module.exports.routes = {
|
|||
'POST /api/v1/create-or-update-one-newsletter-subscription': { action: 'create-or-update-one-newsletter-subscription' },
|
||||
'/api/v1/unsubscribe-from-all-newsletters': { action: 'unsubscribe-from-all-newsletters' },
|
||||
'POST /api/v1/admin/build-license-key': { action: 'admin/build-license-key' },
|
||||
'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request' },
|
||||
'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request', csrf: false },
|
||||
'GET /redirect-vanta-authorization-request': { action: 'redirect-vanta-authorization-request' },
|
||||
'POST /api/v1/deliver-mdm-beta-signup': { action: 'deliver-mdm-beta-signup' },
|
||||
'POST /api/v1/get-human-interpretation-from-osquery-sql': { action: 'get-human-interpretation-from-osquery-sql', csrf: false },
|
||||
'POST /api/v1/deliver-apple-csr ': { action: 'deliver-apple-csr', csrf: false},
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
<a class="pb-3" target="_blank" href="https://github.com/fleetdm/fleet/releases">Releases</a>
|
||||
<a class="pb-3" target="_blank" href="/support">Support</a>
|
||||
<div class="d-none d-lg-block" purpose="swag-cta" v-if="showSwagForm">
|
||||
<a class="d-flex align-items-center justify-content-center" href="https://kqphpqst851.typeform.com/to/ZfA3sOu0" target="_blank">
|
||||
<a class="d-flex align-items-center justify-content-center" @click="clickSwagRequestCTA()">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<img style="height: auto; width: 47px; margin-bottom: 8px;" alt="A very nice Fleet branded shirt" src="/images/fleet-shirt-60x55@2x.png">
|
||||
<p class="mb-0">Request Fleet swag</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue