Merge branch 'main' into feat-resend-config-profile

This commit is contained in:
Gabriel Hernandez 2024-04-15 11:35:14 +01:00
commit 5df04c8cca
222 changed files with 4551 additions and 1532 deletions

View file

@ -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>

View file

@ -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.

View 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}```"
}
}
]
}

View file

@ -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 }}

View file

@ -24,7 +24,7 @@ defaults:
shell: bash
env:
FLEET_DESKTOP_VERSION: 1.22.0
FLEET_DESKTOP_VERSION: 1.23.0
permissions:
contents: read

View file

@ -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

View file

@ -7,6 +7,7 @@
"redhat.vscode-yaml",
"dbaeumer.vscode-eslint",
"firefox-devtools.vscode-firefox-debug",
"editorconfig.editorconfig"
"editorconfig.editorconfig",
"timonwong.shellcheck"
]
}

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1 @@
- UI revamp: Run query on an online host

View file

@ -0,0 +1 @@
* Add filters by platform to select a new policy modal

View 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.

View file

@ -0,0 +1,2 @@
* Ignore leading and trailing whitespace when filtering Fleet entities by name

View 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.

View file

@ -0,0 +1 @@
In GET fleet/hosts/:id response, added orbit_version, fleet_desktop_version, and scripts_enabled fields.

View 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

View file

@ -0,0 +1 @@
* Fix bug where query retrieving bitlocker info from windows server wouldn't return

View 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)

View 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.

View file

@ -0,0 +1 @@
- Updated label endpoints and UI to prevent creating, updating, or deleting built-in labels.

View 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.

View 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.

View file

@ -0,0 +1 @@
* Fixed an issue with automatic release of the device after setup when a DDM profile is pending.

View file

@ -0,0 +1 @@
* Fixed license checks to allow migration and restoring DEP devices during trial

View file

@ -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

View file

@ -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:

View file

@ -1457,9 +1457,9 @@ func TestGetQuery(t *testing.T) {
Platform: "linux",
Logging: "differential",
}, nil
} else {
return nil, &notFoundError{}
}
return nil, &notFoundError{}
}
expectedYaml := `---

View file

@ -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, &notFoundError{}
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, &notFoundError{}
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, &notFoundError{}
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.`},

View file

@ -18,6 +18,9 @@
"uuid": "",
"platform": "",
"osquery_version": "",
"orbit_version": null,
"fleet_desktop_version": null,
"scripts_enabled": null,
"os_version": "",
"build": "",
"platform_like": "",

View file

@ -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

View file

@ -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": "",

View file

@ -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": "",

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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. |

View 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.

View file

@ -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).**
![Example Okta IdP Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-idp-setup.png)
## 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
```
![Where to find SSO links for Fleet](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-retrieve-links.png)
- _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
![The Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-1.png)
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.
![Adding a new app to Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-2.png)
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.
![Download metadata](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-3.png)
- _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
![Configuring the service provider details in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-5.png)
6. Click **Finish**.
![Finish configuring the new SAML app in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-6.png)
7. Click the down arrow on the **User access** section of the app details page.
![The new SAML app's details page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-7.png)
8. Check **ON for everyone**. Click **Save**.
![The new SAML app's service status page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-8.png)
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.
![Example SSO Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/sso-setup.png)
## 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
![Example Okta IDP Configuration](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-idp-setup.png)
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:
![Where to find SSO links for Fleet](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/okta-retrieve-links.png)
> 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_.
![The Google Workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-1.png)
2. Enter `Fleet` for the _App name_ and click _Continue_.
![Adding a new app to Google workspace admin dashboard](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-2.png)
3. Click _Download Metadata_, saving the metadata to your computer. Click _Continue_.
![Download metadata](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-3.png)
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.
![Fleet's SAML single sign on options page](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-4.png)
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.
![Configuring the service provider details in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-5.png)
6. Click _Finish_.
![Finish configuring the new SAML app in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-6.png)
7. Click the down arrow on the _User access_ section of the app details page.
![The new SAML app's details page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-7.png)
8. Check _ON for everyone_. Click _Save_.
![The new SAML app's service status page in Google Workspace](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/google-sso-configuration-step-8.png)
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">

View file

@ -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_
| &nbsp;&nbsp;&nbsp;&nbsp;enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. |
| &nbsp;&nbsp;&nbsp;&nbsp;destination_url | string | body | The URL to deliver the webhook requests to. |
| &nbsp;&nbsp;&nbsp;&nbsp;policy_ids | array | body | List of policy IDs to enable failing policies webhook. |
| &nbsp;&nbsp;host_status_webhook | object | body | Host status webhook settings. |
| &nbsp;&nbsp;&nbsp;&nbsp;enable_host_status_webhook | boolean | body | Whether or not the host status webhook is enabled. |
| &nbsp;&nbsp;&nbsp;&nbsp;destination_url | string | body | The URL to deliver the webhook request to. |
| &nbsp;&nbsp;&nbsp;&nbsp;host_percentage | integer | body | The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. |
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;&nbsp;&nbsp;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). |
| &nbsp;&nbsp;jira | array | body | Jira integrations configuration. |
@ -8602,6 +8645,10 @@ _Available in Fleet Premium_
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. |
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;google_calendar | object | body | Google Calendar integration settings. |
| &nbsp;&nbsp;&nbsp;&nbsp;enable_calendar_events | boolean | body | Whether or not calendar events are enabled for this team. |
| &nbsp;&nbsp;&nbsp;&nbsp;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. |
| &nbsp;&nbsp;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. |
| &nbsp;&nbsp;host_expiry_window | integer | body | If a host has not communicated with Fleet in the specified number of days, it will be removed. |

View file

@ -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">

View file

@ -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]
}
}
```

View file

@ -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.

View file

@ -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

View file

@ -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
}
}

View file

@ -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 });
});

View file

@ -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 };

View file

@ -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)))

View file

@ -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

View file

@ -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;

View file

@ -193,7 +193,7 @@ const TableContainer = <T,>({
);
const onSearchQueryChange = (value: string) => {
setSearchQuery(value);
setSearchQuery(value.trim());
};
const hasPageIndexChangedRef = useRef(false);

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./DisabledOptionTooltipWrapper";

View file

@ -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}

View file

@ -51,6 +51,7 @@
&__option {
display: flex;
flex-direction: column;
width: 100%;
}
&__help-text {

View file

@ -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

View file

@ -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)}
</>
);

View file

@ -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}>

View file

@ -11,4 +11,5 @@ export interface IDropdownOption {
label: string | JSX.Element;
value: string | number;
premiumOnly?: boolean;
disabledTooltipContent?: string | JSX.Element;
}

View file

@ -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;

View file

@ -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]
);
};

View file

@ -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'"

View file

@ -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;
}

View file

@ -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,

View file

@ -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),
}

View file

@ -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,
},

View file

@ -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,
},
};

View file

@ -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,
},
};

View file

@ -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,
},
};

View file

@ -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 {

View file

@ -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 cant be modified or deleted.");
} else {
renderFlash("error", "Could not delete label. Please try again.");
}
} finally {
setIsUpdatingLabel(false);
}

View file

@ -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 (
<>

View file

@ -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>
);
};

View file

@ -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"];

View file

@ -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
/>
);

View file

@ -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

View file

@ -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&apos;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")

View file

@ -675,6 +675,7 @@ const HostDetailsPage = ({
hostMdmEnrollmentStatus={host.mdm.enrollment_status}
doesStoreEncryptionKey={host.mdm.encryption_key_available}
mdmName={mdm?.name}
hostScriptsEnabled={host.scripts_enabled}
/>
);
};

View file

@ -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>
);
};

View file

@ -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;
}
}

View file

@ -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();
});
});
});

View file

@ -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}

View file

@ -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",

View file

@ -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;

View file

@ -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>

View file

@ -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 {

View file

@ -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"

View file

@ -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}
>

View file

@ -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)),
[]
);

View file

@ -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 {

View file

@ -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",
};
});

View file

@ -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

View file

@ -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).

View file

@ -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."

View file

@ -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">

View file

@ -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.

View file

@ -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 dont 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 dont 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!

View file

@ -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.

View file

@ -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?

View file

@ -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