mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Merge branch 'main' into feat-resend-config-profile
This commit is contained in:
commit
5df04c8cca
222 changed files with 4551 additions and 1532 deletions
2
.github/ISSUE_TEMPLATE/release-qa.md
vendored
2
.github/ISSUE_TEMPLATE/release-qa.md
vendored
|
|
@ -147,7 +147,7 @@ Run the instructions in [tools/percona/test/README.md](../../tools/percona/test/
|
|||
<tr><td>Release blockers</td><td>Verify there are no outstanding release blocking tickets.</td><td>
|
||||
|
||||
1. Check [this](https://github.com/fleetdm/fleet/labels/~release%20blocker) filter to view all open `~release blocker` tickets.
|
||||
2. If any are found raise an alarm in the `#help-engineering` and `#help-product-design` channels.
|
||||
2. If any are found raise an alarm in the `#help-engineering` and `#g-mdm` (or `#g-endpoint-ops`) channels.
|
||||
</td><td>pass/fail</td></tr>
|
||||
</table>
|
||||
|
||||
|
|
|
|||
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
|
|
@ -9,10 +9,11 @@ If some of the following don't apply, delete the relevant line.
|
|||
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)
|
||||
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.
|
||||
- [ ] Added/updated tests
|
||||
- [ ] If database migrations are included, checked table schema to confirm autoupdate
|
||||
- [ ] If database migrations are included, checked table schema to confirm autoupdate
|
||||
- For database migrations:
|
||||
- [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration.
|
||||
- [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
|
||||
- [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
|
||||
- [ ] Manual QA for all new/changed functionality
|
||||
- For Orbit and Fleet Desktop changes:
|
||||
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.
|
||||
|
|
|
|||
19
.github/workflows/config/slack_payload_template.json
vendored
Normal file
19
.github/workflows/config/slack_payload_template.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"text": "${{ env.JOB_STATUS }}\n${{ env.EVENT_URL }}",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Go tests result: ${{ env.JOB_STATUS }}\n${{ env.RUN_URL }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Summary:\n```${GO_FAIL_SUMMARY}```"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
1
.github/workflows/dogfood-gitops.yml
vendored
1
.github/workflows/dogfood-gitops.yml
vendored
|
|
@ -64,3 +64,4 @@ jobs:
|
|||
DOGFOOD_SERVERS_ENROLL_SECRET: ${{ secrets.DOGFOOD_SERVERS_ENROLL_SECRET }}
|
||||
DOGFOOD_SERVERS_CANARY_ENROLL_SECRET: ${{ secrets.DOGFOOD_SERVERS_CANARY_ENROLL_SECRET }}
|
||||
DOGFOOD_EXPLORE_DATA_ENROLL_SECRET: ${{ secrets.DOGFOOD_EXPLORE_DATA_ENROLL_SECRET }}
|
||||
DOGFOOD_CALENDAR_API_KEY: ${{ secrets.DOGFOOD_CALENDAR_API_KEY }}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defaults:
|
|||
shell: bash
|
||||
|
||||
env:
|
||||
FLEET_DESKTOP_VERSION: 1.22.0
|
||||
FLEET_DESKTOP_VERSION: 1.23.0
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
39
.github/workflows/test-go.yaml
vendored
39
.github/workflows/test-go.yaml
vendored
|
|
@ -99,7 +99,6 @@ jobs:
|
|||
done
|
||||
echo "mysql is ready"
|
||||
|
||||
|
||||
- name: Run Go Tests
|
||||
run: |
|
||||
GO_TEST_EXTRA_FLAGS="-v -race=$RACE_ENABLED -timeout=$GO_TEST_TIMEOUT" \
|
||||
|
|
@ -119,24 +118,29 @@ jobs:
|
|||
files: coverage.txt
|
||||
flags: backend
|
||||
|
||||
- name: Generate summary of errors
|
||||
if: github.event.schedule == '0 4 * * *' && failure()
|
||||
run: |
|
||||
c1grep() { grep "$@" || test $? = 1; }
|
||||
c1grep -oP 'FAIL: .*$' /tmp/gotest.log > /tmp/summary.txt
|
||||
c1grep 'test timed out after' /tmp/gotest.log >> /tmp/summary.txt
|
||||
c1grep 'fatal error:' /tmp/gotest.log >> /tmp/summary.txt
|
||||
GO_FAIL_SUMMARY=$(head -n 5 /tmp/summary.txt | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
echo "GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY"
|
||||
if [[ -z "$GO_FAIL_SUMMARY" ]]; then
|
||||
GO_FAIL_SUMMARY="unknown, please check the build URL"
|
||||
fi
|
||||
GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY envsubst < .github/workflows/config/slack_payload_template.json > ./payload.json
|
||||
|
||||
- name: Slack Notification
|
||||
if: github.event.schedule == '0 4 * * *' && failure()
|
||||
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Go tests result: ${{ job.status }}\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
payload-file-path: ./payload.json
|
||||
env:
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
EVENT_URL: ${{ github.event.pull_request.html_url || github.event.head.html_url }}
|
||||
RUN_URL: https://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
|
|
@ -147,3 +151,10 @@ jobs:
|
|||
name: test-log
|
||||
path: /tmp/gotest.log
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload summary test log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
|
||||
with:
|
||||
name: summary-test-log
|
||||
path: /tmp/summary.txt
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
|
@ -7,6 +7,7 @@
|
|||
"redhat.vscode-yaml",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"firefox-devtools.vscode-firefox-debug",
|
||||
"editorconfig.editorconfig"
|
||||
"editorconfig.editorconfig",
|
||||
"timonwong.shellcheck"
|
||||
]
|
||||
}
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -1,3 +1,18 @@
|
|||
## Fleet 4.48.2 (Apr 09, 2024)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration where it could fail if the database did not default to the `utf8mb4_unicode_ci` collation.
|
||||
* Fixed an issue with automatic release of the device after setup when a DDM profile is pending.
|
||||
|
||||
## Fleet 4.48.1 (Apr 08, 2024)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Made block_id mismatch errors more informative as 400s instead of 500s
|
||||
- Fixed a bug where values were not being rendered in host-specific query reports
|
||||
- Fixed potential server panic when events are created with calendar integration, but then global calendar integration is disabled
|
||||
|
||||
## Fleet 4.48.0 (Apr 03, 2024)
|
||||
|
||||
### Endpoint operations
|
||||
|
|
@ -27,7 +42,7 @@
|
|||
- Fixed a bug where valid MDM enrollments would show up as unmanaged (EnrollmentState 3).
|
||||
- Fixed flash message from closing when a modal closes.
|
||||
- Fixed a bug where OS version information would not get detected on Windows Server 2019.
|
||||
- Fixed issue where getting host details failed when attempting to read the host's BitLocker status from the datastore.
|
||||
- Fixed issue where getting host details failed when attempting to read the host's bitlocker status from the datastore.
|
||||
- Fixed false negative vulnerabilities on macOS Homebrew python packages.
|
||||
- Fixed styling of live query disabled warning.
|
||||
- Fixed issue where Windows MDM profile processing was skipping `<Add>` commands.
|
||||
|
|
@ -36,11 +51,11 @@
|
|||
- Fixed `GET fleet/os_versions` and `GET fleet/os_versions/[id]` so team users no longer have access to os versions on hosts from other teams.
|
||||
- `fleetctl gitops` now batch processes queries and policies.
|
||||
- Fixed UI bug to render the query platform correctly for queries imported from the standard query library.
|
||||
- Fixed issue where Microsoft Edge was not reporting vulnerabilities.
|
||||
- Fixed issue where microsoft edge was not reporting vulnerabilities.
|
||||
- Fixed a bug where all Windows MDM enrollments were detected as automatic.
|
||||
- Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500.
|
||||
- Fixed query reports so they reset when there is a change to the selected platform or selected minimum osquery version.
|
||||
- Fixed live query sort of SQL result sort for both string and numerical columns.
|
||||
- Fixed live query sort of sql result sort for both string and numerical columns.
|
||||
|
||||
## Fleet 4.47.3 (Mar 26, 2024)
|
||||
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -318,7 +318,9 @@ changelog:
|
|||
sh -c "git rm changes/*"
|
||||
|
||||
changelog-orbit:
|
||||
sh -c "find orbit/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md"
|
||||
$(eval TODAY_DATE := $(shell date "+%b %d, %Y"))
|
||||
@echo -e "## Orbit $(version) ($(TODAY_DATE))\n" > new-CHANGELOG.md
|
||||
sh -c "find orbit/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {} | sed -E "s/^-/*/"; echo' >> new-CHANGELOG.md"
|
||||
sh -c "cat new-CHANGELOG.md orbit/CHANGELOG.md > tmp-CHANGELOG.md && rm new-CHANGELOG.md && mv tmp-CHANGELOG.md orbit/CHANGELOG.md"
|
||||
sh -c "git rm orbit/changes/*"
|
||||
|
||||
|
|
@ -394,7 +396,7 @@ ifneq ($(shell uname), Darwin)
|
|||
@exit 1
|
||||
endif
|
||||
# locking the version of swiftDialog to 2.2.1-4591 as newer versions
|
||||
# migth have layout issues.
|
||||
# might have layout issues.
|
||||
ifneq ($(version), 2.2.1)
|
||||
@echo "Version is locked at 2.1.0, see comments in Makefile target for details"
|
||||
@exit 1
|
||||
|
|
|
|||
1
changes/12290-run-query-on-host
Normal file
1
changes/12290-run-query-on-host
Normal file
|
|
@ -0,0 +1 @@
|
|||
- UI revamp: Run query on an online host
|
||||
1
changes/12292-policies-filter-by-platform
Normal file
1
changes/12292-policies-filter-by-platform
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Add filters by platform to select a new policy modal
|
||||
1
changes/15929-migration-rate-limit
Normal file
1
changes/15929-migration-rate-limit
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Do not allow an MDM migration to start if the device doesn't have the right ADE JSON profile already assigned.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
* Ignore leading and trailing whitespace when filtering Fleet entities by name
|
||||
|
||||
3
changes/17157-translate-api-error
Normal file
3
changes/17157-translate-api-error
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
- Fix a bug where the translate API returned "forbidden" instead of "bad request" for an empty JSON body.
|
||||
- Fixed an uncaught bug where "forbidden" would be returned for invalid payload type, which should
|
||||
also be a bad request.
|
||||
1
changes/17362-orbit-and-desktop-version
Normal file
1
changes/17362-orbit-and-desktop-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
In GET fleet/hosts/:id response, added orbit_version, fleet_desktop_version, and scripts_enabled fields.
|
||||
1
changes/17787-hidden-columns
Normal file
1
changes/17787-hidden-columns
Normal file
|
|
@ -0,0 +1 @@
|
|||
- UI and website show hidden columns in schema with a note that they won't be returned by running select \* from table
|
||||
1
changes/17796-bitlocker-server
Normal file
1
changes/17796-bitlocker-server
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fix bug where query retrieving bitlocker info from windows server wouldn't return
|
||||
2
changes/17946-fleetd-chrome-numbers
Normal file
2
changes/17946-fleetd-chrome-numbers
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Fix a bug where values not derived from "actual" fleetd-chrome tables were not being displayed
|
||||
correctly (e.g., `SELECT 1` gets its value from the query itself, not a table)
|
||||
1
changes/18003-windows-mdm-reserved-profiles
Normal file
1
changes/18003-windows-mdm-reserved-profiles
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fixed issue where applying Windows MDM profiles using `fleetctl apply` would cause Fleet to overwrite the reserved profile used to manage Windows OS updates.
|
||||
1
changes/18034-builtin-labels
Normal file
1
changes/18034-builtin-labels
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Updated label endpoints and UI to prevent creating, updating, or deleting built-in labels.
|
||||
6
changes/18041-query-params-parsing
Normal file
6
changes/18041-query-params-parsing
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
- Correctly parse query params for `GET` ...`policies/count`, `GET` ...`teams/:id/policies/count`, and
|
||||
`GET` ...`vulnerabilities`
|
||||
- Also updates `GET` ...`labels` to return `400` when the non-supported `query` url param is
|
||||
included in the request. Previous behavior was to silently ignore that param and return `200`.
|
||||
This is technically a minor breaking change, but one that breaks in the right direction, i.e., if
|
||||
you see this break, you were using a URL param that was being ignored, which you are now aware of.
|
||||
1
changes/18142-fix-migration-issue-related-to-collation
Normal file
1
changes/18142-fix-migration-issue-related-to-collation
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration where it could fail if the database did not default to the `utf8mb4_unicode_ci` collation.
|
||||
|
|
@ -0,0 +1 @@
|
|||
* Fixed an issue with automatic release of the device after setup when a DDM profile is pending.
|
||||
1
changes/license-comparison
Normal file
1
changes/license-comparison
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed license checks to allow migration and restoring DEP devices during trial
|
||||
|
|
@ -8,7 +8,7 @@ version: v6.0.2
|
|||
home: https://github.com/fleetdm/fleet
|
||||
sources:
|
||||
- https://github.com/fleetdm/fleet.git
|
||||
appVersion: v4.48.0
|
||||
appVersion: v4.48.2
|
||||
dependencies:
|
||||
- name: mysql
|
||||
condition: mysql.enabled
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# All settings related to how Fleet is deployed in Kubernetes
|
||||
hostName: fleet.localhost
|
||||
replicas: 3 # The number of Fleet instances to deploy
|
||||
imageTag: v4.48.0 # Version of Fleet to deploy
|
||||
imageTag: v4.48.2 # Version of Fleet to deploy
|
||||
podAnnotations: {} # Additional annotations to add to the Fleet pod
|
||||
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
|
||||
resources:
|
||||
|
|
|
|||
|
|
@ -1457,9 +1457,9 @@ func TestGetQuery(t *testing.T) {
|
|||
Platform: "linux",
|
||||
Logging: "differential",
|
||||
}, nil
|
||||
} else {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
expectedYaml := `---
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -487,6 +488,21 @@ func TestMDMLockCommand(t *testing.T) {
|
|||
return h.MDMInfo, nil
|
||||
}
|
||||
|
||||
ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
|
||||
hostIDMod := hostID % 3
|
||||
switch hostIDMod {
|
||||
case 0:
|
||||
return nil, ¬FoundError{}
|
||||
case 1:
|
||||
return &fleet.HostOrbitInfo{}, nil
|
||||
case 2:
|
||||
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
|
||||
default:
|
||||
t.Errorf("unexpected hostIDMod %v", hostIDMod)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
|
||||
successfulOutput := func(ident string) string {
|
||||
|
|
@ -730,6 +746,21 @@ func TestMDMUnlockCommand(t *testing.T) {
|
|||
return h.MDMInfo, nil
|
||||
}
|
||||
|
||||
ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
|
||||
hostIDMod := hostID % 3
|
||||
switch hostIDMod {
|
||||
case 0:
|
||||
return nil, ¬FoundError{}
|
||||
case 1:
|
||||
return &fleet.HostOrbitInfo{}, nil
|
||||
case 2:
|
||||
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
|
||||
default:
|
||||
t.Errorf("unexpected hostIDMod %v", hostIDMod)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
|
||||
successfulOutput := func(ident string) string {
|
||||
|
|
@ -793,11 +824,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)")},
|
||||
}
|
||||
linuxEnrolled := &fleet.Host{
|
||||
ID: 3,
|
||||
UUID: "linux-enrolled",
|
||||
Platform: "linux",
|
||||
}
|
||||
winNotEnrolled := &fleet.Host{
|
||||
ID: 4,
|
||||
UUID: "win-not-enrolled",
|
||||
|
|
@ -892,6 +918,23 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
|
||||
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")},
|
||||
}
|
||||
linuxEnrolled := &fleet.Host{
|
||||
ID: 18,
|
||||
UUID: "linux-enrolled",
|
||||
Platform: "linux",
|
||||
}
|
||||
linuxEnrolled2 := &fleet.Host{
|
||||
ID: 19,
|
||||
UUID: "linux-enrolled",
|
||||
Platform: "linux",
|
||||
}
|
||||
linuxEnrolled3 := &fleet.Host{
|
||||
ID: 20,
|
||||
UUID: "linux-enrolled",
|
||||
Platform: "linux",
|
||||
}
|
||||
|
||||
linuxHostIDs := []uint{linuxEnrolled.ID, linuxEnrolled2.ID, linuxEnrolled3.ID}
|
||||
|
||||
hostByUUID := make(map[string]*fleet.Host)
|
||||
hostsByID := make(map[uint]*fleet.Host)
|
||||
|
|
@ -899,6 +942,8 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
winEnrolled,
|
||||
macEnrolled,
|
||||
linuxEnrolled,
|
||||
linuxEnrolled2,
|
||||
linuxEnrolled3,
|
||||
macNotEnrolled,
|
||||
winNotEnrolled,
|
||||
macPending,
|
||||
|
|
@ -1039,6 +1084,26 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
return h.MDMInfo, nil
|
||||
}
|
||||
|
||||
// This function should only run on linux
|
||||
ds.GetHostOrbitInfoFunc = func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
|
||||
if !slices.Contains(linuxHostIDs, hostID) {
|
||||
t.Errorf("GetHostOrbitInfo should not be called for non-linux host %v", hostID)
|
||||
return nil, nil
|
||||
}
|
||||
hostIDMod := hostID % 3
|
||||
switch hostIDMod {
|
||||
case 0:
|
||||
return nil, ¬FoundError{}
|
||||
case 1:
|
||||
return &fleet.HostOrbitInfo{}, nil
|
||||
case 2:
|
||||
return &fleet.HostOrbitInfo{ScriptsEnabled: ptr.Bool(true)}, nil
|
||||
default:
|
||||
t.Errorf("unexpected hostIDMod %v", hostIDMod)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
|
||||
appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}}
|
||||
|
||||
|
|
@ -1055,6 +1120,8 @@ func TestMDMWipeCommand(t *testing.T) {
|
|||
{appCfgAllMDM, "valid windows", []string{"--host", winEnrolled.UUID}, ""},
|
||||
{appCfgAllMDM, "valid macos", []string{"--host", macEnrolled.UUID}, ""},
|
||||
{appCfgNoMDM, "valid linux", []string{"--host", linuxEnrolled.UUID}, ""},
|
||||
{appCfgNoMDM, "valid linux 2", []string{"--host", linuxEnrolled2.UUID}, ""},
|
||||
{appCfgNoMDM, "valid linux 3", []string{"--host", linuxEnrolled3.UUID}, ""},
|
||||
{appCfgNoMDM, "valid windows but no mdm", []string{"--host", winEnrolled.UUID}, `Windows MDM isn't turned on.`},
|
||||
{appCfgMacMDM, "valid macos but not enrolled", []string{"--host", macNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
{appCfgWinMDM, "valid windows but not enrolled", []string{"--host", winNotEnrolled.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
"uuid": "",
|
||||
"platform": "",
|
||||
"osquery_version": "",
|
||||
"orbit_version": null,
|
||||
"fleet_desktop_version": null,
|
||||
"scripts_enabled": null,
|
||||
"os_version": "",
|
||||
"build": "",
|
||||
"platform_like": "",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ spec:
|
|||
display_text: test_host
|
||||
display_name: test_host
|
||||
distributed_interval: 0
|
||||
fleet_desktop_version: null
|
||||
gigs_disk_space_available: 0
|
||||
gigs_total_disk_space: 0
|
||||
hardware_model: ""
|
||||
|
|
@ -40,6 +41,7 @@ spec:
|
|||
pending_action: ""
|
||||
server_url: null
|
||||
memory: 0
|
||||
orbit_version: null
|
||||
os_version: ""
|
||||
osquery_version: ""
|
||||
pack_stats: null
|
||||
|
|
@ -83,6 +85,7 @@ spec:
|
|||
primary_mac: ""
|
||||
refetch_requested: false
|
||||
refetch_critical_queries_until: null
|
||||
scripts_enabled: null
|
||||
seen_time: "0001-01-01T00:00:00Z"
|
||||
software_updated_at: "0001-01-01T00:00:00Z"
|
||||
status: offline
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
"uuid": "",
|
||||
"platform": "",
|
||||
"osquery_version": "",
|
||||
"orbit_version": null,
|
||||
"fleet_desktop_version": null,
|
||||
"scripts_enabled": null,
|
||||
"os_version": "",
|
||||
"build": "",
|
||||
"platform_like": "",
|
||||
|
|
@ -89,6 +92,9 @@
|
|||
"uuid": "",
|
||||
"platform": "",
|
||||
"osquery_version": "",
|
||||
"orbit_version": null,
|
||||
"fleet_desktop_version": null,
|
||||
"scripts_enabled": null,
|
||||
"os_version": "",
|
||||
"build": "",
|
||||
"platform_like": "",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
"uuid": "",
|
||||
"platform": "",
|
||||
"osquery_version": "",
|
||||
"orbit_version": null,
|
||||
"fleet_desktop_version": null,
|
||||
"scripts_enabled": null,
|
||||
"os_version": "",
|
||||
"build": "",
|
||||
"platform_like": "",
|
||||
|
|
@ -90,6 +93,9 @@
|
|||
"uuid": "",
|
||||
"platform": "",
|
||||
"osquery_version": "",
|
||||
"orbit_version": null,
|
||||
"fleet_desktop_version": null,
|
||||
"scripts_enabled": null,
|
||||
"os_version": "",
|
||||
"build": "",
|
||||
"platform_like": "",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ spec:
|
|||
detail_updated_at: "0001-01-01T00:00:00Z"
|
||||
display_text: test_host
|
||||
distributed_interval: 0
|
||||
fleet_desktop_version: null
|
||||
gigs_disk_space_available: 0
|
||||
gigs_total_disk_space: 0
|
||||
hardware_model: ""
|
||||
|
|
@ -42,6 +43,7 @@ spec:
|
|||
name: ""
|
||||
server_url: null
|
||||
memory: 0
|
||||
orbit_version: null
|
||||
os_version: ""
|
||||
osquery_version: ""
|
||||
pack_stats: null
|
||||
|
|
@ -54,6 +56,7 @@ spec:
|
|||
primary_mac: ""
|
||||
refetch_requested: false
|
||||
refetch_critical_queries_until: null
|
||||
scripts_enabled: null
|
||||
seen_time: "0001-01-01T00:00:00Z"
|
||||
software_updated_at: "0001-01-01T00:00:00Z"
|
||||
status: offline
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,32 @@ func (a *agent) diskEncryptionLinux() []map[string]string {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *agent) orbitInfo() []map[string]string {
|
||||
version := "1.22.0"
|
||||
desktopVersion := version
|
||||
if a.disableFleetDesktop {
|
||||
desktopVersion = ""
|
||||
}
|
||||
deviceAuthToken := ""
|
||||
if a.deviceAuthToken != nil {
|
||||
deviceAuthToken = *a.deviceAuthToken
|
||||
}
|
||||
return []map[string]string{
|
||||
{
|
||||
"version": version,
|
||||
"device_auth_token": deviceAuthToken,
|
||||
"enrolled": "true",
|
||||
"last_recorded_error": "",
|
||||
"orbit_channel": "stable",
|
||||
"osqueryd_channel": "stable",
|
||||
"desktop_channel": "stable",
|
||||
"desktop_version": desktopVersion,
|
||||
"uptime": "10000",
|
||||
"scripts_enabled": "1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) runLiveQuery(query string) (results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats) {
|
||||
if a.liveQueryFailProb > 0.0 && rand.Float64() <= a.liveQueryFailProb {
|
||||
ss := fleet.OsqueryStatus(1)
|
||||
|
|
@ -1800,6 +1826,11 @@ func (a *agent) processQuery(name, query string) (
|
|||
// the caller knows it is handled, will not try to return lorem-ipsum-style
|
||||
// results.
|
||||
return true, nil, &statusNotOK, nil, nil
|
||||
case name == hostDetailQueryPrefix+"orbit_info":
|
||||
if a.orbitNodeKey == nil {
|
||||
return true, nil, &statusNotOK, nil, nil
|
||||
}
|
||||
return true, a.orbitInfo(), &statusOK, nil, nil
|
||||
default:
|
||||
// Look for results in the template file.
|
||||
if t := a.templates.Lookup(name); t == nil {
|
||||
|
|
|
|||
|
|
@ -234,6 +234,12 @@ spec:
|
|||
secrets:
|
||||
- secret: RzTlxPvugG4o4O5IKS/HqEDJUmI1hwBoffff
|
||||
- secret: JZ/C/Z7ucq22dt/zjx2kEuDBN0iLjqfz
|
||||
webhook_settings:
|
||||
host_status_webhook:
|
||||
days_count: 0
|
||||
destination_url: ""
|
||||
enable_host_status_webhook: false
|
||||
host_percentage: 0
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: true
|
||||
host_expiry_window: 14
|
||||
|
|
|
|||
|
|
@ -1654,7 +1654,7 @@ for which the user has an observer role.
|
|||
|
||||
| Name | Type | In | Description |
|
||||
| ----------------- | ------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| query | string | body | The query used to identify hosts to target. Searchable items include a host's hostname or IPv4 address. |
|
||||
| query | string | body | The query used to identify hosts to target. Searchable items include a `display_name`, `hostname`, `hardware_serial`, `uuid` or `primary_ip`. |
|
||||
| query_id | integer | body | The saved query (if any) that will be run. The `observer_can_run` property on the query and the user's roles affect which targets are included. |
|
||||
| excluded_host_ids | array | body | The list of host ids to omit from the search results. |
|
||||
|
||||
|
|
|
|||
174
docs/Contributing/research/mdm/software-version-extract.md
Normal file
174
docs/Contributing/research/mdm/software-version-extract.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
### Research: extracting name and version from installer packages
|
||||
|
||||
> [!WARNING]
|
||||
> This document is about extracting name and version from the installers, not
|
||||
> about actually installing them on the device.
|
||||
>
|
||||
> For example, extracting info from `.dmg` files is hard for us, but installing
|
||||
> those files should be a low effort task.
|
||||
|
||||
| Type | Eng effort | Accuracy | UX notes |
|
||||
| ------ | ---------- | -------- | ------------------------------------------- |
|
||||
| `.dmg` | High | Medium | - |
|
||||
| `.msi` | Medium | Medium | - |
|
||||
| `.app` | Low | High | It's a folder, needs compression to upload. |
|
||||
| `.pkg` | Low | High | - |
|
||||
| `.exe` | Low | High | - |
|
||||
| `.deb` | Low | High | - |
|
||||
|
||||
More details:
|
||||
|
||||
- Draft PR with a PoC implementation for `.app`, `.exe`, `.pgk`, `.deb` and half of `.msi` in #18232
|
||||
- Research notes with more details for each type below
|
||||
- Additional concerns at the end of this doc
|
||||
|
||||
### Windows Installer (.msi)
|
||||
|
||||
`.msi` files are a relational database (!) laid out in [CFB format](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b)
|
||||
|
||||
Getting the database tables in binary format form the CBF file is
|
||||
[possible](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/msi.go#L40-L41), but extracting the information from the tables is a challenge
|
||||
because the DB format is closed source and Microsoft doesn't disclose any
|
||||
details about the implementation.
|
||||
|
||||
That's why this is labeled as a `Medium` engineering effort.
|
||||
|
||||
The strategy to parse the DB files is to rely on two tables: `_StringData` and `_StringPool`,
|
||||
that contain all unique strings in the DB:
|
||||
|
||||
> there is a single stream in the MSI file that holds all the strings. This
|
||||
> stream is called the string pool contains a single entry for each unique
|
||||
> string. That way a string column in a table is just an integer offset into
|
||||
> the string pool.
|
||||
>
|
||||
> Source: https://robmensching.com/blog/posts/2003/11/25/inside-the-msi-file-format/
|
||||
|
||||
One possibly, but very low accuracy strategy could be to regex the contents of
|
||||
`_StringData` for anything that looks like an application name or version.
|
||||
|
||||
A more sophisticated approach is taken by [this Python library](https://github.com/binref/refinery/blob/de99c87f6dedd6d42508a3d436b6df9181837e34/refinery/units/formats/msi.py#L131) that is able to reverse engineer some of the data based on both tables:
|
||||
|
||||
```
|
||||
$ emit fleet-osquery.msi | ./pyenv/bin/xtmsi MsiTables.json | jq '.Property[] | select(.Property == "ProductName" or .Property == "ProductVersion")'
|
||||
{
|
||||
"Property": "ProductName",
|
||||
"Value": "Fleet osquery"
|
||||
}
|
||||
{
|
||||
"Property": "ProductVersion",
|
||||
"Value": "1.22.0"
|
||||
}
|
||||
```
|
||||
|
||||
A partial implementation that reads the CFB format can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/msi.go).
|
||||
|
||||
### Apple Disk Image (.dmg)
|
||||
|
||||
From Wikipedia:
|
||||
|
||||
> A disk image is a compressed copy of the contents of a disk or folder. Disk
|
||||
> images have .dmg at the end of their names. To see the contents of a disk
|
||||
> image, you must first open the disk image so it appears on the desktop or in
|
||||
> a Finder window.
|
||||
|
||||
There are two challenges that make `.dmg` files a High engineering effort:
|
||||
|
||||
#### Finding the software
|
||||
|
||||
A good mental model would be to imagine `.dmg` files as an USB stick that you
|
||||
plug in a computer: it can contain anything, there are no rules about the kind
|
||||
of files or the structure of them.
|
||||
|
||||
My proposal to fix this problem would be to go for the 80% of the cases and
|
||||
extract the information from the first `.app` or `.pkg` file we find and fail
|
||||
if we don't find anything.
|
||||
|
||||
#### Accessing the contents on the server
|
||||
|
||||
With the strategy to find the software in place, we still need to access the
|
||||
dmg contents on the server, from Wikipedia:
|
||||
|
||||
> Different file systems can be contained inside these disk images, and there
|
||||
> is also support for creating hybrid optical media images that contain
|
||||
> multiple file systems. Some of the file systems supported include
|
||||
> Hierarchical File System (HFS), HFS Plus (HFS+), File Allocation Table (FAT),
|
||||
> ISO9660, and Universal Disk Format (UDF).
|
||||
|
||||
Becuse we can't mount a `dmg` image in the server, and unless we find a
|
||||
creative way to hack around this, we'll need to implement the logic to in Go.
|
||||
|
||||
The only [library I could find](https://github.com/blacktop/go-apfs) is a WIP,
|
||||
and failed to open Google Chrome and Slack `dmg` files provided in their
|
||||
websites, but it's a good starting point if we decide to go this route.
|
||||
|
||||
### Application Bundle (.app)
|
||||
|
||||
[Application Bundles](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW5) can be thought as a file directory with a defined structure and file extension
|
||||
that macOS treats as a single item.
|
||||
|
||||
This folder contains all resources necessary for the app to run. As an example,
|
||||
this is how the Firefox bundle is structured:
|
||||
|
||||
```
|
||||
/Applications $ tree Firefox.app/ -L 2
|
||||
Firefox.app/
|
||||
└── Contents
|
||||
├── CodeResources
|
||||
├── Info.plist
|
||||
├── Library
|
||||
├── MacOS
|
||||
├── PkgInfo
|
||||
├── Resources
|
||||
├── _CodeSignature
|
||||
└── embedded.provisionprofile
|
||||
```
|
||||
|
||||
The `Info.plist` file is a required file that contains metadata about the app.
|
||||
We can read the app version and the display name from there.
|
||||
|
||||
Because a bundle is a folder, we'll need to ask the IT admin to upload the
|
||||
bundle compressed (eg: zip, tar).
|
||||
|
||||
Here's how different browsers behave when you try to upload an `.app` using a
|
||||
file input:
|
||||
|
||||
- Firefox treats it as a folder, and won't let you select it as a unit (screenshot)
|
||||
- Safari and Chrome automatically compresses the folder in zip format (screenshot)
|
||||
|
||||
A full implementation that reads the name and version from `Info.plist` can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/app.go).
|
||||
|
||||
### PKG installers (.pkg)
|
||||
|
||||
Under the hood, `.pkg` installers are compressed files in `xar` format.
|
||||
|
||||
PKG installers are required to have a [Distribution](https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html) file from which we can extract the name and version.
|
||||
|
||||
A full implementation that reads the name and version from the `Distribution` file
|
||||
can be found [here](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/xar.go).
|
||||
|
||||
### Portable Executable (.exe)
|
||||
|
||||
The PE format is well documented in [here](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
|
||||
|
||||
The Go standard library provides a `"debug/pe"` package that we could use as a starting point, but it's not really tailored to our use case.
|
||||
|
||||
The file is composed by different sections, and the name and version can be found in the [`.rsrc` section](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section)
|
||||
|
||||
For the PoC, I used a Go library that's a bit heavy but does the heavy lifting for us ([link](https://github.com/fleetdm/fleet/blob/85ee1f7bb9fe33ece20aca0f38678fb5390d3e9c/pkg/file/pe.go))
|
||||
|
||||
### .deb
|
||||
|
||||
Deb files are `ar` archives that contain a `control.tar` archive with
|
||||
meta-information, including name and version.
|
||||
|
||||
Code that extracts the values can be found [here](https://github.com/sassoftware/relic/blob/6c510a666832163a5d02587bda8be970d5e29b8c/lib/signdeb/control.go#L38-L39)
|
||||
|
||||
## Additional considerations
|
||||
|
||||
### Security
|
||||
|
||||
In many cases, we'll have to write custom parsing logic or rely on third party libraries outside of the standard lib.
|
||||
|
||||
Keeping that in mind we should take special care and consider any installer as untrusted input, common attacks for Go servers rely on malformed files that make the server OOM or panic.
|
||||
|
||||
|
||||
|
|
@ -1,76 +1,99 @@
|
|||
# Single sign-on (SSO)
|
||||
|
||||
Learn how to configure single sign-on (SSO) and just-in-time (JIT) user provisioning.
|
||||
Fleet supports SSO and just-in-time (JIT) user provisioning using any identity provider (IdP) that supports SAML.
|
||||
|
||||
## Overview
|
||||
Fleet supports both service (SP) initiated login and IdP initiated login.
|
||||
|
||||
Fleet supports SAML single sign-on capability.
|
||||
To configure SSO, follow steps for your IdP and then complete [Fleet configuration](#fleet-configuration).
|
||||
|
||||
Fleet supports both SP-initiated SAML login and IDP-initiated login. However, IDP-initiated login must be enabled in the web interface's SAML single sign-on options.
|
||||
## Okta
|
||||
|
||||
Fleet supports the SAML Web Browser SSO Profile using the HTTP Redirect Binding.
|
||||
Create a new SAML app in Okta:
|
||||
|
||||
> Note: The email used in the SAML Assertion must match a user that already exists in Fleet unless you enable [JIT provisioning](#just-in-time-jit-user-provisioning).**
|
||||

|
||||
|
||||
## Identity provider (IDP) configuration
|
||||
If you're configuring [end user authentication](../Using%20Fleet/MDM-macOS-setup-experience.md#end-user-authentication-and-eula), use `https://<your_fleet_url>/api/v1/fleet/mdm/sso/callback` for the **Single sign on URL** instead.
|
||||
|
||||
Setting up the service provider (Fleet) with an identity provider generally requires the following information:
|
||||
Once configured, you will need to retrieve the issuer URI from **View Setup Instructions** and metadata URL from the **Identity Provider metadata** link within the application **Sign on** settings. See below for where to find them:
|
||||
|
||||
- _Assertion Consumer Service_ - This is the call-back URL that the identity provider
|
||||
will use to send security assertions to Fleet. In Okta, this field is called _single sign-on URL_. On Google, it is "ACS URL." The value you supply will be a fully qualified URL consisting of your Fleet web address and the call-back path `/api/v1/fleet/sso/callback`. For example, if your Fleet web address is https://fleet.example.com, then the value you would use in the identity provider configuration would be:
|
||||
```text
|
||||
https://fleet.example.com/api/v1/fleet/sso/callback
|
||||
```
|
||||

|
||||
|
||||
- _Entity ID_ - This value is an identifier that you choose. It identifies your Fleet instance as the service provider that issues authorization requests. The value must match the Entity ID that you define in the Fleet SSO configuration.
|
||||
> The Provider Sign-on URL within **View Setup Instructions** has a similar format as the Provider SAML Metadata URL, but this link provides a redirect to _sign into_ the application, not the metadata necessary for dynamic configuration.
|
||||
|
||||
- _Name ID Format_ - The value should be `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. This may be shortened in the IDP setup to something like `email` or `EmailAddress`.
|
||||
## Google Workspace
|
||||
|
||||
- _Subject Type (Application username in Okta)_ - `email`.
|
||||
Create a new SAML app in Google Workspace:
|
||||
|
||||
After supplying the above information, the IDP will generate an issuer URI and metadata that will be used to configure Fleet as a service provider.
|
||||
1. Navigate to the [Web and Mobile Apps](https://admin.google.com/ac/apps/unified) section of the Google Workspace dashboard. Click **Add App -> Add custom SAML app**.
|
||||
|
||||
## Fleet SSO configuration
|
||||

|
||||
|
||||
A Fleet user must be assigned the Admin role to configure Fleet for SSO. In Fleet, SSO configuration settings are located in **Settings > Organization settings > SAML single sign-on options**.
|
||||
2. Enter "Fleet" for the **App name** and click **Continue**.
|
||||
|
||||
If your IDP supports dynamic configuration, like Okta, you only need to provide an _identity provider name_ and _entity ID_, then paste a link in the metadata URL field. Make sure you create the SSO application within your IDP before configuring it in Fleet.
|
||||

|
||||
|
||||
Otherwise, the following values are required:
|
||||
3. Click **Download Metadata**, saving the metadata to your computer. Click **Continue**.
|
||||
|
||||
- _Identity provider name_ - A human-readable name of the IDP. This is rendered on the login page.
|
||||

|
||||
|
||||
- _Entity ID_ - A URI that identifies your Fleet instance as the issuer of authorization
|
||||
requests (e.g., `fleet.example.com`). This must match the _Entity ID_ configured with the IDP.
|
||||
5. Configure the **Service provider details**:
|
||||
|
||||
- _Metadata URL_ - Obtain this value from the IDP and is used by Fleet to
|
||||
issue authorization requests to the IDP.
|
||||
- For **ACS URL**, use `https://<your_fleet_url>/api/v1/fleet/sso/callback`. If you're configuring [end user authentication](../Using%20Fleet/MDM-macOS-setup-experience.md#end-user-authentication-and-eula), use `https://<your_fleet_url>/api/v1/fleet/mdm/sso/callback` instead.
|
||||
- For Entity ID, use **the same unique identifier from step four** (e.g., "fleet.example.com").
|
||||
- For **Name ID format**, choose `EMAIL`.
|
||||
- For **Name ID**, choose `Basic Information > Primary email`.
|
||||
- All other fields can be left blank.
|
||||
|
||||
- _Metadata_ - If the IDP does not provide a metadata URL, the metadata must
|
||||
be obtained from the IDP and entered. Note that the metadata URL is preferred if
|
||||
the IDP provides metadata in both forms.
|
||||
Click **Continue** at the bottom of the page.
|
||||
|
||||
### Example Fleet SSO configuration
|
||||

|
||||
|
||||
6. Click **Finish**.
|
||||
|
||||

|
||||
|
||||
7. Click the down arrow on the **User access** section of the app details page.
|
||||
|
||||

|
||||
|
||||
8. Check **ON for everyone**. Click **Save**.
|
||||
|
||||

|
||||
|
||||
9. Enable SSO for a test user and try logging in. Note that Google sometimes takes a long time to propagate the SSO configuration, and it can help to try logging in to Fleet with an Incognito/Private window in the browser.
|
||||
|
||||
## Other IdPs
|
||||
|
||||
IdPs generally requires the following information:
|
||||
|
||||
- Assertion Consumer Service - This is the call-back URL that the identity provider will use to send security assertions to Fleet. Use `https://<your_fleet_url>/api/v1/fleet/sso/callback`. If you're configuring end user authentication, use `https://<your_fleet_url>/api/v1/fleet/mdm/sso/callback` instead.
|
||||
|
||||
- Entity ID - This value is an identifier that you choose. It identifies your Fleet instance as the service provider that issues authorization requests. The value must match the Entity ID that you define in the Fleet SSO configuration.
|
||||
|
||||
- Name ID Format - The value should be `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. This may be shortened in the IdP setup to something like `email` or `EmailAddress`.
|
||||
|
||||
- Subject Type - `email`.
|
||||
|
||||
After supplying the above information, your IdP will generate an issuer URI and metadata that will be used to configure Fleet as a service provider.
|
||||
|
||||
## Fleet configuration
|
||||
|
||||
To configure SSO in Fleet head to **Settings > Organization settings > Single sign-on options**.
|
||||
|
||||
If you're configuring end user authentication head to **Settings > Integrations > Automatic enrollment > End user authentication**.
|
||||
|
||||
- **Identity provider name** - A human-readable name of the IdP. This is rendered on the login page.
|
||||
|
||||
- **Entity ID** - A URI that identifies your Fleet instance as the issuer of authorization requests (e.g., `fleet.example.com`). This must match the Entity ID configured with the IdP.
|
||||
|
||||
- **Metadata URL** - Obtain this value from your IdP. and is used by Fleet to
|
||||
issue authorization requests to the IdP.
|
||||
|
||||
- **Metadata** - If the IdP does not provide a metadata URL, the metadata must
|
||||
be obtained from the IdP and entered. Coming soon to end user authentication.
|
||||
|
||||

|
||||
|
||||
## Creating SSO users in Fleet
|
||||
|
||||
When an admin creates a new user in Fleet, they may select the `Enable single sign on` option. The
|
||||
SSO-enabled users will not be able to sign in with a regular user ID and password.
|
||||
|
||||
It is strongly recommended that at least one admin user is set up to use the traditional password-based login so that there is a fallback method for logging into Fleet in the event of SSO
|
||||
configuration problems.
|
||||
|
||||
> Individual users must also be set up on the IDP before signing in to Fleet.
|
||||
|
||||
## Enabling SSO for existing users in Fleet
|
||||
As an admin, you can enable SSO for existing users in Fleet. To do this, go to the Settings page,
|
||||
then click on the Users tab. Locate the user you want to enable SSO for, and in the Actions dropdown
|
||||
menu for that user, click on "Edit." In the dialogue that opens, check the box labeled "Enable
|
||||
single sign-on," then click "Save." If you are unable to check that box, you must first [configure
|
||||
and enable SSO for the organization](https://fleetdm.com/docs/deploying/configuration#configuring-single-sign-on-sso).
|
||||
|
||||
## Just-in-time (JIT) user provisioning
|
||||
|
||||
`Applies only to Fleet Premium`
|
||||
|
|
@ -85,8 +108,8 @@ To enable this option, go to **Settings > Organization settings > Single sign-on
|
|||
|
||||
For this to work correctly make sure that:
|
||||
|
||||
- Your IDP is configured to send the user email as the Name ID (instructions for configuring different providers are detailed below)
|
||||
- Your IDP sends the full name of the user as an attribute with any of the following names (if this value is not provided Fleet will fallback to the user email)
|
||||
- Your IdP is configured to send the user email as the Name ID (instructions for configuring different providers are detailed below)
|
||||
- Your IdP sends the full name of the user as an attribute with any of the following names (if this value is not provided Fleet will fallback to the user email)
|
||||
- `name`
|
||||
- `displayname`
|
||||
- `cn`
|
||||
|
|
@ -167,73 +190,7 @@ Here's a `SAMLResponse` sample to set the role of SSO users to `observer` in tea
|
|||
|
||||
Each IdP will have its own way of setting these SAML custom attributes, here are instructions for how to set it for Okta: https://support.okta.com/help/s/article/How-to-define-and-configure-a-custom-SAML-attribute-statement?language=en_US.
|
||||
|
||||
### Okta IDP configuration
|
||||
|
||||

|
||||
|
||||
Once configured, you will need to retrieve the Issuer URI from the `View Setup Instructions` and metadata URL from the `Identity Provider metadata` link within the application `Sign on` settings. See below for where to find them:
|
||||
|
||||

|
||||
|
||||
> The Provider Sign-on URL within the `View Setup Instructions` has a similar format as the Provider SAML Metadata URL, but this link provides a redirect to _sign into_ the application, not the metadata necessary for dynamic configuration.
|
||||
|
||||
> The names of the items required to configure an identity provider may vary from provider to provider and may not conform to the SAML spec.
|
||||
|
||||
### Google Workspace IDP Configuration
|
||||
|
||||
Follow these steps to configure Fleet SSO with Google Workspace. This will require administrator permissions in Google Workspace.
|
||||
|
||||
1. Navigate to the [Web and Mobile Apps](https://admin.google.com/ac/apps/unified) section of the Google Workspace dashboard. Click _Add App -> Add custom SAML app_.
|
||||
|
||||

|
||||
|
||||
2. Enter `Fleet` for the _App name_ and click _Continue_.
|
||||
|
||||

|
||||
|
||||
3. Click _Download Metadata_, saving the metadata to your computer. Click _Continue_.
|
||||
|
||||

|
||||
|
||||
4. In Fleet, navigate to the _Organization Settings_ page. Configure the _SAML single sign-on options_ section.
|
||||
|
||||
- Check the _Enable single sign-on_ checkbox.
|
||||
- For _Identity provider name_, use `Google`.
|
||||
- For _Entity ID_, use a unique identifier such as `fleet.example.com`. Note that Google seems to error when the provided ID includes `https://`.
|
||||
- For _Metadata_, paste the contents of the downloaded metadata XML from step three.
|
||||
- All other fields can be left blank.
|
||||
|
||||
Click _Update settings_ at the bottom of the page.
|
||||
|
||||

|
||||
|
||||
5. In Google Workspace, configure the _Service provider details_.
|
||||
|
||||
- For _ACS URL_, use `https://<your_fleet_url>/api/v1/fleet/sso/callback` (e.g., `https://fleet.example.com/api/v1/fleet/sso/callback`).
|
||||
- For Entity ID, use **the same unique identifier from step four** (e.g., `fleet.example.com`).
|
||||
- For _Name ID format_, choose `EMAIL`.
|
||||
- For _Name ID_, choose `Basic Information > Primary email`.
|
||||
- All other fields can be left blank.
|
||||
|
||||
Click _Continue_ at the bottom of the page.
|
||||
|
||||

|
||||
|
||||
6. Click _Finish_.
|
||||
|
||||

|
||||
|
||||
7. Click the down arrow on the _User access_ section of the app details page.
|
||||
|
||||

|
||||
|
||||
8. Check _ON for everyone_. Click _Save_.
|
||||
|
||||

|
||||
|
||||
9. Enable SSO for a test user and try logging in. Note that Google sometimes takes a long time to propagate the SSO configuration, and it can help to try logging in to Fleet with an Incognito/Private window in the browser.
|
||||
|
||||
<meta name="title" value="Single sign-on (SSO)">
|
||||
<meta name="pageOrderInSection" value="800">
|
||||
<meta name="description" value="Learn how to configure single sign-on (SSO)">
|
||||
<meta name="navSection" value="TBD">
|
||||
<meta name="navSection" value="TBD">
|
||||
|
|
|
|||
|
|
@ -981,7 +981,25 @@ None.
|
|||
}
|
||||
},
|
||||
"integrations": {
|
||||
"jira": null
|
||||
"jira": null,
|
||||
"google_calendar": [
|
||||
{
|
||||
"domain": "example.com",
|
||||
"api_key_json": {
|
||||
"type": "service_account",
|
||||
"project_id": "fleet-in-your-calendar",
|
||||
"private_key_id": "<private key id>",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\n<private key>\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "fleet-calendar-events@fleet-in-your-calendar.iam.gserviceaccount.com",
|
||||
"client_id": "<client id>",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fleet-calendar-events%40fleet-in-your-calendar.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"debug": false,
|
||||
|
|
@ -1083,6 +1101,8 @@ Modifies the Fleet's configuration with the supplied information.
|
|||
| email | string | body | _integrations.zendesk[] settings_. The Zendesk user email to use for this Zendesk integration. |
|
||||
| api_token | string | body | _integrations.zendesk[] settings_. The Zendesk API token to use for this Zendesk integration. |
|
||||
| group_id | integer | body | _integrations.zendesk[] settings_. The Zendesk group id to use for this integration. Zendesk tickets will be created in this group. |
|
||||
| domain | string | body | _integrations.google_calendar[] settings_. The domain for the Google Workspace service account to be used for this calendar integration. |
|
||||
| api_key_json | object | body | _integrations.google_calendar[] settings_. The private key JSON downloaded when generating the service account API key to be used for this calendar integration. |
|
||||
| apple_bm_default_team | string | body | _mdm settings_. The default team to use with Apple Business Manager. **Requires Fleet Premium license** |
|
||||
| windows_enabled_and_configured | boolean | body | _mdm settings_. Enables Windows MDM support. |
|
||||
| minimum_version | string | body | _mdm.macos_updates settings_. Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version. **Requires Fleet Premium license** |
|
||||
|
|
@ -1272,6 +1292,12 @@ Note that when making changes to the `integrations` object, all integrations mus
|
|||
"project_key": "jira_project",
|
||||
"enable_software_vulnerabilities": false
|
||||
}
|
||||
],
|
||||
"google_calendar": [
|
||||
{
|
||||
"domain": "",
|
||||
"api_key_json": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
|
|
@ -6009,7 +6035,8 @@ Team policies work the same as policies, but at the team level.
|
|||
"updated_at": "2021-12-16T16:39:00Z",
|
||||
"passing_host_count": 2000,
|
||||
"failing_host_count": 300,
|
||||
"host_count_updated_at": "2023-12-20T15:23:57Z"
|
||||
"host_count_updated_at": "2023-12-20T15:23:57Z",
|
||||
"calendar_events_enabled": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
|
@ -6027,7 +6054,8 @@ Team policies work the same as policies, but at the team level.
|
|||
"updated_at": "2021-12-16T16:39:00Z",
|
||||
"passing_host_count": 2300,
|
||||
"failing_host_count": 0,
|
||||
"host_count_updated_at": "2023-12-20T15:23:57Z"
|
||||
"host_count_updated_at": "2023-12-20T15:23:57Z",
|
||||
"calendar_events_enabled": false
|
||||
}
|
||||
],
|
||||
"inherited_policies": [
|
||||
|
|
@ -6116,7 +6144,8 @@ Team policies work the same as policies, but at the team level.
|
|||
"updated_at": "2021-12-16T16:39:00Z",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0,
|
||||
"host_count_updated_at": null
|
||||
"host_count_updated_at": null,
|
||||
"calendar_events_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -6181,7 +6210,8 @@ Either `query` or `query_id` must be provided.
|
|||
"updated_at": "2021-12-16T16:39:00Z",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0,
|
||||
"host_count_updated_at": null
|
||||
"host_count_updated_at": null,
|
||||
"calendar_events_enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -6235,6 +6265,7 @@ Either `query` or `query_id` must be provided.
|
|||
| resolution | string | body | The resolution steps for the policy. |
|
||||
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
|
||||
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
|
||||
| calendar_events_enabled | boolean | body | _Available in Fleet Premium_. Whether to trigger calendar events when policy is failing. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -6275,7 +6306,8 @@ Either `query` or `query_id` must be provided.
|
|||
"updated_at": "2021-12-16T16:39:00Z",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0,
|
||||
"host_count_updated_at": null
|
||||
"host_count_updated_at": null,
|
||||
"calendar_events_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -8463,6 +8495,12 @@ _Available in Fleet Premium_
|
|||
"host_batch_size": 0
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"google_calendar": {
|
||||
"enable_calendar_events": true,
|
||||
"webhook_url": "https://server.com/example"
|
||||
}
|
||||
},
|
||||
"mdm": {
|
||||
"macos_updates": {
|
||||
"minimum_version": "12.3.1",
|
||||
|
|
@ -8578,6 +8616,11 @@ _Available in Fleet Premium_
|
|||
| enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. |
|
||||
| destination_url | string | body | The URL to deliver the webhook requests to. |
|
||||
| policy_ids | array | body | List of policy IDs to enable failing policies webhook. |
|
||||
| host_status_webhook | object | body | Host status webhook settings. |
|
||||
| enable_host_status_webhook | boolean | body | Whether or not the host status webhook is enabled. |
|
||||
| destination_url | string | body | The URL to deliver the webhook request to. |
|
||||
| host_percentage | integer | body | The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. |
|
||||
| days_count | integer | body | The minimum number of days that the configured `host_percentage` must fail to check in to Fleet in order to trigger the webhook request. |
|
||||
| host_batch_size | integer | body | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). |
|
||||
| integrations | object | body | Integrations settings for the team. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). |
|
||||
| jira | array | body | Jira integrations configuration. |
|
||||
|
|
@ -8602,6 +8645,10 @@ _Available in Fleet Premium_
|
|||
| custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. |
|
||||
| macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. |
|
||||
| enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). |
|
||||
| integrations | object | body | Integration settings for this team. |
|
||||
| google_calendar | object | body | Google Calendar integration settings. |
|
||||
| enable_calendar_events | boolean | body | Whether or not calendar events are enabled for this team. |
|
||||
| webhook_url | string | body | The URL to send a request to during calendar events, to trigger auto-remediation. |
|
||||
| host_expiry_settings | object | body | Host expiry settings for the team. |
|
||||
| host_expiry_enabled | boolean | body | When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. When disabled, defaults to the global setting. |
|
||||
| host_expiry_window | integer | body | If a host has not communicated with Fleet in the specified number of days, it will be removed. |
|
||||
|
|
|
|||
|
|
@ -1049,6 +1049,65 @@ This activity contains the following fields:
|
|||
}
|
||||
```
|
||||
|
||||
## created_declaration_profile
|
||||
|
||||
Generated when a user adds a new macOS declaration to a team (or no team).
|
||||
|
||||
This activity contains the following fields:
|
||||
- "profile_name": Name of the declaration.
|
||||
- "identifier": Identifier of the declaration.
|
||||
- "team_id": The ID of the team that the declaration applies to, `null` if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the declaration applies to, `null` if it applies to devices that are not in a team.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"profile_name": "Passcode requirements",
|
||||
"profile_identifier": "com.my.declaration",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}
|
||||
```
|
||||
|
||||
## deleted_declaration_profile
|
||||
|
||||
Generated when a user removes a macOS declaration from a team (or no team).
|
||||
|
||||
This activity contains the following fields:
|
||||
- "profile_name": Name of the declaration.
|
||||
- "identifier": Identifier of the declaration.
|
||||
- "team_id": The ID of the team that the declaration applies to, `null` if it applies to devices that are not in a team.
|
||||
- "team_name": The name of the team that the declaration applies to, `null` if it applies to devices that are not in a team.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"profile_name": "Passcode requirements",
|
||||
"profile_identifier": "com.my.declaration",
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}
|
||||
```
|
||||
|
||||
## edited_declaration_profile
|
||||
|
||||
Generated when a user edits the macOS declarations of a team (or no team) via the fleetctl CLI.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "team_id": The ID of the team that the declarations apply to, `null` if they apply to devices that are not in a team.
|
||||
- "team_name": The name of the team that the declarations apply to, `null` if they apply to devices that are not in a team.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"team_id": 123,
|
||||
"team_name": "Workstations"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<meta name="title" value="Audit logs">
|
||||
<meta name="pageOrderInSection" value="1400">
|
||||
|
|
|
|||
|
|
@ -130,9 +130,9 @@ Follow the steps below to configure Jira or Zendesk as a ticket destination:
|
|||
|
||||
## Host status automations
|
||||
|
||||
Host status automations send a webhook request if a configured percentage of hosts have not checked in to Fleet for a configured number of days.
|
||||
Host status automations send a webhook request if a configured percentage of hosts have not checked in to Fleet for a configured number of days. This can be customized [globally](https://fleetdm.com/docs/configuration/configuration-files#organization-settingss) or [per-team](https://fleetdm.com/docs/configuration/configuration-files#teams).
|
||||
|
||||
Fleet sends these webhook requests once per day by default. This interval can be updated with the `webhook_settings.interval` configuration option using the [`config` YAML document](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) and the `fleetctl apply` command. Note that this interval currently configures both host status and failing policy automations.
|
||||
Fleet sends these webhook requests once per day by default. This interval can be updated with the `webhook_settings.interval` [configuration option](https://fleetdm.com/docs/configuration/configuration-files#organization-settings). Note that this interval currently configures both host status and failing policy automations.
|
||||
|
||||
Example webhook payload:
|
||||
|
||||
|
|
@ -147,9 +147,11 @@ POST https://server.com/example
|
|||
because the Host status webhook is enabeld in your Fleet
|
||||
instance.",
|
||||
"data": {
|
||||
"unseen_hosts": 1,
|
||||
"total_hosts": 2,
|
||||
"unseen_hosts": 3,
|
||||
"total_hosts": 12,
|
||||
"days_unseen": 3,
|
||||
"team_id": 123,
|
||||
"host_ids": [1, 2, 3]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,110 +18,19 @@ MacOS setup features require connecting Fleet to Apple Business Manager (ABM). L
|
|||
|
||||
Using Fleet, you can require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new Mac.
|
||||
|
||||
To require end user authentication, we will do the following steps:
|
||||
### End user authentication
|
||||
|
||||
1. Connect Fleet to your IdP
|
||||
2. Upload a EULA to Fleet (optional)
|
||||
3. Enable end user authentication
|
||||
To require end user authentication, first [configure single sign-on (SSO)](../Deploy/single-sign-on-sso.md). Next, enable end user authentication by heading to to **Controls > Setup experience End user authentication** or use [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
|
||||
|
||||
### Step 1: connect Fleet to your IdP
|
||||
If you've already configured SSO in Fleet, create a new SAML app in your IdP. In your new app, use `https://<your_fleet_url>/api/v1/fleet/mdm/sso/callback` for the SSO URL.
|
||||
|
||||
Fleet UI:
|
||||
In your IdP, make sure your end users' full names are set to one of the following attributes (depends on IdP): `name`, `displayname`, `cn`, `urn:oid:2.5.4.3`, or `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`. Fleet will automatically populate and lock the macOS local account **Full Name** with any of these.
|
||||
|
||||
1. Head to the **Settings > Integrations > Automatic enrollment** page.
|
||||
In your IdP, set **Name ID** to email. Fleet will trim this email and use it to populate and lock the macOS local account **Account Name**. For example, a "johndoe@example.com" email turn into a "johndoe" account name.
|
||||
|
||||
2. Under **End user authentication**, enter your IdP credentials and select **Save**.
|
||||
### EULA
|
||||
|
||||
> If you've already configured [single sign-on (SSO) for logging in to Fleet](https://fleetdm.com/docs/configuration/fleet-server-configuration#okta-idp-configuration), you'll need to create a separate app in your IdP so your end users can't log in to Fleet. In this separate app, use "https://fleetserver.com/api/v1/fleet/mdm/sso/callback" for the SSO URL.
|
||||
|
||||
fleetctl CLI:
|
||||
|
||||
1. Create a `fleet-config.yaml` file or add to your existing `config` YAML file:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
end_user_authentication:
|
||||
identity_provider_name: "Okta"
|
||||
entity_id: "https://fleetserver.com"
|
||||
issuer_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
|
||||
metadata_url: "https://okta-instance.okta.com/84598y345hjdsshsfg/sso/saml/metadata"
|
||||
...
|
||||
```
|
||||
|
||||
2. Fill in the relevant information from your IdP under the `mdm.end_user_authentication` key.
|
||||
|
||||
3. Run the fleetctl `apply -f fleet-config.yml` command to add your IdP credentials.
|
||||
|
||||
4. Confirm that your IdP credentials were saved by running `fleetctl get config`.
|
||||
|
||||
### Step 2: upload a EULA to Fleet
|
||||
|
||||
1. Head to the **Settings > Integrations > Automatic enrollment** page.
|
||||
|
||||
2. Under **End user license agreement (EULA)**, select **Upload** and choose your EULA.
|
||||
|
||||
> Uploading a EULA is optional. If you don't upload a EULA, the end user will skip this step and continue to the next step of the new Mac setup experience after they authenticate with your IdP.
|
||||
|
||||
### Step 3: enable end user authentication
|
||||
|
||||
You can enable end user authentication using the Fleet UI or fleetctl command-line tool.
|
||||
|
||||
Fleet UI:
|
||||
|
||||
1. Head to the **Controls > macOS settings > macOS setup > End user authentication** page.
|
||||
|
||||
2. Choose which team you want to enable end user authentication for by selecting the desired team in the teams dropdown in the upper left corner.
|
||||
|
||||
3. Select the **On** checkbox and select **Save**.
|
||||
|
||||
fleetctl CLI:
|
||||
|
||||
1. Choose which team you want to enable end user authentication on.
|
||||
|
||||
In this example, we'll enable end user authentication on the "Workstations (canary)" team so that the authentication is only required for hosts that automatically enroll to this team.
|
||||
|
||||
2. Create a `workstations-canary-config.yaml` file:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
name: Workstations (canary)
|
||||
mdm:
|
||||
macos_setup:
|
||||
enable_end_user_authentication: true
|
||||
...
|
||||
```
|
||||
|
||||
Learn more about team configurations options [here](./configuration-files/README.md#teams).
|
||||
|
||||
If you want to enable authentication on hosts that automatically enroll to "No team," we'll need to create a `fleet-config.yaml` file:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
macos_setup:
|
||||
enable_end_user_authentication: true
|
||||
...
|
||||
```
|
||||
|
||||
Learn more about "No team" configuration options [here](./configuration-files/README.md#organization-settings).
|
||||
|
||||
3. Add an `mdm.macos_setup.enable_end_user_authentication` key to your YAML document. This key accepts a boolean value.
|
||||
|
||||
4. Run the `fleetctl apply -f workstations-canary-config.yml` command to enable authentication for this team.
|
||||
|
||||
5. Confirm that end user authentication is enabled by running the `fleetctl get teams --name=Workstations --yaml` command.
|
||||
|
||||
If you enabled authentication on "No team," run `fleetctl get config`.
|
||||
|
||||
You should see a `true` value for `mdm.macos_setup.enable_end_user_authentication`.
|
||||
To require a EULA, in Fleet, head to **Settings > Integrations > Automatic enrollment > End user license agreement (EULA)** or use the [Fleet API](https://fleetdm.com/docs/rest-api/rest-api#upload-an-eula-file).
|
||||
|
||||
## Bootstrap package
|
||||
|
||||
|
|
@ -273,7 +182,7 @@ To customize the macOS Setup Assistant, we will do the following steps:
|
|||
|
||||
### Step 1: create an automatic enrollment profile
|
||||
|
||||
1. Download Fleet's example automatic enrollment profile by navigating to the example [here on GitHub](https://github.com/fleetdm/fleet/blob/main/it-and-security/lib/automatic-enrollment.dep.json) and clicking the download icon.
|
||||
1. Download Fleet's example automatic enrollment profile by navigating to the example [here](fleetdm.com/example-dep-profile) and clicking the download icon.
|
||||
|
||||
2. Open the automatic enrollment profile and replace the `profile_name` key with your organization's name.
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ WITH registry_keys AS (
|
|||
-- coalesce to 'unknown' and keep that state in the list
|
||||
-- in order to account for hosts that might not have this
|
||||
-- key, and servers
|
||||
WHERE COALESCE(e.state, '0') IN ('0', '1', '2')
|
||||
WHERE COALESCE(e.state, '0') IN ('0', '1', '2', '3')
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
|
|
@ -373,12 +373,20 @@ SELECT * FROM os_version LIMIT 1
|
|||
|
||||
- Query:
|
||||
```sql
|
||||
SELECT os.name, r.data as display_version, k.version
|
||||
WITH display_version_table AS (
|
||||
SELECT data as display_version
|
||||
FROM registry
|
||||
WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
|
||||
)
|
||||
SELECT
|
||||
os.name,
|
||||
COALESCE(d.display_version, '') AS display_version,
|
||||
k.version
|
||||
FROM
|
||||
registry r,
|
||||
os_version os,
|
||||
kernel_info k
|
||||
WHERE r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
|
||||
LEFT JOIN
|
||||
display_version_table d
|
||||
```
|
||||
|
||||
## os_windows
|
||||
|
|
@ -387,19 +395,23 @@ SELECT os.name, r.data as display_version, k.version
|
|||
|
||||
- Query:
|
||||
```sql
|
||||
SELECT
|
||||
WITH display_version_table AS (
|
||||
SELECT data as display_version
|
||||
FROM registry
|
||||
WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
|
||||
)
|
||||
SELECT
|
||||
os.name,
|
||||
os.platform,
|
||||
os.arch,
|
||||
k.version as kernel_version,
|
||||
os.version,
|
||||
r.data as display_version
|
||||
COALESCE(d.display_version, '') AS display_version
|
||||
FROM
|
||||
os_version os,
|
||||
kernel_info k,
|
||||
registry r
|
||||
WHERE
|
||||
r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
|
||||
kernel_info k
|
||||
LEFT JOIN
|
||||
display_version_table d
|
||||
```
|
||||
|
||||
## osquery_flags
|
||||
|
|
|
|||
|
|
@ -764,8 +764,8 @@ func (p *passphraseHandler) checkPassphrase(store tuf.LocalStore, role string) e
|
|||
continue
|
||||
} else if len(keys) == 0 {
|
||||
return fmt.Errorf("%s key not found", role)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ import VirtualDatabase from "./db";
|
|||
test("Simple query", async () => {
|
||||
const db = await VirtualDatabase.init();
|
||||
const res = await db.query("select 1");
|
||||
expect(res).toEqual({"data": [{ "1": 1 }], "warnings": null});
|
||||
expect(res).toEqual({ data: [{ "1": "1" }], warnings: null });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import SQLiteAsyncESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs";
|
||||
import * as SQLite from "wa-sqlite";
|
||||
|
||||
// Alphabetical order
|
||||
import Table from "./tables/Table";
|
||||
import TableChromeExtensions from "./tables/chrome_extensions";
|
||||
import TableDiskInfo from "./tables/disk_info";
|
||||
|
|
@ -34,7 +33,6 @@ export default class VirtualDatabase {
|
|||
this.sqlite3 = sqlite3;
|
||||
this.db = db;
|
||||
|
||||
// Alphabetical order
|
||||
VirtualDatabase.register(
|
||||
sqlite3,
|
||||
db,
|
||||
|
|
@ -81,7 +79,23 @@ export default class VirtualDatabase {
|
|||
await this.sqlite3.exec(this.db, sql, (row, columns) => {
|
||||
// map each row to object
|
||||
rows.push(
|
||||
Object.fromEntries(columns.map((_, i) => [columns[i], row[i]]))
|
||||
Object.fromEntries(
|
||||
columns.map((_, i) => {
|
||||
let [colName, val] = [columns[i], row[i]];
|
||||
if (typeof val !== "string") {
|
||||
if (val.toString) {
|
||||
val = val.toString();
|
||||
} else {
|
||||
this.warnings.push({
|
||||
column: colName,
|
||||
error_message: `Value is not a string and doesn't have a toString method: ${val}`,
|
||||
});
|
||||
val = null;
|
||||
}
|
||||
}
|
||||
return [colName, val];
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
return { data: rows, warnings: this.warnings };
|
||||
|
|
|
|||
|
|
@ -96,6 +96,20 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
|
|||
if appCfg.ServerSettings.ScriptsDisabled {
|
||||
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")
|
||||
}
|
||||
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
||||
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.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
|
|
@ -166,6 +180,20 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
|
|||
if appCfg.ServerSettings.ScriptsDisabled {
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't unlock 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")
|
||||
}
|
||||
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
||||
return "", ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError(
|
||||
"host_id", "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
|
|
@ -248,6 +276,20 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
|
|||
if appCfg.ServerSettings.ScriptsDisabled {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe 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")
|
||||
}
|
||||
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
|
||||
return ctxerr.Wrap(
|
||||
ctx, fleet.NewInvalidArgumentError(
|
||||
"host_id", "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const (
|
|||
// These are just example keys generated locally with openssl. They are intentionally published
|
||||
// and should never be used in production.
|
||||
exampleKeyPassphrase = "password"
|
||||
exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
||||
// #nosec G101
|
||||
exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,5F4D77F29A9E2675
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { IHost, IHostResponse } from "interfaces/host";
|
||||
import { IHost } from "interfaces/host";
|
||||
import { IHostMdmProfile } from "interfaces/mdm";
|
||||
import { pick } from "lodash";
|
||||
|
||||
import { normalizeEmptyValues } from "utilities/helpers";
|
||||
import { HOST_SUMMARY_DATA } from "utilities/constants";
|
||||
|
||||
const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = {
|
||||
profile_uuid: "123-abc",
|
||||
|
|
@ -35,7 +39,7 @@ const DEFAULT_HOST_MOCK: IHost = {
|
|||
platform: "ubuntu",
|
||||
osquery_version: "4.9.0",
|
||||
orbit_version: "1.22.0",
|
||||
fleet_desktop_version: "1.22.0",
|
||||
fleet_desktop_version: "1.22.1",
|
||||
os_version: "Ubuntu 18.4.0",
|
||||
build: "",
|
||||
platform_like: "debian",
|
||||
|
|
@ -91,6 +95,7 @@ const DEFAULT_HOST_MOCK: IHost = {
|
|||
failing_policies_count: 0,
|
||||
},
|
||||
status: "offline",
|
||||
scripts_enabled: false,
|
||||
labels: [],
|
||||
packs: [],
|
||||
software: [],
|
||||
|
|
@ -105,4 +110,10 @@ const createMockHost = (overrides?: Partial<IHost>): IHost => {
|
|||
|
||||
export const createMockHostResponse = { host: createMockHost() };
|
||||
|
||||
export const createMockHostSummary = (overrides?: Partial<IHost>) => {
|
||||
return normalizeEmptyValues(
|
||||
pick(createMockHost(overrides), HOST_SUMMARY_DATA)
|
||||
);
|
||||
};
|
||||
|
||||
export default createMockHost;
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ const TableContainer = <T,>({
|
|||
);
|
||||
|
||||
const onSearchQueryChange = (value: string) => {
|
||||
setSearchQuery(value);
|
||||
setSearchQuery(value.trim());
|
||||
};
|
||||
|
||||
const hasPageIndexChangedRef = useRef(false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
interface IDisabledOptionTooltipWrapper {
|
||||
children: React.ReactNode;
|
||||
isDelayed?: boolean;
|
||||
className?: string;
|
||||
tooltipClass?: string;
|
||||
clickable?: boolean;
|
||||
tipContent: React.ReactNode;
|
||||
/** Location defaults to left */
|
||||
place?: "left" | "right" | "top" | "bottom";
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
const baseClass = "disabled-option-tooltip-wrapper";
|
||||
|
||||
const DisabledOptionTooltipWrapper = ({
|
||||
children,
|
||||
tipContent,
|
||||
isDelayed,
|
||||
className,
|
||||
tooltipClass,
|
||||
clickable = true,
|
||||
place = "left",
|
||||
offset = 24,
|
||||
}: IDisabledOptionTooltipWrapper) => {
|
||||
const wrapperClassNames = classnames(baseClass, className);
|
||||
|
||||
const elementClassNames = classnames(`${baseClass}__element`);
|
||||
|
||||
const tipClassNames = classnames(
|
||||
`${baseClass}__tip-text`,
|
||||
`${baseClass}__dropdown-tooltip-arrow`,
|
||||
tooltipClass
|
||||
);
|
||||
|
||||
const tipId = uniqueId();
|
||||
|
||||
return (
|
||||
<span className={wrapperClassNames}>
|
||||
<div className={elementClassNames} data-tooltip-id={tipId}>
|
||||
{children}
|
||||
</div>
|
||||
<ReactTooltip5
|
||||
className={tipClassNames}
|
||||
id={tipId}
|
||||
delayShow={isDelayed ? 500 : undefined}
|
||||
delayHide={isDelayed ? 500 : undefined}
|
||||
place={place}
|
||||
opacity={1}
|
||||
disableStyleInjection
|
||||
clickable={clickable}
|
||||
offset={offset}
|
||||
positionStrategy="fixed"
|
||||
classNameArrow="tooltip-arrow"
|
||||
>
|
||||
{tipContent}
|
||||
</ReactTooltip5>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisabledOptionTooltipWrapper;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
.Select > .Select-menu-outer {
|
||||
.is-disabled * {
|
||||
color: $ui-fleet-black-50;
|
||||
.disabled-option-tooltip-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.disabled-option-tooltip-wrapper__element {
|
||||
// for broader tooltip activation area, equally increase padding and decrease margin
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
width: 100%;
|
||||
}
|
||||
.react-tooltip {
|
||||
@include tooltip-text;
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// arrow styles directly from react-tooltip-5 css
|
||||
.tooltip-arrow {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
[class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
[class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
[class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T {
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
[class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T {
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DisabledOptionTooltipWrapper";
|
||||
|
|
@ -7,6 +7,7 @@ import Select from "react-select";
|
|||
import dropdownOptionInterface from "interfaces/dropdownOption";
|
||||
import FormField from "components/forms/FormField";
|
||||
import Icon from "components/Icon";
|
||||
import DisabledOptionTooltipWrapper from "./DisabledOptionTooltipWrapper";
|
||||
|
||||
const baseClass = "dropdown";
|
||||
|
||||
|
|
@ -109,6 +110,22 @@ class Dropdown extends Component {
|
|||
};
|
||||
|
||||
renderOption = (option) => {
|
||||
if (option.disabledTooltipContent) {
|
||||
return (
|
||||
<DisabledOptionTooltipWrapper
|
||||
tipContent={option.disabledTooltipContent}
|
||||
>
|
||||
<div className={`${baseClass}__option`}>
|
||||
{option.label}
|
||||
{option.helpText && (
|
||||
<span className={`${baseClass}__help-text`}>
|
||||
{option.helpText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DisabledOptionTooltipWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`${baseClass}__option`}>
|
||||
{option.label}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
&__option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ describe("QuerySidePanel - component", () => {
|
|||
expect(platformCompatibility).toHaveTextContent(/chromeos/i);
|
||||
});
|
||||
|
||||
it("renders the correct number of columns including hiding columns set to hidden", () => {
|
||||
it("renders the correct number of columns including rendering hidden columns", () => {
|
||||
const { container } = render(
|
||||
<QuerySidePanel
|
||||
selectedOsqueryTable={createMockOsqueryTable()}
|
||||
|
|
@ -48,9 +48,21 @@ describe("QuerySidePanel - component", () => {
|
|||
);
|
||||
|
||||
const platformList = container.getElementsByClassName("column-list-item");
|
||||
expect(platformList.length).toBe(11); // 2 columns are set to hidden
|
||||
expect(platformList.length).toBe(13); // 2 of 13 columns are set to hidden but still show
|
||||
});
|
||||
it("renders the hidden column tooltip", async () => {
|
||||
render(
|
||||
<QuerySidePanel
|
||||
selectedOsqueryTable={createMockOsqueryTable()}
|
||||
onOsqueryTableSelect={() => noop}
|
||||
onClose={noop}
|
||||
/>
|
||||
);
|
||||
await fireEvent.mouseEnter(screen.getByText("type"));
|
||||
|
||||
const tooltip = screen.getByText(/Not returned in SELECT */i);
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
it("renders the platform specific column tooltip", async () => {
|
||||
render(
|
||||
<QuerySidePanel
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ const renderTooltip = (
|
|||
);
|
||||
};
|
||||
|
||||
const renderHiddenFootnote = () => {
|
||||
return (
|
||||
<span className={`${baseClass}__footnote`}>
|
||||
Not returned in SELECT * FROM {selectedTableName}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPlatformFootnotes = (columnPlatforms: OsqueryPlatform[]) => {
|
||||
let platformsCopy;
|
||||
switch (columnPlatforms.length) {
|
||||
|
|
@ -78,6 +86,7 @@ const renderTooltip = (
|
|||
<span className={`${baseClass}__footnote`}>{FOOTNOTES.required}</span>
|
||||
)}
|
||||
{column.requires_user_context && renderUserContextFootnote()}
|
||||
{column.hidden && renderHiddenFootnote()}
|
||||
{column.platforms && renderPlatformFootnotes(column.platforms)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,17 +35,15 @@ const baseClass = "query-table-columns";
|
|||
const QueryTableColumns = ({ columns }: IQueryTableColumnsProps) => {
|
||||
const { selectedOsqueryTable } = useContext(QueryContext);
|
||||
|
||||
const columnListItems = orderColumns(columns)
|
||||
.filter((column) => !column.hidden)
|
||||
.map((column) => {
|
||||
return (
|
||||
<ColumnListItem
|
||||
key={column.name}
|
||||
column={column}
|
||||
selectedTableName={selectedOsqueryTable.name}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const columnListItems = orderColumns(columns).map((column) => {
|
||||
return (
|
||||
<ColumnListItem
|
||||
key={column.name}
|
||||
column={column}
|
||||
selectedTableName={selectedOsqueryTable.name}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ export interface IDropdownOption {
|
|||
label: string | JSX.Element;
|
||||
value: string | number;
|
||||
premiumOnly?: boolean;
|
||||
disabledTooltipContent?: string | JSX.Element;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,8 +269,8 @@ export interface IHost {
|
|||
uuid: string;
|
||||
platform: string;
|
||||
osquery_version: string;
|
||||
orbit_version?: string;
|
||||
fleet_desktop_version?: string;
|
||||
orbit_version: string | null;
|
||||
fleet_desktop_version: string | null;
|
||||
os_version: string;
|
||||
build: string;
|
||||
platform_like: string; // TODO: replace with more specific union type
|
||||
|
|
@ -307,6 +307,7 @@ export interface IHost {
|
|||
display_text: string;
|
||||
display_name: string;
|
||||
target_type?: string;
|
||||
scripts_enabled: boolean | null;
|
||||
users: IHostUser[];
|
||||
device_users?: IDeviceUser[];
|
||||
munki?: IMunkiData;
|
||||
|
|
|
|||
|
|
@ -73,5 +73,7 @@ export const HOST_LINUX_PLATFORMS = [
|
|||
* the possible Linux-like platform values.
|
||||
*/
|
||||
export const isLinuxLike = (platform: string) => {
|
||||
return HOST_LINUX_PLATFORMS.includes(platform as any);
|
||||
return HOST_LINUX_PLATFORMS.includes(
|
||||
platform as typeof HOST_LINUX_PLATFORMS[number]
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ const LabelPage = ({
|
|||
setLabelValidator({
|
||||
name: "A label with this name already exists",
|
||||
});
|
||||
} else if (updateError.data.errors[0].reason.includes("built-in")) {
|
||||
setLabelValidator({
|
||||
name: "A built-in label with this name already exists",
|
||||
});
|
||||
} else if (
|
||||
updateError.data.errors[0].reason.includes(
|
||||
"Data too long for column 'name'"
|
||||
|
|
@ -150,6 +154,10 @@ const LabelPage = ({
|
|||
setLabelValidator({
|
||||
name: "A label with this name already exists",
|
||||
});
|
||||
} else if (updateError.data.errors[0].reason.includes("built-in")) {
|
||||
setLabelValidator({
|
||||
name: "A built-in label with this name already exists",
|
||||
});
|
||||
} else if (
|
||||
updateError.data.errors[0].reason.includes(
|
||||
"Data too long for column 'name'"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import paths from "router/paths";
|
|||
|
||||
import SideNav from "../components/SideNav";
|
||||
import ORG_SETTINGS_NAV_ITEMS from "./OrgSettingsNavItems";
|
||||
import { DeepPartial } from "./cards/constants";
|
||||
|
||||
interface IOrgSettingsPageProps {
|
||||
params: Params;
|
||||
|
|
@ -50,7 +51,7 @@ const OrgSettingsPage = ({ params, router }: IOrgSettingsPageProps) => {
|
|||
});
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
(formUpdates: Partial<IConfig>) => {
|
||||
(formUpdates: DeepPartial<IConfig>) => {
|
||||
if (!appConfig) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,23 +67,11 @@ const Advanced = ({
|
|||
// Formatting of API not UI
|
||||
const formDataToSubmit = {
|
||||
server_settings: {
|
||||
server_url: appConfig.server_settings.server_url || "",
|
||||
live_query_disabled: disableLiveQuery,
|
||||
enable_analytics: appConfig.server_settings.enable_analytics,
|
||||
query_reports_disabled: disableQueryReports,
|
||||
scripts_disabled: disableScripts,
|
||||
},
|
||||
smtp_settings: {
|
||||
enable_smtp: appConfig.smtp_settings?.enable_smtp || false,
|
||||
sender_address: appConfig.smtp_settings?.sender_address || "",
|
||||
server: appConfig.smtp_settings?.server || "",
|
||||
port: Number(appConfig.smtp_settings?.port),
|
||||
authentication_type: appConfig.smtp_settings?.authentication_type || "",
|
||||
user_name: appConfig.smtp_settings?.user_name || "",
|
||||
password: appConfig.smtp_settings?.password || "",
|
||||
enable_ssl_tls: appConfig.smtp_settings?.enable_ssl_tls || false,
|
||||
authentication_method:
|
||||
appConfig.smtp_settings?.authentication_method || "",
|
||||
domain,
|
||||
verify_ssl_certs: verifySSLCerts,
|
||||
enable_start_tls: enableStartTLS,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,15 @@ import YamlAce from "components/YamlAce";
|
|||
import CustomLink from "components/CustomLink";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import { IAppConfigFormProps, IAppConfigFormErrors } from "../constants";
|
||||
import { IAppConfigFormProps } from "../constants";
|
||||
|
||||
interface IAgentOptionsFormData {
|
||||
agentOptions?: string;
|
||||
}
|
||||
|
||||
interface IAgentOptionsFormErrors {
|
||||
agent_options?: string | null;
|
||||
}
|
||||
|
||||
const baseClass = "app-config-form";
|
||||
|
||||
|
|
@ -25,10 +33,10 @@ const Agents = ({
|
|||
}: IAppConfigFormProps): JSX.Element => {
|
||||
const { ADMIN_TEAMS } = paths;
|
||||
|
||||
const [formData, setFormData] = useState<any>({
|
||||
const [formData, setFormData] = useState<IAgentOptionsFormData>({
|
||||
agentOptions: agentOptionsToYaml(appConfig.agent_options),
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState<IAppConfigFormErrors>({});
|
||||
const [formErrors, setFormErrors] = useState<IAgentOptionsFormErrors>({});
|
||||
|
||||
const { agentOptions } = formData;
|
||||
|
||||
|
|
@ -37,7 +45,7 @@ const Agents = ({
|
|||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const errors: IAppConfigFormErrors = {};
|
||||
const errors: IAgentOptionsFormErrors = {};
|
||||
|
||||
if (agentOptions) {
|
||||
const { error: yamlError, valid: yamlValid } = validateYaml(agentOptions);
|
||||
|
|
@ -58,7 +66,7 @@ const Agents = ({
|
|||
evt.preventDefault();
|
||||
|
||||
// Formatting of API not UI and allows empty agent options
|
||||
const formDataToSubmit = agentOptions
|
||||
const formDataToSubmit: any = agentOptions
|
||||
? {
|
||||
agent_options: yaml.load(agentOptions),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const FleetDesktop = ({
|
|||
const onFormSubmit = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const formDataForAPI: Pick<IConfig, "fleet_desktop"> = {
|
||||
const formDataForAPI = {
|
||||
fleet_desktop: {
|
||||
transparency_url: formData.transparencyUrl,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -116,9 +116,6 @@ const Smtp = ({
|
|||
password: smtpPassword,
|
||||
enable_ssl_tls: smtpEnableSSLTLS,
|
||||
authentication_method: smtpAuthenticationMethod,
|
||||
domain: appConfig.smtp_settings?.domain || "",
|
||||
verify_ssl_certs: appConfig.smtp_settings?.verify_ssl_certs || false,
|
||||
enable_start_tls: appConfig.smtp_settings?.enable_start_tls,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ const Statistics = ({
|
|||
// Formatting of API not UI
|
||||
const formDataToSubmit = {
|
||||
server_settings: {
|
||||
server_url: appConfig.server_settings.server_url || "",
|
||||
live_query_disabled:
|
||||
appConfig.server_settings.live_query_disabled || false,
|
||||
enable_analytics: enableUsageStatistics,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ const WebAddress = ({
|
|||
const formDataToSubmit = {
|
||||
server_settings: {
|
||||
server_url: serverURL,
|
||||
live_query_disabled: appConfig.server_settings.live_query_disabled,
|
||||
enable_analytics: appConfig.server_settings.enable_analytics,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ import { IConfig } from "interfaces/config";
|
|||
|
||||
export const DEFAULT_TRANSPARENCY_URL = "https://fleetdm.com/transparency";
|
||||
|
||||
export type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export interface IAppConfigFormProps {
|
||||
appConfig: IConfig;
|
||||
isPremiumTier?: boolean;
|
||||
isUpdatingSettings?: boolean;
|
||||
handleSubmit: any;
|
||||
handleSubmit: (formUpdates: DeepPartial<IConfig>) => false | undefined;
|
||||
}
|
||||
|
||||
export interface IFormField {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
IEnrollSecret,
|
||||
IEnrollSecretsResponse,
|
||||
} from "interfaces/enroll_secret";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
|
||||
|
|
@ -1023,7 +1024,11 @@ const ManageHostsPage = ({
|
|||
renderFlash("success", "Successfully deleted label.");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
renderFlash("error", "Could not delete label. Please try again.");
|
||||
if (getErrorReason(error).includes("built-in")) {
|
||||
renderFlash("error", "Built-in labels can’t be modified or deleted.");
|
||||
} else {
|
||||
renderFlash("error", "Could not delete label. Please try again.");
|
||||
}
|
||||
} finally {
|
||||
setIsUpdatingLabel(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
|
||||
import {
|
||||
PLATFORM_LABEL_DISPLAY_NAMES,
|
||||
isPlatformLabelNameFromAPI,
|
||||
PolicyResponse,
|
||||
} from "utilities/constants";
|
||||
|
||||
|
|
@ -134,7 +135,9 @@ const HostsFilterBlock = ({
|
|||
if (selectedLabel) {
|
||||
const { description, display_text, label_type } = selectedLabel;
|
||||
const pillLabel =
|
||||
PLATFORM_LABEL_DISPLAY_NAMES[display_text] ?? display_text;
|
||||
(isPlatformLabelNameFromAPI(display_text) &&
|
||||
PLATFORM_LABEL_DISPLAY_NAMES[display_text]) ||
|
||||
display_text;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ import Select, { GroupBase, SelectInstance, components } from "react-select-5";
|
|||
import classnames from "classnames";
|
||||
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { PLATFORM_LABEL_DISPLAY_NAMES } from "utilities/constants";
|
||||
import {
|
||||
hasPlatformTypeIcon,
|
||||
isPlatformLabelNameFromAPI,
|
||||
PLATFORM_LABEL_DISPLAY_NAMES,
|
||||
PLATFORM_TYPE_ICONS,
|
||||
} from "utilities/constants";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import CustomLabelGroupHeading from "../CustomLabelGroupHeading";
|
||||
import { PLATFORM_TYPE_ICONS } from "./constants";
|
||||
import { createDropdownOptions, IEmptyOption, IGroupOption } from "./helpers";
|
||||
import CustomDropdownIndicator from "../CustomDropdownIndicator";
|
||||
|
||||
|
|
@ -39,23 +43,25 @@ const formatOptionLabel = (data: ILabel | IEmptyOption) => {
|
|||
const isLabel = "display_text" in data;
|
||||
const isPlatform = isLabel && data.type === "platform";
|
||||
|
||||
let labelText = isLabel ? data.display_text : data.label;
|
||||
let displayText = isLabel ? data.display_text : data.label;
|
||||
|
||||
// the display names for platform options are slightly different then the display_text
|
||||
// property, so we get the correct display name here
|
||||
if (isLabel && isPlatform) {
|
||||
labelText = PLATFORM_LABEL_DISPLAY_NAMES[data.display_text];
|
||||
if (isPlatformLabelNameFromAPI(data.display_text)) {
|
||||
displayText = PLATFORM_LABEL_DISPLAY_NAMES[data.display_text];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="option-label">
|
||||
{isPlatform && (
|
||||
{isLabel && hasPlatformTypeIcon(data.display_text) && (
|
||||
<Icon
|
||||
name={PLATFORM_TYPE_ICONS[data.display_text]}
|
||||
className="option-icon"
|
||||
/>
|
||||
)}
|
||||
<span>{labelText}</span>
|
||||
<span>{displayText}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,11 +8,4 @@ export const EMPTY_OPTION = {
|
|||
isDisabled: true,
|
||||
};
|
||||
|
||||
export const PLATFORM_TYPE_ICONS: Record<string, any> = {
|
||||
"All Linux": "linux",
|
||||
macOS: "darwin",
|
||||
"MS Windows": "windows",
|
||||
chrome: "chrome",
|
||||
};
|
||||
|
||||
export const FILTERED_LINUX = ["Red Hat Linux", "CentOS Linux", "Ubuntu Linux"];
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus={null}
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus={null}
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -81,6 +83,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostMdmEnrollmentStatus={null}
|
||||
doesStoreEncryptionKey
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -110,6 +113,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -138,6 +142,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -167,6 +172,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -196,6 +202,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -223,6 +230,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Non Fleet MDM"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -251,6 +259,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -283,6 +292,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="windows"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -310,6 +320,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -335,6 +346,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -361,6 +373,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -387,6 +400,7 @@ describe("Host Actions Dropdown", () => {
|
|||
hostStatus="online"
|
||||
hostMdmEnrollmentStatus="On (automatic)"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -418,6 +432,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -447,6 +462,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -476,6 +492,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Non Fleet MDM"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -507,6 +524,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -536,6 +554,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocking"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -565,6 +584,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -594,6 +614,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Non Fleet MDM"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -624,6 +645,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="locked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -655,6 +677,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -685,6 +708,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="windows"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -715,6 +739,7 @@ describe("Host Actions Dropdown", () => {
|
|||
mdmName="Fleet"
|
||||
hostPlatform="darwin"
|
||||
hostMdmDeviceStatus="unlocked"
|
||||
hostScriptsEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface IHostActionsDropdownProps {
|
|||
mdmName?: string;
|
||||
hostPlatform?: string;
|
||||
onSelect: (value: string) => void;
|
||||
hostScriptsEnabled: boolean | null;
|
||||
}
|
||||
|
||||
const HostActionsDropdown = ({
|
||||
|
|
@ -32,6 +33,7 @@ const HostActionsDropdown = ({
|
|||
doesStoreEncryptionKey,
|
||||
mdmName,
|
||||
hostPlatform = "",
|
||||
hostScriptsEnabled = false,
|
||||
onSelect,
|
||||
}: IHostActionsDropdownProps) => {
|
||||
const {
|
||||
|
|
@ -73,6 +75,7 @@ const HostActionsDropdown = ({
|
|||
doesStoreEncryptionKey: doesStoreEncryptionKey ?? false,
|
||||
isSandboxMode,
|
||||
hostMdmDeviceStatus,
|
||||
hostScriptsEnabled,
|
||||
});
|
||||
|
||||
// No options to render. Exit early
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ interface IHostActionConfigOptions {
|
|||
doesStoreEncryptionKey: boolean;
|
||||
isSandboxMode: boolean;
|
||||
hostMdmDeviceStatus: HostMdmDeviceStatusUIState;
|
||||
hostScriptsEnabled: boolean | null;
|
||||
}
|
||||
|
||||
const canTransferTeam = (config: IHostActionConfigOptions) => {
|
||||
|
|
@ -284,11 +285,39 @@ const filterOutOptions = (
|
|||
|
||||
const setOptionsAsDisabled = (
|
||||
options: IDropdownOption[],
|
||||
{ isHostOnline, isSandboxMode, hostMdmDeviceStatus }: IHostActionConfigOptions
|
||||
{
|
||||
isHostOnline,
|
||||
isSandboxMode,
|
||||
hostMdmDeviceStatus,
|
||||
hostScriptsEnabled,
|
||||
}: IHostActionConfigOptions
|
||||
) => {
|
||||
// Available tooltips for disabled options
|
||||
const disabledTooltipContent = (value: string | number) => {
|
||||
const tooltipAction: Record<string, string> = {
|
||||
runScript: "run scripts on",
|
||||
wipe: "wipe",
|
||||
lock: "lock",
|
||||
unlock: "unlock",
|
||||
};
|
||||
if (tooltipAction[value]) {
|
||||
return (
|
||||
<>
|
||||
To {tooltipAction[value]} this host, deploy the
|
||||
<br />
|
||||
fleetd agent with --enable-scripts
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!isHostOnline && value === "query") {
|
||||
return <>You can't query an offline host.</>;
|
||||
}
|
||||
};
|
||||
|
||||
const disableOptions = (optionsToDisable: IDropdownOption[]) => {
|
||||
optionsToDisable.forEach((option) => {
|
||||
option.disabled = true;
|
||||
option.disabledTooltipContent = disabledTooltipContent(option.value);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -305,6 +334,12 @@ const setOptionsAsDisabled = (
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!hostScriptsEnabled) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter((option) => option.value === "runScript")
|
||||
);
|
||||
}
|
||||
if (isSandboxMode) {
|
||||
optionsToDisable = optionsToDisable.concat(
|
||||
options.filter((option) => option.value === "transfer")
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ const HostDetailsPage = ({
|
|||
hostMdmEnrollmentStatus={host.mdm.enrollment_status}
|
||||
doesStoreEncryptionKey={host.mdm.encryption_key_available}
|
||||
mdmName={mdm?.name}
|
||||
hostScriptsEnabled={host.scripts_enabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -80,19 +80,25 @@ const SelectQueryModal = ({
|
|||
|
||||
const queriesCount = queriesFiltered.length;
|
||||
|
||||
const customQueryButton = () => {
|
||||
const renderDescription = (): JSX.Element => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => onQueryHostCustom()}
|
||||
variant="brand"
|
||||
className={`${baseClass}__custom-query-button`}
|
||||
>
|
||||
Create custom query
|
||||
</Button>
|
||||
<div className={`${baseClass}__description`}>
|
||||
Choose a query to run on this host
|
||||
{(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && (
|
||||
<>
|
||||
{" "}
|
||||
or{" "}
|
||||
<Button variant="text-link" onClick={onQueryHostCustom}>
|
||||
create your own query
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const results = (): JSX.Element => {
|
||||
const renderResults = (): JSX.Element => {
|
||||
if (queryErrors) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
|
@ -105,10 +111,6 @@ const SelectQueryModal = ({
|
|||
Expecting to see queries? Try again in a few seconds as the system
|
||||
catches up.
|
||||
</span>
|
||||
<div className="modal-cta-wrap">
|
||||
{(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) &&
|
||||
customQueryButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -131,55 +133,38 @@ const SelectQueryModal = ({
|
|||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${baseClass}__filter-create-wrapper`}>
|
||||
<div className={`${baseClass}__filter-queries`}>
|
||||
<InputFieldWithIcon
|
||||
name="query-filter"
|
||||
onChange={onFilterQueries}
|
||||
placeholder="Filter queries"
|
||||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
</div>
|
||||
{(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && (
|
||||
<div className={`${baseClass}__create-query`}>
|
||||
<span>OR</span>
|
||||
{customQueryButton()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<InputFieldWithIcon
|
||||
name="query-filter"
|
||||
onChange={onFilterQueries}
|
||||
placeholder="Filter queries"
|
||||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
<div>{queryList}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (queriesFilter && queriesCount === 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={`${baseClass}__filter-create-wrapper`}>
|
||||
<div className={`${baseClass}__filter-queries`}>
|
||||
<InputFieldWithIcon
|
||||
name="query-filter"
|
||||
onChange={onFilterQueries}
|
||||
placeholder="Filter queries"
|
||||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
</div>
|
||||
{(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && (
|
||||
<div className={`${baseClass}__create-query`}>
|
||||
<span>OR</span>
|
||||
{customQueryButton()}
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<div className={`${baseClass}__filter-queries`}>
|
||||
<InputFieldWithIcon
|
||||
name="query-filter"
|
||||
onChange={onFilterQueries}
|
||||
placeholder="Filter queries"
|
||||
value={queriesFilter}
|
||||
autofocus
|
||||
iconSvg="search"
|
||||
iconPosition="start"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__no-query-results`}>
|
||||
<div className={`${baseClass}__no-queries`}>
|
||||
<span className="info__header">
|
||||
No queries match the current search criteria.
|
||||
</span>
|
||||
|
|
@ -188,7 +173,7 @@ const SelectQueryModal = ({
|
|||
catches up.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
|
|
@ -198,10 +183,13 @@ const SelectQueryModal = ({
|
|||
<Modal
|
||||
title="Select a query"
|
||||
onExit={onCancel}
|
||||
className={`${baseClass}__modal`}
|
||||
width="xlarge"
|
||||
className={baseClass}
|
||||
width="large"
|
||||
>
|
||||
{results()}
|
||||
<>
|
||||
{renderDescription()}
|
||||
{renderResults()}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,76 +1,34 @@
|
|||
.select-query-modal {
|
||||
min-height: 400px;
|
||||
height: 80%;
|
||||
overflow: hidden;
|
||||
|
||||
.modal__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__no-queries {
|
||||
.info {
|
||||
&__header {
|
||||
margin: $pad-medium 0 $pad-medium 0;
|
||||
}
|
||||
&__data {
|
||||
margin: 0 0 $pad-large 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__modal {
|
||||
.info {
|
||||
&__header {
|
||||
display: block;
|
||||
color: $core-fleet-black;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__data {
|
||||
display: block;
|
||||
color: $core-fleet-black;
|
||||
font-weight: normal;
|
||||
font-size: $x-small;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.modal-cta-wrap {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__filter-create-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-direction: column;
|
||||
gap: $pad-medium;
|
||||
margin: $pad-large 0;
|
||||
}
|
||||
|
||||
&__filter-queries {
|
||||
flex-grow: 3;
|
||||
position: relative;
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
.info {
|
||||
&__header,
|
||||
&__data {
|
||||
display: block;
|
||||
color: $core-fleet-black;
|
||||
font-size: $x-small;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.fleeticon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
font-size: $medium;
|
||||
color: $ui-fleet-black-25;
|
||||
}
|
||||
}
|
||||
|
||||
&__create-query {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin: 15px;
|
||||
&__header {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__custom-query-button {
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
import React from "react";
|
||||
import { noop } from "lodash";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import { createMockHostSummary } from "__mocks__/hostMock";
|
||||
|
||||
import HostSummary from "./HostSummary";
|
||||
|
||||
describe("Host Actions Dropdown", () => {
|
||||
describe("Agent data", () => {
|
||||
it("with all info present, render Agent header with orbit_version and tooltip with all 3 data points", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary();
|
||||
const orbitVersion = summaryData.orbit_version as string;
|
||||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
const fleetdVersion = summaryData.fleet_desktop_version as string;
|
||||
|
||||
const { user } = render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionButtons={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
await user.hover(screen.getByText(new RegExp(orbitVersion, "i")));
|
||||
|
||||
expect(
|
||||
screen.getByText(new RegExp(osqueryVersion, "i"))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(new RegExp(fleetdVersion, "i"))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omit fleet desktop from tooltip if no fleet desktop version", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary({
|
||||
fleet_desktop_version: null,
|
||||
});
|
||||
const orbitVersion = summaryData.orbit_version as string;
|
||||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
|
||||
const { user } = render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionButtons={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
await user.hover(screen.getByText(new RegExp(orbitVersion, "i")));
|
||||
|
||||
expect(
|
||||
screen.getByText(new RegExp(osqueryVersion, "i"))
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary({
|
||||
platform: "chrome",
|
||||
osquery_version: "fleetd-chrome 1.2.0",
|
||||
});
|
||||
|
||||
const fleetdChromeVersion = summaryData.osquery_version as string;
|
||||
|
||||
const { user } = render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionButtons={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
await user.hover(screen.getByText(new RegExp(fleetdChromeVersion, "i")));
|
||||
expect(screen.queryByText("Osquery")).not.toBeInTheDocument();
|
||||
});
|
||||
it("for non-Chromebooks with no orbit_version, render Osquery header with osquery_version and no tooltip", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isPremiumTier: true,
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const summaryData = createMockHostSummary({
|
||||
orbit_version: null,
|
||||
});
|
||||
|
||||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
|
||||
const { user } = render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
onRefetchHost={noop}
|
||||
renderActionButtons={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Osquery")).toBeInTheDocument();
|
||||
await user.hover(screen.getByText(new RegExp(osqueryVersion, "i")));
|
||||
expect(screen.queryByText(/Orbit/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Fleet desktop/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -321,7 +321,7 @@ const HostSummary = ({
|
|||
if (platform === "chrome") {
|
||||
return <DataSet title="Agent" value={summaryData.osquery_version} />;
|
||||
}
|
||||
if (summaryData.orbit_version) {
|
||||
if (summaryData.orbit_version !== DEFAULT_EMPTY_CELL_VALUE) {
|
||||
return (
|
||||
<DataSet
|
||||
title="Agent"
|
||||
|
|
@ -332,7 +332,8 @@ const HostSummary = ({
|
|||
osquery: {summaryData.osquery_version}
|
||||
<br />
|
||||
Orbit: {summaryData.orbit_version}
|
||||
{summaryData.fleet_desktop_version && (
|
||||
{summaryData.fleet_desktop_version !==
|
||||
DEFAULT_EMPTY_CELL_VALUE && (
|
||||
<>
|
||||
<br />
|
||||
Fleet Desktop: {summaryData.fleet_desktop_version}
|
||||
|
|
|
|||
|
|
@ -775,57 +775,26 @@ const ManagePolicyPage = ({
|
|||
|
||||
const getAutomationsDropdownOptions = () => {
|
||||
const isAllTeams = teamIdForApi === undefined || teamIdForApi === -1;
|
||||
let calEventsLabel: React.ReactNode = "Calendar events";
|
||||
let disabledTooltipContent: React.ReactNode;
|
||||
if (!isPremiumTier) {
|
||||
const tipId = uniqueId();
|
||||
calEventsLabel = (
|
||||
<span>
|
||||
<div className="label-text" data-tooltip-id={tipId}>
|
||||
Calendar events
|
||||
</div>
|
||||
<ReactTooltip5
|
||||
id={tipId}
|
||||
place="left"
|
||||
positionStrategy="fixed"
|
||||
offset={24}
|
||||
opacity={1}
|
||||
disableStyleInjection
|
||||
classNameArrow="tooltip-arrow"
|
||||
>
|
||||
Available in Fleet Premium
|
||||
</ReactTooltip5>
|
||||
</span>
|
||||
);
|
||||
disabledTooltipContent = "Available in Fleet Premium.";
|
||||
} else if (isAllTeams) {
|
||||
const tipId = uniqueId();
|
||||
calEventsLabel = (
|
||||
<span>
|
||||
<div className="label-text" data-tooltip-id={tipId}>
|
||||
Calendar events
|
||||
</div>
|
||||
<ReactTooltip5
|
||||
id={tipId}
|
||||
place="left"
|
||||
positionStrategy="fixed"
|
||||
offset={24}
|
||||
opacity={1}
|
||||
disableStyleInjection
|
||||
classNameArrow="tooltip-arrow"
|
||||
>
|
||||
Select a team to manage
|
||||
<br />
|
||||
calendar events.
|
||||
</ReactTooltip5>
|
||||
</span>
|
||||
disabledTooltipContent = (
|
||||
<>
|
||||
Select a team to manage
|
||||
<br />
|
||||
calendar events.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: calEventsLabel,
|
||||
label: "Calendar events",
|
||||
value: "calendar_events",
|
||||
disabled: !isPremiumTier || isAllTeams,
|
||||
helpText: "Automatically reserve time to resolve failing policies.",
|
||||
disabledTooltipContent,
|
||||
},
|
||||
{
|
||||
label: "Other workflows",
|
||||
|
|
|
|||
|
|
@ -24,34 +24,6 @@
|
|||
.dropdown__help-text {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
.is-disabled * {
|
||||
color: $ui-fleet-black-25;
|
||||
.label-text {
|
||||
font-style: normal;
|
||||
// increase height to allow for broader tooltip activation area
|
||||
position: absolute;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
}
|
||||
.dropdown__help-text {
|
||||
// compensate for absolute label-text height
|
||||
margin-top: 20px;
|
||||
}
|
||||
.react-tooltip {
|
||||
@include tooltip-text;
|
||||
font-style: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// arrow styles directly from react-tooltip-5 css
|
||||
.tooltip-arrow {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
[class*="react-tooltip__place-left"] > .tooltip-arrow {
|
||||
transform: rotate(315deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.Select-control {
|
||||
margin-top: 0;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import React, { useCallback, useContext } from "react";
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import PATHS from "router/paths";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
|
||||
import { DEFAULT_POLICY, DEFAULT_POLICIES } from "pages/policies/constants";
|
||||
|
||||
import { IPolicyNew } from "interfaces/policy";
|
||||
import { SelectedPlatform } from "interfaces/platform";
|
||||
|
||||
import { PolicyContext } from "context/policy";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
export interface IAddPolicyModalProps {
|
||||
onCancel: () => void;
|
||||
|
|
@ -18,6 +22,32 @@ export interface IAddPolicyModalProps {
|
|||
teamName?: string;
|
||||
}
|
||||
|
||||
const CONTRIBUTE_TO_POLICIES_DOCS_URL =
|
||||
"https://www.fleetdm.com/contribute-to/policies";
|
||||
|
||||
const PLATFORM_FILTER_OPTIONS = [
|
||||
{
|
||||
label: "All platforms",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: "macOS",
|
||||
value: "darwin",
|
||||
},
|
||||
{
|
||||
label: "Windows",
|
||||
value: "windows",
|
||||
},
|
||||
{
|
||||
label: "Linux",
|
||||
value: "linux",
|
||||
},
|
||||
{
|
||||
label: "ChromeOS",
|
||||
value: "chrome",
|
||||
},
|
||||
];
|
||||
|
||||
const baseClass = "add-policy-modal";
|
||||
|
||||
const AddPolicyModal = ({
|
||||
|
|
@ -38,6 +68,9 @@ const AddPolicyModal = ({
|
|||
setDefaultPolicy,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
const [filteredPolicies, setFilteredPolicies] = useState(DEFAULT_POLICIES);
|
||||
const [platform, setPlatform] = useState<SelectedPlatform>("all");
|
||||
|
||||
const onAddPolicy = (selectedPolicy: IPolicyNew) => {
|
||||
setDefaultPolicy(true);
|
||||
teamName
|
||||
|
|
@ -70,7 +103,22 @@ const AddPolicyModal = ({
|
|||
teamId,
|
||||
]);
|
||||
|
||||
const policiesAvailable = DEFAULT_POLICIES.map((policy: IPolicyNew) => {
|
||||
const onPlatformFilterChange = (platformSelected: SelectedPlatform) => {
|
||||
if (platformSelected === "all") {
|
||||
setFilteredPolicies(DEFAULT_POLICIES);
|
||||
} else {
|
||||
// Note: Default policies currently map to a single platform
|
||||
const policiesFilteredByPlatform = DEFAULT_POLICIES.filter((policy) => {
|
||||
return policy.platform === platformSelected;
|
||||
});
|
||||
setFilteredPolicies(policiesFilteredByPlatform);
|
||||
}
|
||||
setPlatform(platformSelected);
|
||||
};
|
||||
|
||||
const filteredPoliciesCount = filteredPolicies.length;
|
||||
|
||||
const filteredPoliciesList = filteredPolicies.map((policy: IPolicyNew) => {
|
||||
return (
|
||||
<Button
|
||||
key={policy.key}
|
||||
|
|
@ -91,23 +139,44 @@ const AddPolicyModal = ({
|
|||
);
|
||||
});
|
||||
|
||||
const renderNoResults = () => {
|
||||
return (
|
||||
<>
|
||||
There are no results that match your filters.{" "}
|
||||
<CustomLink
|
||||
text="Everyone can contribute"
|
||||
url={CONTRIBUTE_TO_POLICIES_DOCS_URL}
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add a policy"
|
||||
onExit={onCancel}
|
||||
className={baseClass}
|
||||
width="xlarge"
|
||||
width="large"
|
||||
>
|
||||
<>
|
||||
<div className={`${baseClass}__create-policy`}>
|
||||
<div className={`${baseClass}__description`}>
|
||||
Choose a policy template to get started or{" "}
|
||||
<Button variant="text-link" onClick={onCreateYourOwnPolicyClick}>
|
||||
create your own policy
|
||||
</Button>
|
||||
.
|
||||
</div>
|
||||
<Dropdown
|
||||
value={platform}
|
||||
className={`${baseClass}__platform-dropdown`}
|
||||
options={PLATFORM_FILTER_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={onPlatformFilterChange}
|
||||
tableFilterDropdown
|
||||
/>
|
||||
<div className={`${baseClass}__policy-selection`}>
|
||||
{policiesAvailable}
|
||||
{filteredPoliciesCount > 0 ? filteredPoliciesList : renderNoResults()}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
.add-policy-modal {
|
||||
height: 80%;
|
||||
height: 90%;
|
||||
overflow: hidden;
|
||||
min-height: 460px;
|
||||
max-height: fit-content;
|
||||
|
||||
.modal__content {
|
||||
height: 100%;
|
||||
height: 90%;
|
||||
overflow: scroll;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
}
|
||||
&__create-policy {
|
||||
padding-top: 1.5rem;
|
||||
|
||||
.Select-multi-value-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
&__policy-selection {
|
||||
padding: $pad-large 0;
|
||||
height: 100%;
|
||||
.Select-menu-outer {
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
&__policy-name {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ const QueriesTable = ({
|
|||
? parseInt(queryParams?.inherited_page, 10)
|
||||
: 0)();
|
||||
|
||||
// Never set as state as URL is source of truth
|
||||
// Source of truth is state held within TableContainer. That state is initialized using URL
|
||||
// params, then subsquent updates to that state are pushed to the URL.
|
||||
const searchQuery = initialSearchQuery;
|
||||
const platform = initialPlatform;
|
||||
const page = isInherited ? initialInheritedPage : initialPage;
|
||||
|
|
@ -286,6 +287,7 @@ const QueriesTable = ({
|
|||
const searchable =
|
||||
!(queriesList?.length === 0 && searchQuery === "") && !isInherited;
|
||||
|
||||
const trimmedSearchQuery = searchQuery.trim();
|
||||
return columnConfigs && !isLoading ? (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
|
|
@ -293,11 +295,11 @@ const QueriesTable = ({
|
|||
resultsTitle="queries"
|
||||
columnConfigs={columnConfigs}
|
||||
data={queriesList}
|
||||
filters={{ name: isInherited ? "" : searchQuery }}
|
||||
filters={{ name: isInherited ? "" : trimmedSearchQuery }}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
|
||||
defaultSearchQuery={isInherited ? "" : searchQuery}
|
||||
defaultSearchQuery={isInherited ? "" : trimmedSearchQuery}
|
||||
defaultPageIndex={page}
|
||||
pageSize={DEFAULT_PAGE_SIZE}
|
||||
inputPlaceHolder="Search by name"
|
||||
|
|
|
|||
|
|
@ -270,7 +270,10 @@ const QueryDetailsPage = ({
|
|||
className={`${baseClass}__run`}
|
||||
variant="blue-green"
|
||||
onClick={() => {
|
||||
queryId && router.push(PATHS.LIVE_QUERY(queryId));
|
||||
queryId &&
|
||||
router.push(
|
||||
PATHS.LIVE_QUERY(queryId, currentTeamId)
|
||||
);
|
||||
}}
|
||||
disabled={disabledLiveQuery}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useQuery } from "react-query";
|
|||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import PATHS from "router/paths";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
|
|
@ -41,6 +42,13 @@ const RunQueryPage = ({
|
|||
}: IRunQueryPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
|
||||
const { currentTeamId } = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const { config } = useContext(AppContext);
|
||||
const {
|
||||
|
|
@ -78,8 +86,8 @@ const RunQueryPage = ({
|
|||
// Reroute users out of live flow when live queries are globally disabled
|
||||
if (disabledLiveQuery) {
|
||||
queryId
|
||||
? router.push(PATHS.QUERY_DETAILS(queryId))
|
||||
: router.push(PATHS.NEW_QUERY());
|
||||
? router.push(PATHS.QUERY_DETAILS(queryId, currentTeamId))
|
||||
: router.push(PATHS.NEW_QUERY(currentTeamId));
|
||||
}
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
|
|
@ -150,8 +158,8 @@ const RunQueryPage = ({
|
|||
const goToQueryEditor = useCallback(
|
||||
() =>
|
||||
queryId
|
||||
? router.push(PATHS.EDIT_QUERY(queryId))
|
||||
: router.push(PATHS.NEW_QUERY()),
|
||||
? router.push(PATHS.EDIT_QUERY(queryId, currentTeamId))
|
||||
: router.push(PATHS.NEW_QUERY(currentTeamId)),
|
||||
[]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import paths from "router/paths";
|
|||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
import React from "react";
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
import { IconNames } from "components/icons";
|
||||
|
||||
const { origin } = global.window.location;
|
||||
export const BASE_URL = `${origin}${URL_PREFIX}/api`;
|
||||
|
|
@ -184,6 +185,25 @@ export const DEFAULT_CAMPAIGN_STATE = {
|
|||
campaign: { ...DEFAULT_CAMPAIGN },
|
||||
};
|
||||
|
||||
const PLATFORM_LABEL_NAMES_FROM_API = [
|
||||
"All Hosts",
|
||||
"All Linux",
|
||||
"CentOS Linux",
|
||||
"macOS",
|
||||
"MS Windows",
|
||||
"Red Hat Linux",
|
||||
"Ubuntu Linux",
|
||||
"chrome",
|
||||
] as const;
|
||||
|
||||
type PlatformLabelNameFromAPI = typeof PLATFORM_LABEL_NAMES_FROM_API[number];
|
||||
|
||||
export const isPlatformLabelNameFromAPI = (
|
||||
s: string
|
||||
): s is PlatformLabelNameFromAPI => {
|
||||
return PLATFORM_LABEL_NAMES_FROM_API.includes(s as PlatformLabelNameFromAPI);
|
||||
};
|
||||
|
||||
export const PLATFORM_DISPLAY_NAMES: Record<string, OsqueryPlatform> = {
|
||||
darwin: "macOS",
|
||||
macOS: "macOS",
|
||||
|
|
@ -193,10 +213,13 @@ export const PLATFORM_DISPLAY_NAMES: Record<string, OsqueryPlatform> = {
|
|||
Linux: "Linux",
|
||||
chrome: "ChromeOS",
|
||||
ChromeOS: "ChromeOS",
|
||||
};
|
||||
} as const;
|
||||
|
||||
// as returned by the TARGETS API; based on display_text
|
||||
export const PLATFORM_LABEL_DISPLAY_NAMES: Record<string, string> = {
|
||||
export const PLATFORM_LABEL_DISPLAY_NAMES: Record<
|
||||
PlatformLabelNameFromAPI,
|
||||
string
|
||||
> = {
|
||||
"All Hosts": "All hosts",
|
||||
"All Linux": "Linux",
|
||||
"CentOS Linux": "CentOS Linux",
|
||||
|
|
@ -205,7 +228,7 @@ export const PLATFORM_LABEL_DISPLAY_NAMES: Record<string, string> = {
|
|||
"Red Hat Linux": "Red Hat Linux",
|
||||
"Ubuntu Linux": "Ubuntu Linux",
|
||||
chrome: "ChromeOS",
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const PLATFORM_LABEL_DISPLAY_ORDER = [
|
||||
"macOS",
|
||||
|
|
@ -214,9 +237,12 @@ export const PLATFORM_LABEL_DISPLAY_ORDER = [
|
|||
"Red Hat Linux",
|
||||
"Ubuntu Linux",
|
||||
"MS Windows",
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const PLATFORM_LABEL_DISPLAY_TYPES: Record<string, string> = {
|
||||
export const PLATFORM_LABEL_DISPLAY_TYPES: Record<
|
||||
PlatformLabelNameFromAPI,
|
||||
string
|
||||
> = {
|
||||
"All Hosts": "all",
|
||||
"All Linux": "platform",
|
||||
"CentOS Linux": "platform",
|
||||
|
|
@ -225,6 +251,28 @@ export const PLATFORM_LABEL_DISPLAY_TYPES: Record<string, string> = {
|
|||
"Red Hat Linux": "platform",
|
||||
"Ubuntu Linux": "platform",
|
||||
chrome: "platform",
|
||||
} as const;
|
||||
|
||||
export const PLATFORM_TYPE_ICONS: Record<
|
||||
Extract<
|
||||
PlatformLabelNameFromAPI,
|
||||
"All Linux" | "macOS" | "MS Windows" | "chrome"
|
||||
>,
|
||||
IconNames
|
||||
> = {
|
||||
"All Linux": "linux",
|
||||
macOS: "darwin",
|
||||
"MS Windows": "windows",
|
||||
chrome: "chrome",
|
||||
} as const;
|
||||
|
||||
export const hasPlatformTypeIcon = (
|
||||
s: string
|
||||
): s is Extract<
|
||||
PlatformLabelNameFromAPI,
|
||||
"All Linux" | "macOS" | "MS Windows" | "chrome"
|
||||
> => {
|
||||
return !!PLATFORM_TYPE_ICONS[s as keyof typeof PLATFORM_TYPE_ICONS];
|
||||
};
|
||||
|
||||
interface IPlatformDropdownOptions {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import {
|
|||
DEFAULT_GRAVATAR_LINK_DARK_FALLBACK,
|
||||
INITIAL_FLEET_DATE,
|
||||
PLATFORM_LABEL_DISPLAY_TYPES,
|
||||
isPlatformLabelNameFromAPI,
|
||||
} from "utilities/constants";
|
||||
import { IScheduledQueryStats } from "interfaces/scheduled_query_stats";
|
||||
import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
|
|
@ -220,10 +221,14 @@ export const formatFloatAsPercentage = (float?: number): string => {
|
|||
|
||||
const formatLabelResponse = (response: any): ILabel[] => {
|
||||
const labels = response.labels.map((label: ILabel) => {
|
||||
let labelType = "custom";
|
||||
if (isPlatformLabelNameFromAPI(label.display_text)) {
|
||||
labelType = PLATFORM_LABEL_DISPLAY_TYPES[label.display_text];
|
||||
}
|
||||
return {
|
||||
...label,
|
||||
slug: labelSlug(label),
|
||||
type: PLATFORM_LABEL_DISPLAY_TYPES[label.display_text] || "custom",
|
||||
type: labelType,
|
||||
target_type: "labels",
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid
|
|||
This reporting is performed to update the status of open or upcoming customer actions regarding the financial health of the opportunity. To complete the report:
|
||||
- Go to this [report folder](https://fleetdm.lightning.force.com/lightning/r/Folder/00lUG000000DstpYAC/view?queryScope=userFolders) in SFDC. The three reports will provide the data used in the report.
|
||||
- Copy the template below and paste it into the [#g-sales slack channel](https://fleetdm.slack.com/archives/C030A767HQV) and complete all "todos" using the data from Salesforce before sending.
|
||||
|
||||
```
|
||||
Weekly revenue report - [@`todo: CRO` and @`todo: CEO`]
|
||||
- Number accounts with outstanding balances = `todo`
|
||||
|
|
@ -376,6 +377,36 @@ Article creation begins with creation of an issue using the "Article request" te
|
|||
Check the "📃 Planned articles" column in [#g-demand board](https://app.zenhub.com/workspaces/g-demand-64e6c8e2d35c7f001a457b7f/board) and continue to work through steps in each event's issue.
|
||||
-->
|
||||
|
||||
### Order SWAG
|
||||
|
||||
**To order T-shirts:**
|
||||
|
||||
- Check [Postal](https://app.postal.io/items/postals) first and see if the warehouse has enough shirts.
|
||||
- Navigate to the [approved items page](https://app.postal.io/items/postals).
|
||||
- Hover over the shirt design and click on the airplane.
|
||||
- Click bulk send and choose one shirt size and the expected quantity of that particular shirt size.
|
||||
- Make sure the address matches the expected receiving address.
|
||||
- If the Postal warehouse can't fulfill the order or To order swag quickly:
|
||||
- Login to [https://www.rushordertees.com/my-account/login/) (saved in 1Password).
|
||||
- Choose Fleet logo design t-shirt under [my designs](https://www.rushordertees.com/my-account/designs/).
|
||||
- Order shirts based on the pre-determined number (~5% of total event attendees).
|
||||
- Submit the order. Ensure the address matches the expected receiving address.
|
||||
|
||||
**To order stickers:**
|
||||
|
||||
- Login to [StickerMule](https://www.stickermule.com/) (saved in 1Password).
|
||||
- Find the [brand kit](https://www.stickermule.com/studio/brand-kits) after logging in.
|
||||
- Click on the "Fleet Device Management" brand kit and order preapproved stickers from the templates.
|
||||
- Total sticker quantity should be ~10% of total event attendees.
|
||||
- Complete the checkout process. Ensure the address matches the expected receiving address.
|
||||
|
||||
**To order pens and sticky note pads**
|
||||
|
||||
- Pens and sticky note pads are ordered through Everything Branded.
|
||||
- Email our sales representative Jake William (saved in 1Password) to order any of the following:
|
||||
- [Javalina™ Metallic Stylus Pen](https://www.everythingbranded.com/product/javalina-metallic-stylus-pen-us-pat-8847930-9092077-350220)
|
||||
- [Sharpie Fine Point Markers](https://www.everythingbranded.com/product/sharpie-fine-point-332908)
|
||||
- [Custom sticky note pads](https://www.everythingbranded.com/product/custom-sticky-notes-585601) (design is in the StickerMule [brand kit](https://www.stickermule.com/studio/brand-kits))
|
||||
|
||||
## Rituals
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Purpose
|
||||
|
||||
Fleet Device Management Inc is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community).
|
||||
Fleet is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linkedin.com/feed/update/urn:li:activity:7120880290859728897/).
|
||||
|
||||
Fleet is dedicated to a comprehensive strategy against [whatever this is](https://chat.openai.com/share/e44ba6f3-b3ed-488a-a15e-a5a723f20c98):
|
||||
|
||||
|
|
@ -113,13 +113,13 @@ Ever wonder why there are 6 circles in the Fleet logo, but only 5 values? Behol
|
|||
## History
|
||||
|
||||
### 2014: Origins of osquery
|
||||
In 2014, our Cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io).
|
||||
In 2014, our cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io).
|
||||
|
||||
### 2016: Origins of Fleet v1.0
|
||||
A few years later, Zach, Mike Arpaia, and [Jason Meller](https://honest.security) founded [Kolide](https://kolide.com) and created Fleet: an open source platform that made it easier and more productive to use osquery in an enterprise setting.
|
||||
|
||||
### 2019: The growing community
|
||||
When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community took over maintenance of the open source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform.
|
||||
When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community [took over maintenance](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community) of the open-source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform.
|
||||
|
||||
### 2020: Fleet was incorporated
|
||||
Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/leadership#ceo-flaws), to found a new, independent company: Fleet Device Management Inc. In November 2020, we [announced](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) the transition and kicked off the logistics of moving the GitHub repository.
|
||||
|
|
@ -127,8 +127,10 @@ Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/
|
|||
### 2022: Millions of hosts
|
||||
Fleet raised its Series A funding round. The world now has at least 1.65 million computers and virtual hosts enrolled in Fleet, including enterprises, governments, startups, families, and hobbyist racks all over the world.
|
||||
|
||||
### 2024: Your last MDM migration
|
||||
Fleet announces [support for Windows and Linux devices](https://fleetdm.com/announcements/fleet-introduces-windows-mdm), enabling IT departments to consolidate tools and implement “zero trust” faster using a modern Mac-first MDM. Removing the need for proprietary alternatives like Jamf Pro, Jamf Protect, Microsoft Intune, Ivanti MobileIron, and Broadcom's recently acquired Workspace ONE (originally known as "Airwatch").
|
||||
### 2023: Your last MDM migration
|
||||
Fleet added support for [scripting and management capabilities](https://fleetdm.com/announcements/fleet-introduces-windows-mdm) on macOS, Windows, _and_ Linux devices, allowing IT departments to manage devices more consistently using modern tooling and best practices. This allowed many customers to simplify their management practices. In several cases, Fleet was also able to save customers several hundreds of thousands of dollars (USD) by cutting tool overlap across platforms such as Jamf, Airwatch, Intune, MobileIron, Nexthink, Tanium, Uptycs, and Rapid7.
|
||||
|
||||
<!-- 2024: and implement "zero trust" faster -->
|
||||
|
||||
> Still curious? Check out this [visualization of the Fleet repo over the years](https://www.linkedin.com/feed/update/urn:li:activity:7045068060168220672/) or listen to this [conversation between Zach and Mike Arpaia about the origin story of osquery](https://fleetdm.com/podcasts/the-future-of-device-management-ep1).
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ All invitations to meetings are welcomed, and quickly considering them is a top
|
|||
|
||||
> **Note:** Please do not add events to the CEO's calendar. **Events added directly to the CEO's calendar will be declined and removed.** Even if the CEO asks you to set up a meeting or add him to a call, please get scheduling help from the [Apprentice](https://www.fleetdm.com/handbook/digital-experience#team)).
|
||||
|
||||
To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BNAME%7D%C2%BB______________________). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved.
|
||||
To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BMeeting%20request%3A%20) at-mentioning the [Head of Digital Experience](https://www.fleetdm.com/handbook/digital-experience#team). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved.
|
||||
|
||||
- **Why the extra step?** There are not enough hours in the day for the CEO to accept every request to meet, so [we have to prioritize](https://www.fleetdm.com/handbook/digital-experience#process-the-ceos-calendar).
|
||||
- **Self-service scheduling:** Unlike other team members, who you can schedule with by simply dropping an event on their calendar unless requested directly from Mike, please do not directly schedule a meeting onto the CEO's calendar without using this process to confirm with the Apprentice first.
|
||||
|
|
@ -130,16 +130,28 @@ This works because every Fleetie grants edit access to everyone else at Fleet as
|
|||
|
||||
### Shared calendars
|
||||
Team calendars are the primary source for sprint rituals; they facilitate the execution of each sprint.
|
||||
|
||||
Looking to add, change, or remove a shared calendar? [Create an issue for the CEO](https://fleetdm.com/handbook/digital-experience#contact-us) and the appropriate DRI will reply with feedback.
|
||||
|
||||
|
||||
## Skip-level 1:1 meetings
|
||||
|
||||
Fleet uses skip-level 1:1 meetings as a recurring pulse check to encourage [valuable personal and departmental feedback](https://fleetdm.com/handbook/company/communications#performance-feedback) across the org. This helps the leadership at Fleet run an effective company with a great team, good alignment, and quick decisions. To schedule a skip-Level 1:1:
|
||||
1. Create a copy of the ["Skip-level 1:1 agenda template"](https://docs.google.com/document/d/191wiy-_a9XBMndLlM97iOwUF6a-0PtkbboQ2FCUIy6w/copy) and rename the document "🧑🚀 YOUR_GITHUB_USER_NAME : SUPERVISOR_GITHUB_USER_NAME".
|
||||
2. [Schedule a meeting](https://fleetdm.com/handbook/company/communications#internal-meeting-scheduling) with your manager's supervisor and title the calendar event by copying your skip-level agenda title and appending "[no shadows]" to the end (this tells other team members that this is a private conversation).
|
||||
|
||||
> **If you're scheduling with the CEO** please [get help](https://fleetdm.com/handbook/company/communications#schedule-time-with-the-ceo) before adding events to the calendar.
|
||||
|
||||
3. Link the skip-level agenda in the calendar event description before saving.
|
||||
|
||||
|
||||
### Zoom
|
||||
|
||||
We use [Zoom](https://zoom.us) for virtual meetings at Fleet, and it is important that every team member feels comfortable hosting, joining, and scheduling Zoom meetings.
|
||||
By default, Zoom settings are the same for all Fleet team members, but you can change your personal settings on your [profile settings](https://zoom.us/profile/setting) page.
|
||||
Settings that have a lock icon next to them have been locked by an administrator and cannot be changed. Zoom administrators can change settings for all team members on the [account settings page](https://zoom.us/account/setting) or for individual accounts on the [user management page](https://zoom.us/account/user#/).
|
||||
|
||||
### Recording meetings
|
||||
|
||||
Capturing video from meetings with customers, prospects, and community members outside the company is an important part of building world-class sales and customer success teams and is a widespread practice across the industry. At Fleet, we use Gong to capture Zoom meetings and share them company-wide. If a team member with a Gong license attends certain meetings, generally those with at least one person from outside of Fleet in attendance.
|
||||
- While some Fleeties may have a Gong seat that is necessary in their work, the typical use case at Fleet is for employees on the company's sales, customer success, or customer support teams.
|
||||
- You should be notified anytime you join a recorded call with an audio message announcing "this meeting is being recorded" or "recording in progress." To stop a recording, the host of the call can press "Stop."
|
||||
|
|
|
|||
|
|
@ -166,23 +166,25 @@ If the consultant is international, you will also provide:
|
|||
|
||||
> To update a consultant's fee, [submit an issue to BizOps](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-business-operations&projects=&title=Update%20consultant%20fee) with the consultant's name and new hourly rate.
|
||||
|
||||
### Advisor
|
||||
### Adding an advisor
|
||||
|
||||
#### Adding an advisor
|
||||
Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement"
|
||||
template.
|
||||
- Send the advisor agreement. To send a new advisor agreement, you'll need the new advisor's name and the number of shares they are offered.
|
||||
- Once you send the agreement, locate an existing empty row and available ID in ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) and enter the new advisor's information.
|
||||
>**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"*
|
||||
First:
|
||||
|
||||
#### Finalizing a new advisor
|
||||
- Update the ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm).
|
||||
- Update "Equity plan" to reflect updated status and equity grant for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual.
|
||||
Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement" template.
|
||||
- Update the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483)
|
||||
>**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"*
|
||||
- Update the "Equity plan" sheet (which should have been automatically updated after updating "Advisors" thanks to the global unique IDs next to each row which are used to connect the spreadsheets) to reflect the default number of shares for advisor equity grants.
|
||||
- Send the advisor agreement [through Docusign](https://apps.docusign.com/send/templates?view=shared&folder=0482b0fd-a752-41be-93a0-185e2fb7ef54) using the CEO's account, pulling the advisor's email address from a recent calendar event on the CEO's calendar.
|
||||
- Complete the first step of signing, which involves filling in the number of shares.
|
||||
- Then wait for the advisor to sign. (Fleet's CEO will sign after that.)
|
||||
|
||||
### Core team member
|
||||
This section is about creating a core team member role, and the hiring process for a new core team member, or Fleetie.
|
||||
Then, to finalize a new advisor after signing is complete:
|
||||
- Schedule quarterly recurring 1h meeting between the CEO and the advisor, with 30m of recurring prep scheduled back to back ahead of the meeting.
|
||||
- Update the status columns in the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm).
|
||||
- Update "Equity plan" status columns to reflect updated status for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual.
|
||||
|
||||
#### Creating a new position
|
||||
|
||||
### Creating a new position
|
||||
|
||||
Want to hire? Use these steps to hire a [fleetie, not a consultant](https://fleetdm.com/handbook/company/leadership#who-isnt-a-consultant). Here's how to open up a new position on the core team:
|
||||
|
||||
|
|
@ -236,7 +238,7 @@ A completed open position entry should look something like this:
|
|||
|
||||
- _**Why bother with approvals?** We avoid cancelling or significantly changing a role after opening it. It hurts candidates too much. Instead, get the position approved first, before you start recruiting and interviewing. This gives you a sounding board and avoids misunderstandings._
|
||||
|
||||
#### Approving a new position
|
||||
### Approving a new position
|
||||
When review is requested on a proposal to open a new position, the 🐈⬛ CEO will complete the following steps when reviewing the pull request:
|
||||
|
||||
1. **Consider role and reporting structure:** Confirm the new row in "Fleeties" has a manager, job title, and department, that it doesn't have any corrupted spreadsheet formulas or formatting, and that the start date is set to the first Monday of the next month.
|
||||
|
|
@ -257,7 +259,7 @@ When review is requested on a proposal to open a new position, the 🐈⬛ CE
|
|||
|
||||
> _**Note:** Most columns of the "Equity plan" are updated automatically when "Fleeties" is, based on the unique identifier of each row, like `🧑🚀890`. (Advisors have their own flavor of unique IDs, such as `🦉755`, which are defined in ["Advisors and investors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit).)_
|
||||
|
||||
#### Recruiting
|
||||
### Recruiting
|
||||
Fleet accepts job applications, but the company does not list positions on general purpose job boards. This prevents us being overwhelmed with candidates so we can fulfill our goal of responding promptly to every applicant.
|
||||
|
||||
This means that outbound recruiting, 3rd party recruiters, and references from team members are important aspect of the company's hiring strategy. Fleet's CEO is happy to assist with outreach, intros, and recruiting strategy for candidates.
|
||||
|
|
@ -272,15 +274,16 @@ When a candidate clicks applies for a job at Fleet, they are taken to a generic
|
|||
#### Candidate correspondence email templates
|
||||
Fleet uses [certain email templates](https://docs.google.com/document/d/1E_gTunZBMNF4AhsOFuDVi9EnvsIGbAYrmmEzdGmnc9U) when responding to candidates. This helps us live our value of [🔴 empathy](https://fleetdm.com/handbook/company#empathy) and helps the company meet the aspiration of replying to all applications within one business day.
|
||||
|
||||
#### Hiring restrictions
|
||||
### Hiring restrictions
|
||||
|
||||
##### Incompatible former employers
|
||||
#### Incompatible former employers
|
||||
Fleet maintains a list of companies with whom Fleet has do-not-solicit terms that prevents us from making offers to employees of these companies. The list is in the Do Not Solicit tab of the [BizOps spreadsheet](https://docs.google.com/spreadsheets/d/1lp3OugxfPfMjAgQWRi_rbyL_3opILq-duHmlng_pwyo/edit#gid=0).
|
||||
|
||||
##### Incompatible locations
|
||||
#### Incompatible locations
|
||||
Fleet is unable to hire team members in some countries. See [this internal document](https://docs.google.com/document/d/1jHHJqShIyvlVwzx1C-FB9GC74Di_Rfdgmhpai1SPC0g/edit) for the list.
|
||||
|
||||
#### Interviewing
|
||||
|
||||
### Interviewing
|
||||
> TODO: Rewrite this section for the hiring manager as our audience.
|
||||
|
||||
We're glad you're interested in joining the team!
|
||||
|
|
@ -302,7 +305,7 @@ Here are the steps hiring managers follow to get an offer out to a candidate:
|
|||
1. **Call references:** Before proceeding, make sure you have 2-5+ references. Ask the candidate for at least 2-5+ references and contact each reference in parallel using the instructions in [Fleet's reference check template](https://docs.google.com/document/d/1LMOUkLJlAohuFykdgxTPL0RjAQxWkypzEYP_AT-bUAw/edit?usp=sharing). Be respectful and keep these calls very short.
|
||||
2. **Add to team database:** Update the [Fleeties](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) doc to accurately reflect the candidate's:
|
||||
- Start date
|
||||
> _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._
|
||||
> _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._
|
||||
- First and last name
|
||||
- Preferred pronoun _("them", "her", or "him")_
|
||||
- LinkedIn URL _(If the fleetie does not have a LinkedIn account, enter `N/A`)_
|
||||
|
|
@ -311,11 +314,10 @@ Here are the steps hiring managers follow to get an offer out to a candidate:
|
|||
4. **Confirm intent to offer:** Compile feedback about the candidate into a single document and share that document (the "interview packet") with the Head of Business Operations via Google Drive. _This will be interpreted as a signal that you are ready for them to make an offer to this candidate._
|
||||
- _Compile feedback into a single doc:_ Include feedback from interviews, reference checks, and challenge submissions. Include any other notes you can think of offhand, and embed links to any supporting documents that were impactful in your final decision-making, such as portfolios or challenge submissions.
|
||||
- _Share_ this single document with the Head of Business Operations via email.
|
||||
- Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line. For example, you could title it:
|
||||
>Why hire Jane Doe ("Train Conductor") - 2023-03-21
|
||||
- When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate.
|
||||
- Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line (e.g. "_Why hire Jane Doe ("Train Conductor") - 2023-03-21_").
|
||||
- When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate.
|
||||
|
||||
#### Making an offer
|
||||
### Making an offer
|
||||
After receiving the interview packet, the Head of Business Operations uses the following steps to make an offer:
|
||||
|
||||
<!-- For future use: some ready-to-go language around rebencharking compensation for cost of living: https://github.com/fleetdm/fleet/pulls/13499 -->
|
||||
|
|
@ -399,12 +401,19 @@ From time to time, someone's job title changes. To do this, Business Operations
|
|||
2. If there is a compensation change, update "Equity plan". Use the first day of a month as the date, and enter this in the corresponding column.
|
||||
3. If applicable, schedule the change in the appropriate payroll system. (Don't worry about updating job titles in the payroll system.)
|
||||
|
||||
## Performance feedback
|
||||
## Delivering performance feedback
|
||||
When it comes to performance feedback, [speak freely](https://fleetdm.com/handbook/company#openness), sooner, and provide an explicit example of the behavior you observed and the impact it had.
|
||||
|
||||
1. Deliver negative feedback privately whenever possible, and be constructive not punitive. Celebrate positive feedback publicly.
|
||||
2. Performance mangement is a part of every 1:1 document. Start each 1:1 by delivering performance feedback.
|
||||
3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc.
|
||||
2. Performance management is a part of every 1:1 document. Start each 1:1 by delivering performance feedback.
|
||||
3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc.
|
||||
|
||||
|
||||
#### Stubs
|
||||
|
||||
##### Performance feedback
|
||||
Please see 📖[handbook/company/leadership#delivering-performance-feedback](https://fleetdm.com/handbook/company/leadership#delivering-performance-feedback).
|
||||
|
||||
|
||||
<meta name="maintainedBy" value="mikermcneil">
|
||||
<meta name="title" value="🛠️ Leadership">
|
||||
|
|
|
|||
|
|
@ -87,6 +87,6 @@
|
|||
- ✍️ Familiarity with shell scripting, Python, Powershell, and using Terminal to execute commands or run scripts, and other line of business applications.
|
||||
- 🟣 Openness: Speak freely. Interrupt and be interrupted. Give pointed and respectful feedback, even when you disagree.
|
||||
- 🔴 Empathy: You should demonstrate empathy by keenly understanding and addressing customer concerns with genuine compassion.
|
||||
- ➕ Bonus: Familiarity with osquery, MYSql, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams.
|
||||
- ➕ Bonus: Familiarity with osquery, MySQL, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ All unreleased bugs are addressed before publishing a release. Released bugs tha
|
|||
- Introduces a security vulnerability
|
||||
|
||||
### Notify the community about a critical bug
|
||||
We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the #help-product-design channel with the filed bug.
|
||||
We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the `#g-mdm` or `#g-endpoint-ops` channel with the filed bug.
|
||||
|
||||
If the bug finder is not a Fleetie (e.g., a member of the community), then whoever sees the critical bug should raise the alarm. Note that the bug finder here is NOT necessarily the **first** person who sees the bug. If you come across a bug you think is critical, but it has not been escalated, raise the alarm!
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ The only exceptions are:
|
|||
- _Confidential:_ [`fleetdm/confidential`](https://github.com/fleetdm/confidential)
|
||||
- _Classified (¶¶):_ [`fleetdm/classified`](https://github.com/fleetdm/classified)
|
||||
3. **GitHub Actions:** Since GitHub requires GitHub Actions to live in dedicated repositories in order to submit them to the marketplace, Fleet uses a separate repo for publishing [GitHub Actions designed for other people to deploy and use (and/or fork)](https://github.com/fleetdm/fleet-mdm-gitops).
|
||||
4. **Software vulnerabilities:** Since GitHub only allows one latest release per repository, we currently maintain two repositories to host our CVE/CPE database releases:
|
||||
- _vulnerabilities:_ [`fleetdm/vulnerabilities`](https://github.com/fleetdm/vulnerabilities)
|
||||
- _nvd:_ [`fleetdm/nvd`](https://github.com/fleetdm/nvd)
|
||||
|
||||
|
||||
Besides the exceptions above, Fleet does not use any other repositories. Other GitHub repositories in `fleetdm` should be archived and made private.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ This handbook page details processes specific to working [with](#contact-us) and
|
|||
## Responsibilities
|
||||
The customer success department is directly responsible for ensuring that customers and community members of Fleet achieve their desired outcomes with Fleet products and services.
|
||||
|
||||
|
||||
### Assign a customer codename
|
||||
Occasionally, we will need to track public issues for customers who wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer.
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ Locate the relevant issue or create it if it doesn't already exist (to avoid dup
|
|||
- Make sure the issue has a "customer request" label or "customer-codename" label.
|
||||
- Occasionally, we will need to track public issues for customers that wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer.
|
||||
- "+" prefixed labels (e.g., "+more info please") indicate we are waiting on an answer from an external community member who does not work at Fleet or that no further action is needed from the Fleet team until an external community member, who doesn't work at Fleet, replies with a comment. At this point, our bot will automatically remove the +-prefixed label.
|
||||
- 1. Required details that will help speed up time to resolution:
|
||||
1. Required details that will help speed up time to resolution:
|
||||
- Fleet server version
|
||||
- Agent version
|
||||
- Osquery or fleetd?
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@
|
|||
frequency: "Triweekly"
|
||||
description: "Check-in before the 🗣️ Product Feature Requests meeting to make sure that all information necessary has been gathered before presenting customer requests and feedback to the Product team."
|
||||
moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more."
|
||||
dri: "patagonia121"
|
||||
dri: "nonpunctual"
|
||||
-
|
||||
task: "Present customer requests at feature fest"
|
||||
startedOn: "2024-02-15"
|
||||
frequency: "Triweekly"
|
||||
description: "Present and advocate for requests and ideas brought to Fleet's attention by customers that are interesting from a product perspective."
|
||||
moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more."
|
||||
dri: "patagonia121"
|
||||
dri: "nonpunctual"
|
||||
-
|
||||
task: "Communicate release notes to stakeholders"
|
||||
startedOn: "2024-02-21"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue