mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Merge branch 'main' into feat-create-policies-from-fleet-apps
This commit is contained in:
commit
c1719478f1
80 changed files with 1539 additions and 353 deletions
210
.github/workflows/check-tuf-timestamps.yml
vendored
210
.github/workflows/check-tuf-timestamps.yml
vendored
|
|
@ -3,10 +3,10 @@ name: Check TUF timestamps
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/check-tuf-timestamps.yml'
|
||||
- ".github/workflows/check-tuf-timestamps.yml"
|
||||
workflow_dispatch: # Manual
|
||||
schedule:
|
||||
- cron: '0 10,22 * * *'
|
||||
- cron: "0 10,22 * * *"
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
|
|
@ -29,81 +29,151 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check remote timestamp.json file
|
||||
id: check_timestamp
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/timestamp.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 4 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
- name: Check remote timestamp.json file
|
||||
id: check_timestamp
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/timestamp.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 4 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "timestamp_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "timestamp_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "timestamp_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "timestamp_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Check remote root.json file
|
||||
id: check_root
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/root.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 30 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
- name: Check remote snapshot.json file
|
||||
id: check_snapshot
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/snapshot.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 30 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "root_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "root_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "snapshot_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "snapshot_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Check remote targets.json file
|
||||
id: check_targets
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/targets.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 30 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
|
||||
- name: Slack Timestamp Notification
|
||||
if: ${{ steps.check_timestamp.outputs.timestamp_warn == 'true' }}
|
||||
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": "⚠️ TUF timestamp.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "targets_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "targets_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Check remote root.json file
|
||||
id: check_root
|
||||
run: |
|
||||
expires=$(curl -s http://tuf.fleetctl.com/root.json | jq -r '.signed.expires' | cut -c 1-10)
|
||||
today=$(date "+%Y-%m-%d")
|
||||
warning_at=$(date -d "$today + 30 day" "+%Y-%m-%d")
|
||||
expires_sec=$(date -d "$expires" "+%s")
|
||||
warning_at_sec=$(date -d "$warning_at" "+%s")
|
||||
|
||||
if [ "$expires_sec" -le "$warning_at_sec" ]; then
|
||||
echo "root_warn=true" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "root_warn=false" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Slack timestamp notification
|
||||
if: ${{ steps.check_timestamp.outputs.timestamp_warn == 'true' }}
|
||||
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": "⚠️ TUF timestamp.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Slack Root Notification
|
||||
if: ${{ steps.check_root.outputs.root_warn == 'true' }}
|
||||
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": "⚠️ TUF root.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
- name: Slack snapshot notification
|
||||
if: ${{ steps.check_snapshot.outputs.snapshot_warn == 'true' }}
|
||||
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": "⚠️ TUF snapshot.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Slack targets notification
|
||||
if: ${{ steps.check_targets.outputs.targets_warn == 'true' }}
|
||||
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": "⚠️ TUF targets.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Slack root notification
|
||||
if: ${{ steps.check_root.outputs.root_warn == 'true' }}
|
||||
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": "⚠️ TUF root.json is about to expire or has already expired\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
|
|
|||
10
.github/workflows/dogfood-gitops.yml
vendored
10
.github/workflows/dogfood-gitops.yml
vendored
|
|
@ -48,13 +48,6 @@ jobs:
|
|||
ref: main
|
||||
path: fleet-gitops
|
||||
|
||||
- name: Apply env vars to profiles
|
||||
env:
|
||||
MANAGED_CHROME_ENROLLMENT_TOKEN: ${{ secrets.CLOUD_MANAGEMENT_ENROLLMENT_TOKEN }}
|
||||
run: |
|
||||
envsubst < ./it-and-security/lib/configuration-profiles/macos-chrome-enrollment.mobileconfig > ./it-and-security/lib/configuration-profiles/macos-chrome-enrollment.confidential.mobileconfig
|
||||
mv ./it-and-security/lib/configuration-profiles/macos-chrome-enrollment.confidential.mobileconfig ./it-and-security/lib/configuration-profiles/macos-chrome-enrollment.mobileconfig
|
||||
|
||||
- name: Apply latest configuration to Fleet
|
||||
uses: ./fleet-gitops/.github/gitops-action
|
||||
with:
|
||||
|
|
@ -81,4 +74,5 @@ jobs:
|
|||
DOGFOOD_CALENDAR_API_KEY: ${{ secrets.DOGFOOD_CALENDAR_API_KEY }}
|
||||
DOGFOOD_COMPLIANCE_EXCLUSIONS_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPLIANCE_EXCLUSIONS_ENROLL_SECRET }}
|
||||
DOGFOOD_COMPANY_OWNED_IPHONES_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPANY_OWNED_IPHONES_ENROLL_SECRET }}
|
||||
DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET }}
|
||||
DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET }}
|
||||
MANAGED_CHROME_ENROLLMENT_TOKEN: ${{ secrets.CLOUD_MANAGEMENT_ENROLLMENT_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Below is the JSON payload that is sent to Fleet Device Management Inc:
|
|||
"numUsers": 999,
|
||||
"numTeams": 999,
|
||||
"numPolicies": 999,
|
||||
"numQueries": 999,
|
||||
"numLabels": 999,
|
||||
"softwareInventoryEnabled": true,
|
||||
"vulnDetectionEnabled": true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||

|
||||
|
||||
[Workbrew recently](https://workbrew.com/) made waves with its official launch, highlighted in a [TechCrunch article](https://techcrunch.com/2024/11/19/workbrew-makes-open-source-package-manager-homebrew-enterprise-friendly/). Backed by $5 million in funding from developer-focused VC firms like Heavybit and Operator Collective, Workbrew is tackling a critical challenge: transforming Homebrew from a developer-centric tool into a secure, enterprise-ready solution.
|
||||
[Workbrew recently](https://workbrew.com/) made waves with its [official launch](https://workbrew.com/blog/workbrew-1-0), highlighted in a [TechCrunch article](https://techcrunch.com/2024/11/19/workbrew-makes-open-source-package-manager-homebrew-enterprise-friendly/). Backed by $5 million in funding from developer-focused VC firms like Heavybit and Operator Collective, Workbrew is tackling a critical challenge: transforming Homebrew from a developer-centric tool into a secure, enterprise-ready solution.
|
||||
|
||||
## Workbrew’s mission: From single-player to multiplayer
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
changes/18539-font-bug
Normal file
1
changes/18539-font-bug
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Update Inter font to latest version for woff2 files
|
||||
2
changes/22361-os-update-ade-sso
Normal file
2
changes/22361-os-update-ade-sso
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM
|
||||
IdP integration was enabled.
|
||||
1
changes/22819-delete-modal
Normal file
1
changes/22819-delete-modal
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fleet UI: Better information on what deleting a host does
|
||||
1
changes/23158-turn-off-windows-mdm-err
Normal file
1
changes/23158-turn-off-windows-mdm-err
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Adds a clearer error message when users attempt to turn MDM off on a Windows host.
|
||||
1
changes/23458-additional-stats
Normal file
1
changes/23458-additional-stats
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added additional statistics item for number of saved queries
|
||||
1
changes/23749-fix-learn-more-link
Normal file
1
changes/23749-fix-learn-more-link
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fleet UI: Fix learn more about JIT provisioning link
|
||||
|
|
@ -0,0 +1 @@
|
|||
* Fixed a bug where the HTTP client used for MDM APNs push notifications did not support using a configured proxy.
|
||||
2
changes/24024-no-setup-exp
Normal file
2
changes/24024-no-setup-exp
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Modifies the Fleet setup experience feature to not run if there is no software or script
|
||||
configured for the setup experience.
|
||||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/e-dard/netbug"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/licensing"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
configpkg "github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -498,7 +499,11 @@ the way that the Fleet server works.
|
|||
|
||||
var mdmPushService push.Pusher
|
||||
nanoMDMLogger := service.NewNanoMDMLogger(kitlog.With(logger, "component", "apple-mdm-push"))
|
||||
pushProviderFactory := buford.NewPushProviderFactory()
|
||||
pushProviderFactory := buford.NewPushProviderFactory(buford.WithNewClient(func(cert *tls.Certificate) (*http.Client, error) {
|
||||
return fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
})), nil
|
||||
}))
|
||||
if os.Getenv("FLEET_DEV_MDM_APPLE_DISABLE_PUSH") == "1" {
|
||||
mdmPushService = nopPusher{}
|
||||
} else {
|
||||
|
|
@ -1029,6 +1034,9 @@ the way that the Fleet server works.
|
|||
"get_frontend",
|
||||
service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger),
|
||||
)
|
||||
|
||||
frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler)
|
||||
|
||||
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore)
|
||||
|
||||
setupRequired, err := svc.SetupRequired(baseCtx)
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
|||
NumSoftwareCVEs: 105,
|
||||
NumTeams: 9,
|
||||
NumPolicies: 0,
|
||||
NumQueries: 200,
|
||||
NumLabels: 3,
|
||||
SoftwareInventoryEnabled: true,
|
||||
VulnDetectionEnabled: true,
|
||||
|
|
@ -139,7 +140,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.True(t, recorded)
|
||||
require.True(t, cleanedup)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0,"aiFeaturesDisabled":true,"maintenanceWindowsEnabled":true,"maintenanceWindowsConfigured":true,"numHostsFleetDesktopEnabled":1984}`, requestBody)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numQueries":200,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0,"aiFeaturesDisabled":true,"maintenanceWindowsEnabled":true,"maintenanceWindowsConfigured":true,"numHostsFleetDesktopEnabled":1984}`, requestBody)
|
||||
}
|
||||
|
||||
func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -2790,7 +2790,7 @@ Device-authenticated routes are routes used by the Fleet Desktop application. Un
|
|||
- [Get device's transparency URL](#get-devices-transparency-url)
|
||||
- [Download device's MDM manual enrollment profile](#download-devices-mdm-manual-enrollment-profile)
|
||||
- [Migrate device to Fleet from another MDM solution](#migrate-device-to-fleet-from-another-mdm-solution)
|
||||
- [Trigger FileVault key escrow](#trigger-filevault-key-escrow)
|
||||
- [Trigger Linux disk encryption escrow](#trigger-linux-disk-encryption-escrow)
|
||||
- [Report an agent error](#report-an-agent-error)
|
||||
|
||||
#### Refetch device's host
|
||||
|
|
@ -2876,7 +2876,6 @@ Gets all information required by Fleet Desktop, this includes things like the nu
|
|||
"notifications": {
|
||||
"needs_mdm_migration": true,
|
||||
"renew_enrollment_profile": false,
|
||||
"enforce_bitlocker_encryption": false,
|
||||
},
|
||||
"config": {
|
||||
"org_info": {
|
||||
|
|
@ -2898,8 +2897,6 @@ In regards to the `notifications` key:
|
|||
|
||||
- `needs_mdm_migration` means that the device fits all the requirements to allow the user to initiate an MDM migration to Fleet.
|
||||
- `renew_enrollment_profile` means that the device is currently unmanaged from MDM but should be DEP enrolled into Fleet.
|
||||
- `enforce_bitlocker_encryption` applies only to Windows devices and means that it should encrypt the disk and report the encryption key back to Fleet.
|
||||
|
||||
|
||||
#### Get device's software
|
||||
|
||||
|
|
@ -3098,7 +3095,7 @@ This supports the dynamic discovery of API features supported by the server for
|
|||
|
||||
#### Get device's transparency URL
|
||||
|
||||
Returns the URL to open when clicking the "Transparency" menu item in Fleet Desktop. Note that _Fleet Premium_ is required to configure a custom transparency URL.
|
||||
Returns the URL to open when clicking the "About Fleet" menu item in Fleet Desktop. Note that _Fleet Premium_ is required to configure a custom transparency URL.
|
||||
|
||||
`GET /api/v1/fleet/device/{token}/transparency`
|
||||
|
||||
|
|
@ -3170,6 +3167,30 @@ Signals the Fleet server to send a webbook request with the device UUID and seri
|
|||
|
||||
---
|
||||
|
||||
### Trigger Linux disk encryption escrow
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
Signals the fleet server to queue up the LUKS disk encryption escrow process (LUKS passphrase and slot key). If validation succeeds (disk encryption must be enforced for the team, the host's platform must be supported, the host's disk must already be encrypted, and the host's Orbit version must be new enough), this adds a notification flag for Orbit that, triggers escrow from the Orbit side.
|
||||
|
||||
`POST /api/v1/fleet/device/{token}/mdm/linux/trigger_escrow`
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ----- | ------ | ---- | ---------------------------------- |
|
||||
| token | string | path | The device's authentication token. |
|
||||
|
||||
##### Example
|
||||
|
||||
`POST /api/v1/fleet/device/abcdef012456789/mdm/linux/trigger_escrow`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 204`
|
||||
|
||||
---
|
||||
|
||||
### Report an agent error
|
||||
|
||||
Notifies the server about an agent error, resulting in two outcomes:
|
||||
|
|
@ -3199,8 +3220,46 @@ Notifies the server about an agent error, resulting in two outcomes:
|
|||
|
||||
## Orbit-authenticated routes
|
||||
|
||||
- [Escrow LUKS data](#escrow-luks-data)
|
||||
- [Get the status of a device in the setup experience](#get-the-status-of-a-device-in-the-setup-experience)
|
||||
|
||||
---
|
||||
|
||||
### Escrow LUKS data
|
||||
|
||||
`POST /api/fleet/orbit/luks_data`
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ----- | ------ | ---- | ---------------------------------- |
|
||||
| orbit_node_key | string | body | The Orbit's node key for authentication. |
|
||||
| client_error | string | body | An error description if the LUKS key escrow process fails client-side. If provided, passphrase/salt/key slot request parameters are ignored and may be omitted. |
|
||||
| passphrase | string | body | The LUKS passphrase generated for Fleet (the end user's existing passphrase is not transmitted) |
|
||||
| key_slot | int | body | The LUKS key slot ID corresponding to the provided passphrase |
|
||||
| salt | string | body | The salt corresponding to the specified LUKS key slot. Provided to track cases where an end user rotates LUKS credentials (at which point we'll no longer be able to decrypt data with the escrowed passphrase). |
|
||||
|
||||
##### Example
|
||||
|
||||
`POST /api/v1/fleet/orbit/luks_data`
|
||||
|
||||
##### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"orbit_node_key":"FbvSsWfTRwXEecUlCBTLmBcjGFAdzqd/",
|
||||
"passphrase": "6e657665-7220676f-6e6e6120-67697665-20796f75-207570",
|
||||
"salt": "d34db33f",
|
||||
"key_slot": 1,
|
||||
"client_error": ""
|
||||
}
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 204`
|
||||
|
||||
---
|
||||
|
||||
### Get the status of a device in the setup experience
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ Fleet requires at least MySQL version 8.0.36, and is tested using the InnoDB sto
|
|||
|
||||
There are many "drop-in replacements" for MySQL available. If you'd like to experiment with some bleeding-edge technology and use Fleet with one of these alternative database servers, we think that's awesome! Please be aware they are not officially supported and that it is very important to set up a dev environment to thoroughly test new releases.
|
||||
|
||||
> If you use multiple databases per database server for multiple Fleet instances, you'll need to provision more resources for your database server to ensure performance. You can experiment with finding the right resourcing for your needs.
|
||||
|
||||
### Redis
|
||||
|
||||
Fleet uses Redis to ingest and queue the results of distributed queries, cache data, etc. Many cloud providers (such as [AWS](https://aws.amazon.com/elasticache/) and [GCP](https://console.cloud.google.com/launcher/details/click-to-deploy-images/redis)) host reliable Redis services which you may consider for this purpose. A well supported Redis [Docker image](https://hub.docker.com/_/redis/) also exists if you would rather run Redis in a container. For more information on how to configure the `fleet` binary to use the correct Redis instance, see the [Redis configuration](https://fleetdm.com/docs/configuration/fleet-server-configuration#redis) documentation.
|
||||
|
|
|
|||
|
|
@ -143,3 +143,4 @@ This workflow takes about 30 minutes to complete and supports between 10 and 350
|
|||
|
||||
<meta name="pageOrderInSection" value="100">
|
||||
<meta name="description" value="Learn how to easily deploy Fleet on Render or AWS with Terraform.">
|
||||
<meta name="title" value="Hosting Fleet">
|
||||
|
|
|
|||
|
|
@ -1516,15 +1516,15 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) {
|
|||
}
|
||||
|
||||
func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.Team, enable *bool) error {
|
||||
var didUpdate, didUpdateMacOSDiskEncryption bool
|
||||
var didUpdate bool
|
||||
if enable != nil {
|
||||
if svc.config.Server.PrivateKey == "" {
|
||||
return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
if tm.Config.MDM.EnableDiskEncryption != *enable {
|
||||
if *enable && svc.config.Server.PrivateKey == "" {
|
||||
return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
tm.Config.MDM.EnableDiskEncryption = *enable
|
||||
didUpdate = true
|
||||
didUpdateMacOSDiskEncryption = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1537,13 +1537,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// macOS-specific stuff. For legacy reasons we check if apple is configured
|
||||
// via `appCfg.MDM.EnabledAndConfigured`
|
||||
//
|
||||
// TODO: is there a missing bitlocker activity feed item? (see same TODO on
|
||||
// other methods that deal with disk encryption)
|
||||
if appCfg.MDM.EnabledAndConfigured && didUpdateMacOSDiskEncryption {
|
||||
if appCfg.MDM.EnabledAndConfigured {
|
||||
var act fleet.ActivityDetails
|
||||
if tm.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import InputField from "components/forms/fields/InputField";
|
|||
import validUrl from "components/forms/validators/valid_url";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
import { IAppConfigFormProps, IFormField } from "../constants";
|
||||
|
||||
const baseClass = "app-config-form";
|
||||
|
|
@ -209,15 +210,18 @@ const Sso = ({
|
|||
name="enableJitProvisioning"
|
||||
value={enableJitProvisioning}
|
||||
parseTarget
|
||||
helpText={
|
||||
<>
|
||||
<CustomLink
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/just-in-time-provisioning`}
|
||||
text="Learn more"
|
||||
newTab
|
||||
/>{" "}
|
||||
about just-in-time (JIT) user provisioning.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
Create user and sync permissions on login{" "}
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/learn-more-about/just-in-time-provisioning"
|
||||
text="Learn more"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
Create user and sync permissions on login
|
||||
</Checkbox>
|
||||
)}
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import strUtils from "utilities/strings";
|
|||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
const baseClass = "delete-host-modal";
|
||||
|
||||
|
|
@ -61,8 +63,21 @@ const DeleteHostModal = ({
|
|||
<Modal title="Delete host" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
<p>
|
||||
This will remove the record of <b>{hostText()}</b>.{largeVolumeText()}
|
||||
This will remove the record of <b>{hostText()}</b> and associated data
|
||||
(e.g. unlock PINs).{largeVolumeText()}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
macOS, Windows, or Linux hosts will re-appear unless Fleet's
|
||||
agent is uninstalled.{" "}
|
||||
<CustomLink
|
||||
text="Uninstall Fleet's agent"
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/uninstall-fleetd`}
|
||||
newTab
|
||||
/>
|
||||
</li>
|
||||
<li>iOS and iPadOS hosts will re-appear unless MDM is turned off.</li>
|
||||
</ul>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
|||
10
frontend/pages/hosts/components/DeleteHostModal/_styles.scss
Normal file
10
frontend/pages/hosts/components/DeleteHostModal/_styles.scss
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.delete-host-modal {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $pad-medium;
|
||||
align-self: stretch;
|
||||
padding-inline-start: $pad-large;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { noop } from "lodash";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
|
|
@ -50,7 +50,7 @@ describe("Host Summary section", () => {
|
|||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
const fleetdVersion = summaryData.fleet_desktop_version as string;
|
||||
|
||||
const { user } = render(
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
|
|
@ -60,18 +60,17 @@ describe("Host Summary section", () => {
|
|||
);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByText(new RegExp(orbitVersion, "i")));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(new RegExp(osqueryVersion, "i"))
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(new RegExp(fleetdVersion, "i"))
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await fireEvent.mouseEnter(
|
||||
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 () => {
|
||||
|
|
@ -90,7 +89,7 @@ describe("Host Summary section", () => {
|
|||
const orbitVersion = summaryData.orbit_version as string;
|
||||
const osqueryVersion = summaryData.osquery_version as string;
|
||||
|
||||
const { user } = render(
|
||||
render(
|
||||
<HostSummary
|
||||
summaryData={summaryData}
|
||||
showRefetchSpinner={false}
|
||||
|
|
@ -100,7 +99,10 @@ describe("Host Summary section", () => {
|
|||
);
|
||||
|
||||
expect(screen.getByText("Agent")).toBeInTheDocument();
|
||||
await user.hover(screen.getByText(orbitVersion));
|
||||
|
||||
await fireEvent.mouseEnter(
|
||||
screen.getByText(new RegExp(orbitVersion, "i"))
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(new RegExp(osqueryVersion, "i"))
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const mdmService = {
|
|||
},
|
||||
|
||||
getProfilesStatusSummary: (teamId: number) => {
|
||||
let { MDM_PROFILES_STATUS_SUMMARY: path } = endpoints;
|
||||
let { PROFILES_STATUS_SUMMARY: path } = endpoints;
|
||||
|
||||
if (teamId) {
|
||||
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
MDM_PROFILE: (id: string) => `/${API_VERSION}/fleet/mdm/profiles/${id}`,
|
||||
|
||||
MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`,
|
||||
MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`,
|
||||
PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/configuration_profiles/summary`,
|
||||
MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`,
|
||||
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
|
||||
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
|
||||
|
|
|
|||
|
|
@ -796,20 +796,6 @@ You can learn more about how Fleet approaches security in the [security handbook
|
|||
In responding to security questionnaires, Fleet endeavors to provide full transparency via our [security policies](https://fleetdm.com/handbook/digital-experience/security-policies#security-policies), [trust](https://trust.fleetdm.com/), and [application security](https://fleetdm.com/handbook/digital-experience/application-security) documentation. In addition to this documentation, please refer to [the vendor questionnaires page](https://fleetdm.com/handbook/digital-experience/vendor-questionnaires). [Contact the Sales department](https://fleetdm.com/handbook/sales#contact-us) to address any pending questionnaires.
|
||||
|
||||
|
||||
## Getting a contract signed
|
||||
|
||||
If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature.
|
||||
|
||||
When a contract is going to be routed for signature by someone outside of Fleet (i.e. the vendor or customer), the requestor is responsible for working with the other party to make sure the document gets routed to the CEO for signature.
|
||||
|
||||
The SLA for contract signature is **2 business days**. Please do not follow up on signature unless this time has elapsed.
|
||||
|
||||
_**Note:** Signature open time for the CEO is not currently measured, to avoid the overhead of creating separate signature issues to measure open and close time. This may change as signature volume increases._
|
||||
|
||||
> _**Note:** If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature._
|
||||
|
||||
Please use [Fleet's billing email address](https://fleetdm.com/handbook/company/communications#email-relays) for all contracts, and never use individual emails except for signature. If the page to sign includes any individual emails in the docusign contract, please remove it before routing to the CEO for signature.
|
||||
|
||||
|
||||
## Getting a contract reviewed
|
||||
|
||||
|
|
@ -831,6 +817,20 @@ When no further review or action is required for an agreement and the document i
|
|||
|
||||
> **Note:** Please submit other legal questions and requests to [Digital Experience](https://fleetdm.com/handbook/digital-experience#contact-us).
|
||||
|
||||
## Getting a contract signed
|
||||
|
||||
If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature.
|
||||
|
||||
When a contract is going to be routed for signature by someone outside of Fleet (i.e. the vendor or customer), the requestor is responsible for working with the other party to make sure the document gets routed to the CEO for signature.
|
||||
|
||||
The SLA for contract signature is **2 business days**. Please do not follow up on signature unless this time has elapsed.
|
||||
|
||||
_**Note:** Signature open time for the CEO is not currently measured, to avoid the overhead of creating separate signature issues to measure open and close time. This may change as signature volume increases._
|
||||
|
||||
> _**Note:** If a contract is ready for signature and requires no review or revision, log into DocuSign (credentials in 1Password) and route the agreement to the CEO for signature._
|
||||
|
||||
Please use [Fleet's billing email address](https://fleetdm.com/handbook/company/communications#email-relays) for all contracts, and never use individual emails except for signature. If the page to sign includes any individual emails in the docusign contract, please remove it before routing to the CEO for signature.
|
||||
|
||||
|
||||
## Trust
|
||||
|
||||
|
|
|
|||
|
|
@ -471,6 +471,11 @@ Beginning with macOS 16, Fleet will offer same-day support for all major version
|
|||
6. When all bugs are fixed, follow the [writing a feature guide](https://fleetdm.com/handbook/engineering#write-a-feature-guide) process to publish an article announcing Fleet same-day support for the new major release.
|
||||
|
||||
|
||||
### Maintain TUF repo for secure agent updates
|
||||
|
||||
Instructions for creating and maintaining a TUF repo are available on our [TUF handbook page](https://fleetdm.com/handbook/engineering/tuf).
|
||||
|
||||
|
||||
## Rituals
|
||||
|
||||
<rituals :rituals="rituals['handbook/engineering/engineering.rituals.yml']"></rituals>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
-
|
||||
task: "Rotate the TUF root keys"
|
||||
startedOn: "2024-09-01"
|
||||
frequency: "Annually"
|
||||
description: https://fleetdm.com/handbook/engineering/tuf#rotate-the-root-keys
|
||||
moreInfoUrl: https://fleetdm.com/handbook/engineering/tuf#rotate-the-root-keys
|
||||
dri: "lukeheath"
|
||||
autoIssue:
|
||||
labels: [ "~engineering-initiated", "P2" ]
|
||||
repo: "fleet"
|
||||
-
|
||||
task: "Renew Apple certificate signing request (CSR)"
|
||||
startedOn: "2024-09-01"
|
||||
|
|
|
|||
138
handbook/engineering/tuf.md
Normal file
138
handbook/engineering/tuf.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# The Update Framework (TUF)
|
||||
|
||||
This handbook page outlines the processes required to create and maintain a TUF repo at Fleet.
|
||||
|
||||
## Create a new TUF repo
|
||||
|
||||
> This process requires use of the `fleetctl` binary on Ubuntu. As of Nov. 6, 2024, Fleet only builds a `fleetctl` binary for Linux on x64. For this reason, a VM of Ubuntu on a newer Silicon MacBook (ARM) will not work. A device with an x64 processor is required.
|
||||
|
||||
1. Follow the guide to create a [bootable Ubuntu USB drive](https://ubuntu.com/tutorials/create-a-usb-stick-on-ubuntu#1-overview) running the latest LTS version of Ubuntu Desktop.
|
||||
|
||||
2. Download the latest version of the `fleetctl` for Linux from the [Fleet releases GitHub page](https://github.com/fleetdm/fleet/releases).
|
||||
|
||||
3. Download the `tuf` CLI from [go-tuf's v0.5.2 GitHub releases page](https://github.com/theupdateframework/go-tuf/releases/tag/v0.5.2). It's important to use the same version of `go-tuf` that is used by `fleetctl`.
|
||||
|
||||
4. Connect a new USB drive and copy the `fleetctl` binary and `tuf` binary to the USB drive. The `tuf` binary will likely be in `~/go/bin/`.
|
||||
|
||||
5. Open 1Password and click "New item", then select "Secure note". Name the note to match the new TUF repo.
|
||||
|
||||
6. Next, generate four passwords, one for each role's key. Click "Add more", select "Password", then "Generate a new password".
|
||||
|
||||
7. Click the "Password" label in the input field and change it to "root passphrase". Repeat this two two more times for "root2 passphrase" and "root3 passphrase" as backups.
|
||||
|
||||
8. Repeat three more times for "targets passphrase", "snapshot passphrase", and "timestamp passphrase". Backups are not necessary for these keys because root keys can generate new ones.
|
||||
|
||||
9. Disconnect both USB drives.
|
||||
|
||||
10. Connect the bootable Ubuntu USB drive to the signing device and boot to Ubuntu. When the boot screen appears, press the key the manufacturer has set to enter the boot menu. This is typically F1, F10, or ESC.
|
||||
|
||||
11. On the boot menu, select the Ubuntu USB drive, then "Try or Install Ubuntu" to boot directly from the USB drive.
|
||||
|
||||
12. Walk through the setup steps and **do not** connect to the internet.
|
||||
|
||||
13. After reaching the Ubuntu desktop, plug in the USB drive containing the `fleetctl` and `tuf` binaries.
|
||||
|
||||
14. Click the "Show Apps" icon in the bottom-left corner, and open the Terminal app.
|
||||
|
||||
15. Mount the USB drive and navigate to the directory.
|
||||
|
||||
16. Run `./fleetctl updates init` to initialize a new TUF repo on the USB drive. Manually type in the passphrases for each role's key that you generated in 1Password.
|
||||
|
||||
17. Create multiple root keys in case one is lost. Run `mv keys/root.json keys/root1.json` to retain the first root key. Then run `./tuf gen-key root` and enter the passphrase for "root2". Repeat one more time for "root3". When complete, you should have three root keys: `root1.json`, `root2.json`, `root3.json`.
|
||||
|
||||
18. The last root key generated (`root3.json`) will be the only signature on the metadata at `staged/root.json`. We want to sign with all root keys. Run `mv keys/root1.json keys/root.json`, then run `./tuf sign root.json` to sign with key 1. Repeat the step for key 2 so that your `staged/root.json` is signed by all three root keys.
|
||||
|
||||
19. Plug in additional USB drives and copy only the `keys` directory. They will serve USB root backups.
|
||||
|
||||
20. Next, plug in a USB drive to serve as the repo drive to copy files for signing. This USB drive will never contain keys. When plugged in, copy only the `/repository` and `/staged` directories. Make sure **not** to copy the `/keys` directory.
|
||||
|
||||
21. Next, plug in a last USB drive to serve as your day-to-day signing drive. This will contains the targets, snapshot, and timestamp keys, but will not contain the root keys. Copy the `repository` and `staged` directories. Next, copy only the `keys/targets.json`, `keys/snapshot.json`, and `keys/timestamp.json` keys to the drive. Do **NOT** copy any of the root keys.
|
||||
|
||||
22. At this point, all USB drives can be removed and your offline signing device or VM turned off.
|
||||
|
||||
23. On your device connected to the internet, plug in the repo USB drive. This one should contain only the `repository` and `staged` directories. Copy the files from the USB drive to a working directory on your internet-connective device.
|
||||
|
||||
24. Upload the files to your desired file hosting location, typically AWS S3 or CloudFlare R2.
|
||||
|
||||
You now have a functional, secure TUF repo. You can now configure and use the [Fleet TUF repo release script](https://github.com/fleetdm/fleet/tree/main/tools/tuf) to add new file targets.
|
||||
|
||||
If you need to run TUF commands that are not available using the `fleetctl` binary, additional functionality is available using the `tuf` binary [documented by go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf#section-readme).
|
||||
|
||||
## Read and write to TUF repo on Cloudflare R2
|
||||
|
||||
Fleet hosts our TUF repo in Cloudflare R2 buckets for production and staging, updates.fleetdm.com and updates-staging.fleetdm.com. Read and write operations are performed used the [AWS CLI](https://developers.cloudflare.com/r2/examples/aws/aws-cli/) tool configured to communicate with R2.
|
||||
|
||||
Once configured, use the [Fleet TUF repo release script](https://github.com/fleetdm/fleet/tree/main/tools/tuf) to add new file targets. You can use the `aws s3 cp` command to push and pull objects: `aws s3 cp . s3://<bucket-name> --recursive --endpoint-url https://<accountid>.r2.cloudflarestorage.com`
|
||||
|
||||
## Add new TUF keys for authorized team members
|
||||
|
||||
The CTO is responsible for determining who has access to push agent updates. Timestamp and Snapshot keys can be held online, so their use can be automated, but Targets and Root keys must always be held offline. The root keys are held by the CTO and CEO in secure locations. Root keys are retrieved once per year to rotate them before their annual expiration, or to sign for new Targets keys as needed. Targets keys may be generated to provide approved team members the ability to push agent updates to the TUF repo.
|
||||
|
||||
This process requires running TUF commands that are not available using the `fleetctl` binary, so the `tuf` v0.5.2 CLI binary [documented by go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf#section-readme) needs to be [downloaded and compiled](https://github.com/theupdateframework/go-tuf/releases/tag/v0.5.2) for local use.
|
||||
|
||||
There are two roles required to complete these steps, the "Root" role who holds the root keys, and the "Releaser" role, who is gaining access to push updates.
|
||||
|
||||
1. The Releaser creates a new local directory to store the TUF repo. The Releaser creates a sub-directory called `repository`.
|
||||
|
||||
2. The Realeaser pulls down the contents of the TUF repo into the `repository` sub-directory.
|
||||
|
||||
3. From the root of their TUF directory, the Releaser runs `tuf gen-key --expires=365 targets`. This will create a `keys` sub-directory and `staged` sub-directory. Next, the Releaser runs `tuf gen-key --expires=365 snapshot`, then `tuf gen-key --expires=365 timestamp` to create keys for those roles. Passphrases should be generated by and stored on 1Password.
|
||||
|
||||
4. The Releaser copies the `keys` directory to a USB drive, and deletes the `keys` directory from their local hard drive.
|
||||
|
||||
5. The Releaser sends the `staged/root.json` to the Root role for signing. Note this file is safe to share and is publicly available.
|
||||
|
||||
6. The Root role receives the `staged/root.json` file and copies it to a USB drive.
|
||||
|
||||
7. The Root role boots into the secure Ubuntu boot drive created during TUF repo creation.
|
||||
|
||||
8. The Root role connects the USB drive containing the `staged/root.json` file for signing.
|
||||
|
||||
9. The Root role connects the USB drive containing the root keys.
|
||||
|
||||
10. The Root role copies the `staged/root.json` onto the root keys USB at `staged/root.json`.
|
||||
|
||||
11. The root keys USB contains the `tuf` binary. Run `./tuf sign root.json` to sign the staged root metadata.
|
||||
|
||||
12. The Root role copies the signed `staged/root.json` back to the original USB drive they copied it from.
|
||||
|
||||
13. The Root role turns off the Ubuntu boot drive and accesses an online computer.
|
||||
|
||||
14. The Root role connects the USB drive containing the signed `staged/root.json` file and copies it to their local hard drive's TUF location in the same `staged/root.json`.
|
||||
|
||||
15. From the root of their local TUF repo, the Root role runs `tuf commit` to commit the staged root metadata to the `repository` directory.
|
||||
|
||||
16. The Root role pushes the updated contents of the `repository` directory to the remote TUF server.
|
||||
|
||||
17. The Releaser role can now run `tuf sign` to sign agent updates using their offline Targets key.
|
||||
|
||||
## Rotate the root keys
|
||||
|
||||
The root keys expire every year and must be manually rotated at least 30 days prior to expiration.
|
||||
|
||||
1. The root keys are retrieved from their secure location.
|
||||
|
||||
2. The offline Ubuntu bootable USB drive is turned on.
|
||||
|
||||
3. The root keys USB drive is connected to the Ubuntu bootable instance. Before proceeding, make two backups of the root keys on USB drives for safe keeping. They will be deleted when the root keys have been successfully rotated.
|
||||
|
||||
4. Add three new root keys using the steps documented in creating a new TUF repo.
|
||||
|
||||
5. Run `tuf sign root.json` to sign the newly added root keys with an existing root key.
|
||||
|
||||
6. Run `tuf commit` to commit the staged metadata with new root keys.
|
||||
|
||||
7. Using one of the new root keys, run `tuf revoke-key <role> <id>`. Run this command for each of the old, expiring root keys.
|
||||
|
||||
8. Using each of the new root keys, run `tuf sign root.json` to sign the root metadata removing the old root keys and adding the new keys so that the new root.json is signed by all root keys.
|
||||
|
||||
9. Using one of the new root keys, run `tuf commit` to commit the staged root metadata.
|
||||
|
||||
10. Confirm the file in `repository/root.json` contains the new root key ids by comparing the ids listed in `signed.roles.root.keyids` to the signatures in `signatures`. Make sure all root ids have signed.
|
||||
|
||||
11. Copy the `repository` directory to the local drive of an online device and push to the remote TUF repo.
|
||||
|
||||
12. Confirm that agent updates are continuing with the new `root.json`. Once confirmed, it is safe to delete the old root keys and backup the new keys.
|
||||
|
||||
<meta name="maintainedBy" value="lukeheath">
|
||||
<meta name="description" value="This page outlines our TUF creation and maintenance processes.">
|
||||
|
|
@ -92,8 +92,8 @@ controls:
|
|||
enable_end_user_authentication: true
|
||||
macos_setup_assistant: null
|
||||
macos_updates:
|
||||
deadline: "2024-08-23"
|
||||
minimum_version: "14.6.1"
|
||||
deadline: "2024-12-02"
|
||||
minimum_version: "15.1.1"
|
||||
windows_settings:
|
||||
custom_settings:
|
||||
- path: ../lib/configuration-profiles/windows-firewall.xml
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ controls:
|
|||
enable_end_user_authentication: true
|
||||
macos_setup_assistant: null
|
||||
macos_updates:
|
||||
deadline: "2024-08-23"
|
||||
minimum_version: "14.6.1"
|
||||
deadline: "2024-12-02"
|
||||
minimum_version: "15.1.1"
|
||||
windows_settings:
|
||||
custom_settings: null
|
||||
windows_updates:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// SYSTEM service on Windows) as the current login user.
|
||||
package execuser
|
||||
|
||||
import "context"
|
||||
|
||||
type eopts struct {
|
||||
env [][2]string
|
||||
args [][2]string
|
||||
|
|
@ -51,14 +49,3 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er
|
|||
}
|
||||
return runWithOutput(path, o)
|
||||
}
|
||||
|
||||
// RunWithWait runs an application as the current login user and waits for it to finish
|
||||
// or to be canceled by the context. Canceling the context will not return an error.
|
||||
// It assumes the caller is running with high privileges (root on UNIX).
|
||||
func RunWithWait(ctx context.Context, path string, opts ...Option) error {
|
||||
var o eopts
|
||||
for _, fn := range opts {
|
||||
fn(&o)
|
||||
}
|
||||
return runWithWait(ctx, path, o)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package execuser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -10,6 +9,8 @@ import (
|
|||
)
|
||||
|
||||
// run uses macOS open command to start application as the current login user.
|
||||
// Note that the child process spawns a new process in user space and thus it is not
|
||||
// effective to add a context to this function to cancel the child process.
|
||||
func run(path string, opts eopts) (lastLogs string, err error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
|
|
@ -53,7 +54,3 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
|||
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
||||
return nil, 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func runWithWait(ctx context.Context, path string, opts eopts) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package execuser
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -33,6 +32,12 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
|||
path,
|
||||
)
|
||||
|
||||
if len(opts.args) > 0 {
|
||||
for _, arg := range opts.args {
|
||||
args = append(args, arg[0], arg[1])
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
|
|
@ -74,37 +79,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
|
|||
return output, exitCode, nil
|
||||
}
|
||||
|
||||
func runWithWait(ctx context.Context, path string, opts eopts) error {
|
||||
args, err := getUserAndDisplayArgs(path, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get args: %w", err)
|
||||
}
|
||||
|
||||
args = append(args, path)
|
||||
|
||||
if len(opts.args) > 0 {
|
||||
for _, arg := range opts.args {
|
||||
args = append(args, arg[0], arg[1])
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", args...)
|
||||
log.Printf("cmd=%s", cmd.String())
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("cmd start %q: %w", path, err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cmd wait %q: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
|
||||
user, err := getLoginUID()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package execuser
|
|||
// To view what was modified/added, you can use the execuser_windows_diff.sh script.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -122,10 +121,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
|
|||
return nil, 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func runWithWait(ctx context.Context, path string, opts eopts) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// getCurrentUserSessionId will attempt to resolve
|
||||
// the session ID of the user currently active on
|
||||
// the system.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ const (
|
|||
entryDialogTitle = "Enter disk encryption passphrase"
|
||||
entryDialogText = "Passphrase:"
|
||||
retryEntryDialogText = "Passphrase incorrect. Please try again."
|
||||
infoFailedTitle = "Encryption key escrow"
|
||||
infoTitle = "Disk encryption"
|
||||
infoFailedText = "Failed to escrow key. Please try again later."
|
||||
infoSuccessTitle = "Encryption key escrow"
|
||||
infoSuccessText = "Key escrowed successfully."
|
||||
infoSuccessText = "Success! Now, return to your browser window and follow the instructions to verify disk encryption."
|
||||
timeoutMessage = "Please visit Fleet Desktop > My device and click Create key"
|
||||
maxKeySlots = 8
|
||||
userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase
|
||||
)
|
||||
|
|
@ -53,6 +53,11 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
|
|||
response.Err = err.Error()
|
||||
}
|
||||
|
||||
if len(key) == 0 && err == nil {
|
||||
// dialog was canceled or timed out
|
||||
return nil
|
||||
}
|
||||
|
||||
response.Passphrase = string(key)
|
||||
response.KeySlot = keyslot
|
||||
|
||||
|
|
@ -76,7 +81,7 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
|
|||
}
|
||||
|
||||
// Show error in dialog
|
||||
if err := lr.infoPrompt(ctx, infoFailedTitle, infoFailedText); err != nil {
|
||||
if err := lr.infoPrompt(ctx, infoTitle, infoFailedText); err != nil {
|
||||
log.Info().Err(err).Msg("failed to show failed escrow key dialog")
|
||||
}
|
||||
|
||||
|
|
@ -84,14 +89,14 @@ func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
|
|||
}
|
||||
|
||||
if response.Err != "" {
|
||||
if err := lr.infoPrompt(ctx, infoFailedTitle, response.Err); err != nil {
|
||||
if err := lr.infoPrompt(ctx, infoTitle, response.Err); err != nil {
|
||||
log.Info().Err(err).Msg("failed to show response error dialog")
|
||||
}
|
||||
return fmt.Errorf("error getting linux escrow key: %s", response.Err)
|
||||
}
|
||||
|
||||
// Show success dialog
|
||||
if err := lr.infoPrompt(ctx, infoSuccessTitle, infoSuccessText); err != nil {
|
||||
if err := lr.infoPrompt(ctx, infoTitle, infoSuccessText); err != nil {
|
||||
log.Info().Err(err).Msg("failed to show success escrow key dialog")
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +113,19 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
|
|||
return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err)
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
|
||||
Title: infoTitle,
|
||||
Text: "Validating passphrase...",
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to show progress dialog")
|
||||
}
|
||||
|
||||
// Validate the passphrase
|
||||
for {
|
||||
valid, err := lr.passphraseIsValid(ctx, device, devicePath, passphrase, userKeySlot)
|
||||
|
|
@ -123,11 +141,27 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err)
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
|
||||
Title: infoTitle,
|
||||
Text: "Validating passphrase...",
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to show progress dialog after retry")
|
||||
}
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out")
|
||||
return nil, nil, nil
|
||||
err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
|
||||
Title: infoTitle,
|
||||
Text: "Key escrow in progress...",
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to show progress dialog")
|
||||
}
|
||||
|
||||
escrowPassphrase, err := generateRandomPassphrase()
|
||||
|
|
@ -216,14 +250,18 @@ func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]by
|
|||
TimeOut: 1 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
switch err {
|
||||
case dialog.ErrCanceled:
|
||||
switch {
|
||||
case errors.Is(err, dialog.ErrCanceled):
|
||||
log.Debug().Msg("end user canceled key escrow dialog")
|
||||
return nil, nil
|
||||
case dialog.ErrTimeout:
|
||||
case errors.Is(err, dialog.ErrTimeout):
|
||||
log.Debug().Msg("key escrow dialog timed out")
|
||||
err := lr.infoPrompt(ctx, infoTitle, timeoutMessage)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("failed to show timeout dialog")
|
||||
}
|
||||
return nil, nil
|
||||
case dialog.ErrUnknown:
|
||||
case errors.Is(err, dialog.ErrUnknown):
|
||||
return nil, err
|
||||
default:
|
||||
return nil, err
|
||||
|
|
@ -237,11 +275,11 @@ func (lr *LuksRunner) infoPrompt(ctx context.Context, title, text string) error
|
|||
err := lr.notifier.ShowInfo(ctx, dialog.InfoOptions{
|
||||
Title: title,
|
||||
Text: text,
|
||||
TimeOut: 30 * time.Second,
|
||||
TimeOut: 1 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
switch err {
|
||||
case dialog.ErrTimeout:
|
||||
switch {
|
||||
case errors.Is(err, dialog.ErrTimeout):
|
||||
log.Debug().Msg("successPrompt timed out")
|
||||
return nil
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -7,28 +7,37 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const zenityProcessName = "zenity"
|
||||
|
||||
type Zenity struct {
|
||||
// cmdWithOutput can be set in tests to mock execution of the dialog.
|
||||
cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error)
|
||||
// cmdWithWait can be set in tests to mock execution of the dialog.
|
||||
cmdWithWait func(ctx context.Context, args ...string) error
|
||||
// killZenityFunc can be set in tests to mock killing the zenity process.
|
||||
killZenityFunc func()
|
||||
}
|
||||
|
||||
// New creates a new Zenity dialog instance for zenity v4 on Linux.
|
||||
// Zenity implements the Dialog interface.
|
||||
func New() *Zenity {
|
||||
return &Zenity{
|
||||
cmdWithOutput: execCmdWithOutput,
|
||||
cmdWithWait: execCmdWithWait,
|
||||
cmdWithOutput: execCmdWithOutput,
|
||||
cmdWithWait: execCmdWithWait,
|
||||
killZenityFunc: killZenityProcesses,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowEntry displays an dialog that accepts end user input. It returns the entered
|
||||
// text or errors ErrCanceled, ErrTimeout, or ErrUnknown.
|
||||
func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) {
|
||||
z.killZenityFunc()
|
||||
|
||||
args := []string{"--entry"}
|
||||
if opts.Title != "" {
|
||||
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
|
||||
|
|
@ -47,9 +56,9 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt
|
|||
if err != nil {
|
||||
switch statusCode {
|
||||
case 1:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrCanceled)
|
||||
return nil, dialog.ErrCanceled
|
||||
case 5:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrTimeout)
|
||||
return nil, dialog.ErrTimeout
|
||||
default:
|
||||
return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
|
||||
}
|
||||
|
|
@ -60,6 +69,8 @@ func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byt
|
|||
|
||||
// ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown.
|
||||
func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
|
||||
z.killZenityFunc()
|
||||
|
||||
args := []string{"--info"}
|
||||
if opts.Title != "" {
|
||||
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
|
||||
|
|
@ -94,6 +105,8 @@ func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
|
|||
// Use this function for cases where a progress dialog is needed to run
|
||||
// alongside other operations, with explicit cancellation or termination.
|
||||
func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error {
|
||||
z.killZenityFunc()
|
||||
|
||||
args := []string{"--progress"}
|
||||
if opts.Title != "" {
|
||||
args = append(args, fmt.Sprintf("--title=%s", opts.Title))
|
||||
|
|
@ -122,7 +135,7 @@ func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error)
|
|||
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
|
||||
}
|
||||
|
||||
output, exitCode, err := execuser.RunWithOutput("zenity", opts...)
|
||||
output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...)
|
||||
|
||||
// Trim the newline from zenity output
|
||||
output = bytes.TrimSuffix(output, []byte("\n"))
|
||||
|
|
@ -136,5 +149,13 @@ func execCmdWithWait(ctx context.Context, args ...string) error {
|
|||
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
|
||||
}
|
||||
|
||||
return execuser.RunWithWait(ctx, "zenity", opts...)
|
||||
_, err := execuser.Run(zenityProcessName, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
func killZenityProcesses() {
|
||||
_, err := platform.KillAllProcessByName(zenityProcessName)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to kill zenity process")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ func TestShowEntryArgs(t *testing.T) {
|
|||
output: []byte("some output"),
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -117,7 +118,8 @@ func TestShowEntryError(t *testing.T) {
|
|||
exitCode: tt.exitCode,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
|
|
@ -133,7 +135,8 @@ func TestShowEntrySuccess(t *testing.T) {
|
|||
output: []byte("some output"),
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -168,7 +171,8 @@ func TestShowInfoArgs(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
err := z.ShowInfo(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -203,7 +207,8 @@ func TestShowInfoError(t *testing.T) {
|
|||
exitCode: tt.exitCode,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
cmdWithOutput: mock.runWithOutput,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
err := z.ShowInfo(ctx, dialog.InfoOptions{})
|
||||
require.ErrorIs(t, err, tt.expectedErr)
|
||||
|
|
@ -233,7 +238,8 @@ func TestProgressArgs(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &mockExecCmd{}
|
||||
z := &Zenity{
|
||||
cmdWithWait: mock.runWithWait,
|
||||
cmdWithWait: mock.runWithWait,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
err := z.ShowProgress(ctx, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -249,7 +255,8 @@ func TestProgressKillOnCancel(t *testing.T) {
|
|||
waitDuration: 5 * time.Second,
|
||||
}
|
||||
z := &Zenity{
|
||||
cmdWithWait: mock.runWithWait,
|
||||
cmdWithWait: mock.runWithWait,
|
||||
killZenityFunc: func() {},
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ func WithTLSConfig(conf *tls.Config) TransportOpt {
|
|||
// NewTransport creates an http transport (a type that implements
|
||||
// http.RoundTripper) with the provided optional options. The transport is
|
||||
// derived from Go's http.DefaultTransport and only overrides the specific
|
||||
// parts it needs to, so that it keeps its sane defaults for the rest.
|
||||
// parts it needs to, so that it keeps its sane defaults for the rest (such as
|
||||
// timeouts and proxy support).
|
||||
func NewTransport(opts ...TransportOpt) *http.Transport {
|
||||
var to transportOpts
|
||||
for _, opt := range opts {
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet
|
|||
return secrets, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
|
||||
func (ds *Datastore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
|
||||
if teamID != nil && *teamID > 0 {
|
||||
tc, err := ds.TeamMDMConfig(ctx, *teamID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.False(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil)
|
||||
enabled, err := ds.GetConfigEnableDiskEncryption(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, enabled)
|
||||
|
||||
|
|
@ -461,7 +461,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil)
|
||||
enabled, err = ds.GetConfigEnableDiskEncryption(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, enabled)
|
||||
|
||||
|
|
@ -474,7 +474,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, tm)
|
||||
require.False(t, tm.Config.MDM.EnableDiskEncryption)
|
||||
|
||||
enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID)
|
||||
enabled, err = ds.GetConfigEnableDiskEncryption(ctx, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, enabled)
|
||||
|
||||
|
|
|
|||
|
|
@ -1218,7 +1218,7 @@ func (ds *Datastore) applyHostFilters(
|
|||
return "", nil, ctxerr.Wrap(ctx, err, "building query to filter macOS settings status")
|
||||
}
|
||||
sqlStmt, whereParams = filterHostsByMacOSDiskEncryptionStatus(sqlStmt, opt, whereParams)
|
||||
if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil, ctxerr.Wrap(
|
||||
ctx, &fleet.BadRequestError{
|
||||
|
|
@ -4547,6 +4547,7 @@ func (ds *Datastore) HostLite(ctx context.Context, id uint) (*fleet.Host, error)
|
|||
"hardware_model",
|
||||
"computer_name",
|
||||
"platform",
|
||||
"os_version",
|
||||
"team_id",
|
||||
"distributed_interval",
|
||||
"logger_tls_period",
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea
|
|||
}
|
||||
query, whereParams = filterHostsByMacOSDiskEncryptionStatus(query, opt, whereParams)
|
||||
query, whereParams = filterHostsByMDMBootstrapPackageStatus(query, opt, whereParams)
|
||||
if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
return "", nil, err
|
||||
} else if opt.OSSettingsFilter.IsValid() {
|
||||
query, whereParams, err = ds.filterHostsByOSSettingsStatus(query, opt, whereParams, enableDiskEncryption)
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@ AND (
|
|||
}
|
||||
|
||||
func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID)
|
||||
enabled, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -655,7 +655,7 @@ func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fle
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID)
|
||||
enabled, err := ds.GetConfigEnableDiskEncryption(ctx, host.TeamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -887,7 +887,7 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, e
|
|||
}
|
||||
|
||||
func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
|
||||
includeBitLocker, err := ds.getConfigEnableDiskEncryption(ctx, teamID)
|
||||
includeBitLocker, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -714,3 +714,15 @@ func (ds *Datastore) UpdateLiveQueryStats(ctx context.Context, queryID uint, sta
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func numSavedQueriesDB(ctx context.Context, db sqlx.QueryerContext) (int, error) {
|
||||
var count int
|
||||
const stmt = `
|
||||
SELECT count(*) FROM queries WHERE saved
|
||||
`
|
||||
if err := sqlx.GetContext(ctx, db, &count, stmt); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,8 +109,11 @@ WHERE global_or_team_id = ?`
|
|||
}
|
||||
totalInsertions += uint(inserts) // nolint: gosec
|
||||
|
||||
if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
|
||||
// Only run setup experience on hosts that have something configured.
|
||||
if totalInsertions > 0 {
|
||||
if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -529,7 +532,7 @@ WHERE host_uuid = ?
|
|||
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &awaitingConfiguration, stmt, hostUUID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
return false, notFound("HostAwaitingConfiguration")
|
||||
}
|
||||
|
||||
return false, ctxerr.Wrap(ctx, err, "getting host awaiting configuration")
|
||||
|
|
|
|||
|
|
@ -40,20 +40,22 @@ func TestSetupExperience(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO(JVE): this test could probably be simplified and most of the ad-hoc SQL removed.
|
||||
func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
// Create some teams
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
team3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// Create some software installers and add them to setup experience
|
||||
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
|
||||
require.NoError(t, err)
|
||||
installerID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
|
|
@ -97,6 +99,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
return err
|
||||
})
|
||||
|
||||
// Create some VPP apps and add them to setup experience
|
||||
app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"}
|
||||
vpp1, err := ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -110,33 +113,17 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
return err
|
||||
})
|
||||
|
||||
var script1ID, script2ID int64
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
res, err := insertScriptContents(ctx, q, "SCRIPT 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id1, _ := res.LastInsertId()
|
||||
res, err = insertScriptContents(ctx, q, "SCRIPT 2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id2, _ := res.LastInsertId()
|
||||
// Create some scripts and add them to setup experience
|
||||
err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script1", ScriptContents: "SCRIPT 1", TeamID: &team1.ID})
|
||||
require.NoError(t, err)
|
||||
err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script2", ScriptContents: "SCRIPT 2", TeamID: &team2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team1.ID, team1.ID, "script1", id1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script1ID, _ = res.LastInsertId()
|
||||
script1, err := ds.GetSetupExperienceScript(ctx, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team2.ID, team2.ID, "script2", id2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script2ID, _ = res.LastInsertId()
|
||||
|
||||
return nil
|
||||
})
|
||||
script2, err := ds.GetSetupExperienceScript(ctx, &team2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
hostTeam1 := "123"
|
||||
hostTeam2 := "456"
|
||||
|
|
@ -145,14 +132,26 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
anythingEnqueued, err := ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, anythingEnqueued)
|
||||
awaitingConfig, err := ds.GetHostAwaitingConfiguration(ctx, hostTeam1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, awaitingConfig)
|
||||
|
||||
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, anythingEnqueued)
|
||||
awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam2)
|
||||
require.NoError(t, err)
|
||||
require.True(t, awaitingConfig)
|
||||
|
||||
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam3, team3.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, anythingEnqueued)
|
||||
// Nothing is configured for setup experience in team 3, so we do not set
|
||||
// host_mdm_apple_awaiting_configuration.
|
||||
awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam3)
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.False(t, awaitingConfig)
|
||||
|
||||
seRows := []setupExperienceInsertTestRows{}
|
||||
|
||||
|
|
@ -191,13 +190,13 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
HostUUID: hostTeam1,
|
||||
Name: "script1",
|
||||
Status: "pending",
|
||||
ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
|
||||
ScriptID: nullableUint(script1.ID),
|
||||
},
|
||||
{
|
||||
HostUUID: hostTeam2,
|
||||
Name: "script2",
|
||||
Status: "pending",
|
||||
ScriptID: nullableUint(uint(script2ID)), // nolint: gosec
|
||||
ScriptID: nullableUint(script2.ID),
|
||||
},
|
||||
} {
|
||||
var found bool
|
||||
|
|
@ -212,35 +211,28 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, row := range seRows {
|
||||
if row.HostUUID == hostTeam3 {
|
||||
t.Error("team 3 shouldn't have any any entries")
|
||||
}
|
||||
}
|
||||
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, "DELETE FROM setup_experience_scripts WHERE global_or_team_id = ?", team2.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
require.Condition(t, func() (success bool) {
|
||||
for _, row := range seRows {
|
||||
if row.HostUUID == hostTeam3 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
_, err = q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = q.ExecContext(ctx, "UPDATE vpp_apps_teams SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
|
||||
// Remove team2's setup experience items
|
||||
err = ds.DeleteSetupExperienceScript(ctx, &team2.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetSetupExperienceSoftwareTitles(ctx, team2.ID, []uint{})
|
||||
require.NoError(t, err)
|
||||
|
||||
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, anythingEnqueued)
|
||||
|
||||
// team2 now has nothing enqueued
|
||||
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, anythingEnqueued)
|
||||
|
|
@ -272,7 +264,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
|
|||
HostUUID: hostTeam1,
|
||||
Name: "script1",
|
||||
Status: "pending",
|
||||
ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
|
||||
ScriptID: nullableUint(script1.ID),
|
||||
},
|
||||
} {
|
||||
var found bool
|
||||
|
|
@ -908,6 +900,12 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) {
|
|||
inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "abc")
|
||||
require.NoError(t, err)
|
||||
require.False(t, inSetupExperience)
|
||||
|
||||
// host without a record in the table returns not found
|
||||
inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "404")
|
||||
require.Error(t, err)
|
||||
require.True(t, fleet.IsNotFound(err))
|
||||
require.False(t, inSetupExperience)
|
||||
}
|
||||
|
||||
func testGetSetupExperienceScriptByID(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,10 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "number of hosts with Fleet desktop installed")
|
||||
}
|
||||
numQueries, err := numSavedQueriesDB(ctx, ds.reader(ctx))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "number of saved queries in DB")
|
||||
}
|
||||
|
||||
stats.NumHostsEnrolled = amountEnrolledHosts
|
||||
stats.NumUsers = amountUsers
|
||||
|
|
@ -152,6 +156,7 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
|||
}
|
||||
}
|
||||
stats.NumHostsFleetDesktopEnabled = numHostsFleetDesktopEnabled
|
||||
stats.NumQueries = numQueries
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, 0, stats.NumSoftwareCVEs)
|
||||
assert.Equal(t, 0, stats.NumTeams)
|
||||
assert.Equal(t, 0, stats.NumPolicies)
|
||||
assert.Equal(t, 0, stats.NumQueries)
|
||||
assert.Equal(t, builtinLabels, stats.NumLabels)
|
||||
assert.Equal(t, false, stats.SoftwareInventoryEnabled)
|
||||
assert.Equal(t, true, stats.SystemUsersEnabled)
|
||||
|
|
@ -220,6 +221,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, 0, stats.NumSoftwareCVEs)
|
||||
assert.Equal(t, 1, stats.NumTeams)
|
||||
assert.Equal(t, 1, stats.NumPolicies)
|
||||
assert.Equal(t, 0, stats.NumQueries)
|
||||
assert.Equal(t, builtinLabels+1, stats.NumLabels)
|
||||
assert.Equal(t, false, stats.SoftwareInventoryEnabled)
|
||||
assert.Equal(t, false, stats.SystemUsersEnabled)
|
||||
|
|
@ -320,6 +322,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, "Fleet", stats.Organization)
|
||||
assert.Equal(t, 5, stats.NumHostsEnrolled)
|
||||
assert.Equal(t, 2, stats.NumUsers)
|
||||
assert.Equal(t, 0, stats.NumQueries)
|
||||
assert.Equal(t, 0, stats.NumSoftwareVersions)
|
||||
assert.Equal(t, 0, stats.NumHostSoftwares)
|
||||
assert.Equal(t, 0, stats.NumSoftwareTitles)
|
||||
|
|
@ -368,6 +371,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, "Fleet", stats.Organization)
|
||||
assert.Equal(t, 5, stats.NumHostsEnrolled)
|
||||
assert.Equal(t, 2, stats.NumUsers)
|
||||
assert.Equal(t, 0, stats.NumQueries)
|
||||
assert.Equal(t, 0, stats.NumSoftwareVersions)
|
||||
assert.Equal(t, 0, stats.NumHostSoftwares)
|
||||
assert.Equal(t, 0, stats.NumSoftwareTitles)
|
||||
|
|
|
|||
|
|
@ -899,6 +899,7 @@ type Datastore interface {
|
|||
GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error)
|
||||
SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error
|
||||
|
||||
GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error)
|
||||
SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error
|
||||
// SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for
|
||||
// a host
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ var (
|
|||
WindowsMDMNotConfiguredMessage = "Windows MDM isn't turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM."
|
||||
AppleMDMNotConfiguredMessage = "macOS MDM isn't turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM."
|
||||
AppleABMDefaultTeamDeprecatedMessage = "mdm.apple_bm_default_team has been deprecated. Please use the new mdm.apple_business_manager key documented here: https://fleetdm.com/learn-more-about/apple-business-manager-gitops"
|
||||
CantTurnOffMDMForWindowsHostsMessage = "Can't turn off MDM for Windows hosts."
|
||||
CantTurnOffMDMForIOSOrIPadOSMessage = "Can't turn off MDM for iOS or iPadOS hosts. Use wipe instead."
|
||||
)
|
||||
|
||||
// ErrWithStatusCode is an interface for errors that should set a specific HTTP
|
||||
|
|
|
|||
|
|
@ -310,8 +310,8 @@ type MDMDiskEncryptionSummary struct {
|
|||
RemovingEnforcement MDMPlatformsCounts `db:"removing_enforcement" json:"removing_enforcement"`
|
||||
}
|
||||
|
||||
// MDMProfilesSummary reports the number of hosts being managed with MDM configuration
|
||||
// profiles. Each host may be counted in only one of four mutually-exclusive categories:
|
||||
// MDMProfilesSummary reports the number of hosts being managed with configuration
|
||||
// profiles and/or disk encryption. Each host may be counted in only one of four mutually-exclusive categories:
|
||||
// Failed, Pending, Verifying, or Verified.
|
||||
type MDMProfilesSummary struct {
|
||||
// Verified includes each host where Fleet has verified the installation of all of the
|
||||
|
|
|
|||
|
|
@ -1059,6 +1059,11 @@ type Service interface {
|
|||
// Returns empty status if the host is not a supported Linux host
|
||||
LinuxHostDiskEncryptionStatus(ctx context.Context, host Host) (HostMDMDiskEncryption, error)
|
||||
|
||||
// GetMDMLinuxProfilesSummary summarizes the current status of Linux disk encryption for
|
||||
// the provided team (or hosts without a team if teamId is nil), or returns zeroes if disk
|
||||
// encryption is not enforced on the selected team
|
||||
GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (MDMProfilesSummary, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Common MDM
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type StatisticsPayload struct {
|
|||
NumSoftwareCVEs int `json:"numSoftwareCVEs"`
|
||||
NumTeams int `json:"numTeams"`
|
||||
NumPolicies int `json:"numPolicies"`
|
||||
NumQueries int `json:"numQueries"`
|
||||
NumLabels int `json:"numLabels"`
|
||||
SoftwareInventoryEnabled bool `json:"softwareInventoryEnabled"`
|
||||
VulnDetectionEnabled bool `json:"vulnDetectionEnabled"`
|
||||
|
|
|
|||
|
|
@ -635,6 +635,8 @@ type GetHostEmailsFunc func(ctx context.Context, hostUUID string, source string)
|
|||
|
||||
type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error
|
||||
|
||||
type GetConfigEnableDiskEncryptionFunc func(ctx context.Context, teamID *uint) (bool, error)
|
||||
|
||||
type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error
|
||||
|
||||
type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error
|
||||
|
|
@ -2087,6 +2089,9 @@ type DataStore struct {
|
|||
SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc
|
||||
SetOrUpdateHostDisksSpaceFuncInvoked bool
|
||||
|
||||
GetConfigEnableDiskEncryptionFunc GetConfigEnableDiskEncryptionFunc
|
||||
GetConfigEnableDiskEncryptionFuncInvoked bool
|
||||
|
||||
SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFunc
|
||||
SetOrUpdateHostDisksEncryptionFuncInvoked bool
|
||||
|
||||
|
|
@ -5034,6 +5039,13 @@ func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint,
|
|||
return s.SetOrUpdateHostDisksSpaceFunc(ctx, hostID, gigsAvailable, percentAvailable, gigsTotal)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
|
||||
s.mu.Lock()
|
||||
s.GetConfigEnableDiskEncryptionFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetConfigEnableDiskEncryptionFunc(ctx, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error {
|
||||
s.mu.Lock()
|
||||
s.SetOrUpdateHostDisksEncryptionFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
|
|||
// 1. To get the JSON value from the database
|
||||
// 2. To update fields with the incoming values
|
||||
if newAppConfig.MDM.EnableDiskEncryption.Valid {
|
||||
if svc.config.Server.PrivateKey == "" {
|
||||
if newAppConfig.MDM.EnableDiskEncryption.Value && svc.config.Server.PrivateKey == "" {
|
||||
return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption
|
||||
|
|
|
|||
|
|
@ -1526,7 +1526,13 @@ func (svc *Service) needsOSUpdateForDEPEnrollment(ctx context.Context, m fleet.M
|
|||
return false, nil
|
||||
}
|
||||
|
||||
return apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
|
||||
needsUpdate, err := apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
|
||||
if err != nil {
|
||||
level.Info(svc.logger).Log("msg", "checking os updates settings, cannot compare versions", "serial", m.Serial, "current_version", m.OSVersion, "minimum_version", settings.MinimumVersion.Value)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return needsUpdate, nil
|
||||
}
|
||||
|
||||
func (svc *Service) getAppleSoftwareUpdateRequiredForDEPEnrollment(m fleet.MDMAppleMachineInfo) (*fleet.MDMAppleSoftwareUpdateRequired, error) {
|
||||
|
|
@ -1603,10 +1609,19 @@ func (svc *Service) EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co
|
|||
return ctxerr.Wrap(ctx, err, "getting host info for mdm apple remove profile command")
|
||||
}
|
||||
|
||||
if h.Platform == "ios" || h.Platform == "ipados" {
|
||||
switch h.Platform {
|
||||
case "ios":
|
||||
fallthrough
|
||||
case "ipados":
|
||||
return &fleet.BadRequestError{
|
||||
Message: "Can't turn off MDM for iOS or iPadOS hosts. Use wipe instead.",
|
||||
Message: fleet.CantTurnOffMDMForIOSOrIPadOSMessage,
|
||||
}
|
||||
case "windows":
|
||||
return &fleet.BadRequestError{
|
||||
Message: fleet.CantTurnOffMDMForWindowsHostsMessage,
|
||||
}
|
||||
default:
|
||||
// host is darwin, so continue
|
||||
}
|
||||
|
||||
info, err := svc.ds.GetHostMDMCheckinInfo(ctx, h.UUID)
|
||||
|
|
@ -2137,12 +2152,15 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable
|
|||
return err
|
||||
}
|
||||
|
||||
var didUpdate, didUpdateMacOSDiskEncryption bool
|
||||
var didUpdate bool
|
||||
if enabled != nil {
|
||||
if ac.MDM.EnableDiskEncryption.Value != *enabled {
|
||||
if *enabled && svc.config.Server.PrivateKey == "" {
|
||||
return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
ac.MDM.EnableDiskEncryption = optjson.SetBool(*enabled)
|
||||
didUpdate = true
|
||||
didUpdateMacOSDiskEncryption = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2150,7 +2168,7 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable
|
|||
if err := svc.ds.SaveAppConfig(ctx, ac); err != nil {
|
||||
return err
|
||||
}
|
||||
if didUpdateMacOSDiskEncryption {
|
||||
if ac.MDM.EnabledAndConfigured { // if macOS MDM is configured, set up FileVault escrow
|
||||
var act fleet.ActivityDetails
|
||||
if ac.MDM.EnableDiskEncryption.Value {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{}
|
||||
|
|
|
|||
|
|
@ -4092,7 +4092,7 @@ func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) {
|
|||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: nil,
|
||||
err: "invalid current version",
|
||||
err: "", // no error, allow enrollment to proceed without software update
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -1233,3 +1234,44 @@ func registerMDM(
|
|||
mux.Handle(apple_mdm.MDMPath, mdmHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/mdm/sso" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version
|
||||
di := r.Header.Get("x-apple-aspen-deviceinfo")
|
||||
if di != "" {
|
||||
parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879)
|
||||
if err != nil {
|
||||
// just log the error and continue to next
|
||||
level.Error(logger).Log("msg", "parsing x-apple-aspen-deviceinfo", "err", err)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(r.Context(), parsed)
|
||||
if err != nil {
|
||||
// just log the error and continue to next
|
||||
level.Error(logger).Log("msg", "checking minimum os version for mdm", "err", err)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if sur != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := json.NewEncoder(w).Encode(sur); err != nil {
|
||||
level.Error(logger).Log("msg", "failed to encode software update required", "err", err)
|
||||
http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8638,6 +8638,11 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
|
|||
require.Equal(t, hostLin.ID, getHostResp.Host.ID)
|
||||
require.True(t, *getHostResp.Host.DiskEncryptionEnabled)
|
||||
|
||||
// should succeed as we no longer require MDM to access this endpoint, as Linux encryption doesn't require MDM
|
||||
var profiles getMDMProfilesSummaryResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles)
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles)
|
||||
|
||||
// set unencrypted for all hosts
|
||||
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostWin.ID, false))
|
||||
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostMac.ID, false))
|
||||
|
|
@ -8653,7 +8658,7 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
|
|||
require.Equal(t, hostMac.ID, getHostResp.Host.ID)
|
||||
require.False(t, *getHostResp.Host.DiskEncryptionEnabled)
|
||||
|
||||
// Linux does not return false, it omits the field when false
|
||||
// Linux may omit the field when false
|
||||
getHostResp = getHostResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostLin.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.Equal(t, hostLin.ID, getHostResp.Host.ID)
|
||||
|
|
|
|||
|
|
@ -2881,6 +2881,168 @@ func (s *integrationEnterpriseTestSuite) TestAppleOSUpdatesTeamConfig() {
|
|||
}, http.StatusUnprocessableEntity, &tmResp)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestLinuxDiskEncryption() {
|
||||
t := s.T()
|
||||
|
||||
// create a Linux host
|
||||
noTeamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"),
|
||||
OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"),
|
||||
UUID: t.Name() + "3",
|
||||
Hostname: t.Name() + "foo3.local",
|
||||
PrimaryIP: "192.168.1.3",
|
||||
PrimaryMac: "30-65-EC-6F-C4-60",
|
||||
Platform: "ubuntu",
|
||||
OSVersion: "Ubuntu 22.04",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
orbitKey := setOrbitEnrollment(t, noTeamHost, s.ds)
|
||||
noTeamHost.OrbitNodeKey = &orbitKey
|
||||
|
||||
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "A team"})
|
||||
require.NoError(t, err)
|
||||
teamID := ptr.Uint(team.ID)
|
||||
teamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
UUID: t.Name() + "2",
|
||||
Hostname: t.Name() + "foo2.local",
|
||||
PrimaryIP: "192.168.1.2",
|
||||
PrimaryMac: "30-65-EC-6F-C4-59",
|
||||
Platform: "rhel",
|
||||
OSVersion: "Fedora 38.0", // this check is why HostLite now includes os_version in the data it's selecting
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
teamOrbitKey := setOrbitEnrollment(t, teamHost, s.ds)
|
||||
teamHost.OrbitNodeKey = &teamOrbitKey
|
||||
|
||||
// NO TEAM //
|
||||
|
||||
// config profiles endpoint should work but show all zeroes
|
||||
var profileSummary getMDMProfilesSummaryResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// set encrypted for host
|
||||
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), noTeamHost.ID, true))
|
||||
|
||||
// should still show zeroes
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// turn on disk encryption enforcement
|
||||
s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
|
||||
|
||||
// should show the Linux host as pending
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// encryption summary should succeed (Linux encryption doesn't require MDM)
|
||||
var summary getMDMDiskEncryptionSummaryResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
|
||||
s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
|
||||
// disk is encrypted but key hasn't been escrowed yet
|
||||
require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
|
||||
|
||||
// trigger escrow process from device
|
||||
token := "much_valid"
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
|
||||
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, noTeamHost.ID, token)
|
||||
return err
|
||||
})
|
||||
// should fail because default Orbit version is too old
|
||||
res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusBadRequest)
|
||||
res.Body.Close()
|
||||
|
||||
// should succeed now that Orbit version isn't too old
|
||||
require.NoError(t, s.ds.SetOrUpdateHostOrbitInfo(context.Background(), noTeamHost.ID, fleet.MinOrbitLUKSVersion, sql.NullString{}, sql.NullBool{}))
|
||||
res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusNoContent)
|
||||
res.Body.Close()
|
||||
|
||||
// confirm that Orbit endpoint shows notification flag
|
||||
var orbitResponse orbitGetConfigResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &orbitResponse)
|
||||
require.True(t, orbitResponse.Notifications.RunDiskEncryptionEscrow)
|
||||
|
||||
// confirm that second Orbit pull doesn't show notification flag
|
||||
var secondOrbitResponse orbitGetConfigResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &secondOrbitResponse)
|
||||
require.False(t, secondOrbitResponse.Notifications.RunDiskEncryptionEscrow)
|
||||
|
||||
// set an error first; the successful write should overwrite that
|
||||
s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
|
||||
OrbitNodeKey: *noTeamHost.OrbitNodeKey,
|
||||
ClientError: "Houston, we had a problem",
|
||||
}, http.StatusNoContent)
|
||||
|
||||
// upload LUKS data
|
||||
keySlot := ptr.Uint(1)
|
||||
s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
|
||||
OrbitNodeKey: *noTeamHost.OrbitNodeKey,
|
||||
Passphrase: "whale makes pail rise",
|
||||
Salt: "the team i like lost",
|
||||
KeySlot: keySlot,
|
||||
}, http.StatusNoContent)
|
||||
|
||||
// confirm verified
|
||||
s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
|
||||
require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
|
||||
|
||||
// get passphrase back
|
||||
var keyResponse getHostEncryptionKeyResponse
|
||||
s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/mdm/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
|
||||
s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
|
||||
require.Equal(t, "whale makes pail rise", keyResponse.EncryptionKey.DecryptedValue)
|
||||
|
||||
// TEAM //
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// set encrypted for host
|
||||
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), teamHost.ID, true))
|
||||
|
||||
// should still show zeroes
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// turn on disk encryption enforcement for team
|
||||
s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{TeamID: teamID, EnableDiskEncryption: true}, http.StatusNoContent)
|
||||
|
||||
// should show the Linux host as pending
|
||||
s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
|
||||
require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary)
|
||||
|
||||
// encryption summary should show host as action required
|
||||
s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary)
|
||||
require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
|
||||
|
||||
// upload LUKS data (no error, and no trigger, first this time)
|
||||
keySlot = ptr.Uint(3)
|
||||
s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
|
||||
OrbitNodeKey: *teamHost.OrbitNodeKey,
|
||||
Passphrase: "the mome raths outgrabe",
|
||||
Salt: "jabberwocky, but salty",
|
||||
KeySlot: keySlot,
|
||||
}, http.StatusNoContent)
|
||||
|
||||
// confirm verified
|
||||
s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary)
|
||||
require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
|
||||
|
||||
// get passphrase back
|
||||
s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, teamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
|
||||
require.Equal(t, "the mome raths outgrabe", keyResponse.EncryptionKey.DecryptedValue)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -15,13 +21,16 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
kitlog "github.com/go-kit/log"
|
||||
|
|
@ -31,6 +40,7 @@ import (
|
|||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mozilla.org/pkcs7"
|
||||
)
|
||||
|
||||
type profileAssignmentReq struct {
|
||||
|
|
@ -111,12 +121,6 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() {
|
|||
|
||||
s.enableABM("fleet_ade_test")
|
||||
|
||||
// test manual and automatic release with the new setup experience flow
|
||||
for _, enableReleaseManually := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
|
||||
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false)
|
||||
})
|
||||
}
|
||||
// test manual and automatic release with the old worker flow
|
||||
for _, enableReleaseManually := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
|
||||
|
|
@ -207,12 +211,6 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() {
|
|||
// enable FileVault
|
||||
s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent)
|
||||
|
||||
// test manual and automatic release with the new setup experience flow
|
||||
for _, enableReleaseManually := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
|
||||
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false)
|
||||
})
|
||||
}
|
||||
// test manual and automatic release with the old worker flow
|
||||
for _, enableReleaseManually := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
|
||||
|
|
@ -528,7 +526,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
|
|||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(b, &orbitConfigResp))
|
||||
// should be notified of the setup experience flow
|
||||
require.True(t, orbitConfigResp.Notifications.RunSetupExperience)
|
||||
require.False(t, orbitConfigResp.Notifications.RunSetupExperience)
|
||||
|
||||
if enableReleaseManually {
|
||||
// get the worker's pending job from the future, there should not be any
|
||||
|
|
@ -2652,3 +2650,457 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingItFromABM
|
|||
// make sure the host gets post enrollment requests
|
||||
checkPostEnrollmentCommands(mdmDevice, true)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
|
||||
t := s.T()
|
||||
s.enableABM(t.Name())
|
||||
|
||||
latestMacOSVersion := "14.6.1" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json)
|
||||
latestMacOSBuild := "23G93" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json)
|
||||
deadline := "2023-12-31"
|
||||
scepChallenge := "scepcha/><llenge"
|
||||
scepURL := s.server.URL + "/mdm/apple/scep"
|
||||
mdmURL := s.server.URL + "/mdm/apple/mdm"
|
||||
|
||||
// for our tests, we'll crete two devices: devices[0] will be enrolled with no team and
|
||||
// devices[1] will be enrolled with a team (created later in this test)
|
||||
devices := []godep.Device{
|
||||
{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
||||
{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
||||
}
|
||||
s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
encoder := json.NewEncoder(w)
|
||||
switch r.URL.Path {
|
||||
case "/session":
|
||||
err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
|
||||
require.NoError(t, err)
|
||||
case "/profile":
|
||||
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
case "/server/devices":
|
||||
// This endpoint is used to get an initial list of
|
||||
// devices, return a single device
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: devices})
|
||||
require.NoError(t, err)
|
||||
case "/devices/sync":
|
||||
// This endpoint is polled over time to sync devices from
|
||||
// ABM, send a repeated serial and a new one
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"})
|
||||
require.NoError(t, err)
|
||||
case "/profile/devices":
|
||||
b, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var prof profileAssignmentReq
|
||||
require.NoError(t, json.Unmarshal(b, &prof))
|
||||
var resp godep.ProfileResponse
|
||||
resp.ProfileUUID = prof.ProfileUUID
|
||||
resp.Devices = make(map[string]string, len(prof.Devices))
|
||||
for _, device := range prof.Devices {
|
||||
resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
|
||||
}
|
||||
err = encoder.Encode(resp)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}
|
||||
}))
|
||||
s.runDEPSchedule()
|
||||
|
||||
// confirm that the devices were created
|
||||
listHostsRes := listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(t, listHostsRes.Hosts, 2)
|
||||
|
||||
// create a team and add the second device to it
|
||||
team := &fleet.Team{
|
||||
Name: t.Name() + "team1",
|
||||
Description: "desc team1",
|
||||
}
|
||||
var createTeamResp teamResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
|
||||
require.NotZero(t, createTeamResp.Team.ID)
|
||||
team = createTeamResp.Team
|
||||
for _, h := range listHostsRes.Hosts {
|
||||
if h.HardwareSerial == devices[1].SerialNumber {
|
||||
err := s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{h.ID})
|
||||
require.NoError(t, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// this helper function mocks the x-aspen-deviceinfo header that is sent by the device during
|
||||
// the enrollment
|
||||
encodeDeviceInfo := func(machineInfo fleet.MDMAppleMachineInfo) string {
|
||||
body, err := plist.Marshal(machineInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
// body is expected to be a PKCS7 signed message, although we don't currently verify the signature
|
||||
signedData, err := pkcs7.NewSignedData(body)
|
||||
require.NoError(t, err)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
crtBytes, err := depot.NewCACert().SelfSign(rand.Reader, key.Public(), key)
|
||||
require.NoError(t, err)
|
||||
crt, err := x509.ParseCertificate(crtBytes)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, signedData.AddSigner(crt, key, pkcs7.SignerInfoConfig{}))
|
||||
sig, err := signedData.Finish()
|
||||
require.NoError(t, err)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
// this helper function calls the /enroll endpoint with the supplied machineInfo (from the test
|
||||
// case) and checks for the expected response
|
||||
checkMDMEnrollEndpoint := func(machineInfo *fleet.MDMAppleMachineInfo, expectEnrollInfo *mdmtest.AppleEnrollInfo, expectSoftwareUpdate *fleet.MDMAppleSoftwareUpdateRequiredDetails) error {
|
||||
request, err := http.NewRequest("GET", s.server.URL+apple_mdm.EnrollPath+"?token="+loadEnrollmentProfileDEPToken(t, s.ds), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
if machineInfo != nil {
|
||||
request.Header.Set("x-apple-aspen-deviceinfo", encodeDeviceInfo(*machineInfo))
|
||||
}
|
||||
|
||||
// nolint:gosec // this client is used for testing only
|
||||
cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}))
|
||||
response, err := cc.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch {
|
||||
case expectEnrollInfo != nil:
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
rawProfile := body
|
||||
if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
|
||||
p7, err := pkcs7.Parse(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enrollment profile is not XML nor PKCS7 parseable: %w", err)
|
||||
}
|
||||
err = p7.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawProfile = p7.Content
|
||||
}
|
||||
enrollInfo, err := mdmtest.ParseEnrollmentProfile(rawProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enrollment profile: %w", err)
|
||||
}
|
||||
require.NotNil(t, enrollInfo)
|
||||
require.Equal(t, expectEnrollInfo, enrollInfo)
|
||||
|
||||
return nil
|
||||
|
||||
case expectSoftwareUpdate != nil:
|
||||
require.Equal(t, http.StatusForbidden, response.StatusCode)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
var sur fleet.MDMAppleSoftwareUpdateRequired
|
||||
require.NoError(t, json.Unmarshal(body, &sur))
|
||||
require.NotNil(t, sur)
|
||||
require.Equal(t, fleet.MDMAppleSoftwareUpdateRequiredCode, sur.Code)
|
||||
require.Equal(t, *expectSoftwareUpdate, sur.Details)
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("request error: %d, %s", response.StatusCode, response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// this helper function calls the /mdm/sso endpoint with the supplied machineInfo (from the test
|
||||
// case) and checks for the expected response
|
||||
checkMDMSSOEndpoint := func(machineInfo *fleet.MDMAppleMachineInfo, expectSoftwareUpdate *fleet.MDMAppleSoftwareUpdateRequiredDetails) error {
|
||||
request, err := http.NewRequest("GET", s.server.URL+"/mdm/sso", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
if machineInfo != nil {
|
||||
request.Header.Set("x-apple-aspen-deviceinfo", encodeDeviceInfo(*machineInfo))
|
||||
}
|
||||
|
||||
// nolint:gosec // this client is used for testing only
|
||||
cc := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}))
|
||||
response, err := cc.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch {
|
||||
case expectSoftwareUpdate != nil:
|
||||
require.Equal(t, http.StatusForbidden, response.StatusCode)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
var sur fleet.MDMAppleSoftwareUpdateRequired
|
||||
require.NoError(t, json.Unmarshal(body, &sur))
|
||||
require.NotNil(t, sur)
|
||||
require.Equal(t, fleet.MDMAppleSoftwareUpdateRequiredCode, sur.Code)
|
||||
require.Equal(t, *expectSoftwareUpdate, sur.Details)
|
||||
|
||||
return nil
|
||||
|
||||
case response.StatusCode == http.StatusOK:
|
||||
// this is the expected happy path based on the test server setup (note that the full
|
||||
// SSO callback flow is not being tested here, just the OS enforcement that is tied to
|
||||
// the `GET /mdm/sso` initial request)
|
||||
// https://github.com/fleetdm/fleet/blob/e62956924bbe041a1e75be2b0b7c6d1dd235a09d/server/service/testing_utils.go#L420-L421
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("request error: %d, %s", response.StatusCode, response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// this helper function sets the minimum OS version for the team or no team
|
||||
setMinOSVersion := func(minVersion string, deadline string, teamID *uint) {
|
||||
raw := json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_updates": { "minimum_version": "%s", "deadline": "%s" } } }`, minVersion, deadline))
|
||||
if teamID == nil {
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", raw, http.StatusOK, &acResp)
|
||||
assert.NotNil(t, acResp.MDM.MacOSUpdates)
|
||||
} else {
|
||||
tcResp := teamResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", *teamID), raw, http.StatusOK, &tcResp)
|
||||
}
|
||||
}
|
||||
|
||||
// this helper function sets the enable end user authentication for the team or no team
|
||||
setEnableEndUserAuth := func(enable bool, teamID *uint) {
|
||||
raw := json.RawMessage(fmt.Sprintf(`{ "mdm": { "macos_setup": { "enable_end_user_authentication": %v } } }`, enable))
|
||||
if teamID == nil {
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", raw, http.StatusOK, &acResp)
|
||||
} else {
|
||||
tcResp := teamResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", *teamID), raw, http.StatusOK, &tcResp)
|
||||
}
|
||||
}
|
||||
|
||||
// configure idp settings
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"end_user_authentication": { "entity_id": "https://example.com", "idp_name": "example-idp", "metadata_url": "https://idp.example.com/metadata" }
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
|
||||
t.Cleanup(func() {
|
||||
acResp := appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
||||
"mdm": {
|
||||
"macos_updates": { "minimum_version": null, "deadline": null },
|
||||
"macos_setup": { "enable_end_user_authentication": false },
|
||||
"end_user_authentication": { "entity_id": "", "idp_name": "", "metadata_url": "", "issuer_uri": "", "metadata": "" }
|
||||
}
|
||||
}`), http.StatusOK, &acResp)
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
machineInfo *fleet.MDMAppleMachineInfo
|
||||
updateRequired *fleet.MDMAppleSoftwareUpdateRequiredDetails
|
||||
// err is reserved for future test cases; currently we aren't expecting errors with this endpoint
|
||||
// because product specs say to allow enrollment to proceed without software update so this
|
||||
// is here so we can be explicit about those expectations and allow for future test cases
|
||||
// that may need to check for errors
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "device above latest",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: true,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: "14.6.2",
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: nil,
|
||||
},
|
||||
{
|
||||
name: "device equal to latest",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: true,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: latestMacOSVersion,
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: nil,
|
||||
},
|
||||
{
|
||||
name: "device below latest",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: true,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: "14.4",
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: &fleet.MDMAppleSoftwareUpdateRequiredDetails{
|
||||
OSVersion: latestMacOSVersion,
|
||||
BuildVersion: latestMacOSBuild,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "device below latest but MDM cannot request software update",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: false,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: "14.4",
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: nil,
|
||||
},
|
||||
{
|
||||
name: "no match for software update device ID",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: true,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: "14.4",
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "INVALID",
|
||||
},
|
||||
updateRequired: nil,
|
||||
err: "", // no error, allow enrollment to proceed without software update
|
||||
},
|
||||
{
|
||||
name: "no machine info",
|
||||
machineInfo: nil,
|
||||
updateRequired: nil,
|
||||
err: "", // no error, allow enrollment to proceed without software update
|
||||
},
|
||||
{
|
||||
name: "cannot parse OS version",
|
||||
machineInfo: &fleet.MDMAppleMachineInfo{
|
||||
MDMCanRequestSoftwareUpdate: true,
|
||||
Product: "Mac15,7",
|
||||
OSVersion: "INVALID",
|
||||
SupplementalBuildVersion: "IRRELEVANT",
|
||||
SoftwareUpdateDeviceID: "J516sAP",
|
||||
},
|
||||
updateRequired: nil,
|
||||
err: "", // no error, allow enrollment to proceed without software update
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("no team setting equal to latest", func(t *testing.T) {
|
||||
setMinOSVersion(latestMacOSVersion, deadline, nil)
|
||||
|
||||
t.Run("sso disabled", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mi fleet.MDMAppleMachineInfo
|
||||
if tc.machineInfo != nil {
|
||||
mi = *tc.machineInfo
|
||||
mi.Serial = devices[0].SerialNumber
|
||||
}
|
||||
var expectEnrollInfo *mdmtest.AppleEnrollInfo
|
||||
if tc.updateRequired == nil && tc.err == "" {
|
||||
expectEnrollInfo = &mdmtest.AppleEnrollInfo{
|
||||
SCEPChallenge: scepChallenge,
|
||||
SCEPURL: scepURL,
|
||||
MDMURL: mdmURL,
|
||||
}
|
||||
}
|
||||
|
||||
err := checkMDMEnrollEndpoint(&mi, expectEnrollInfo, tc.updateRequired)
|
||||
if tc.err != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sso enabled", func(t *testing.T) {
|
||||
setEnableEndUserAuth(true, nil)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mi fleet.MDMAppleMachineInfo
|
||||
if tc.machineInfo != nil {
|
||||
mi = *tc.machineInfo
|
||||
mi.Serial = devices[0].SerialNumber
|
||||
}
|
||||
|
||||
err := checkMDMSSOEndpoint(&mi, tc.updateRequired)
|
||||
if tc.err != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("team setting equal to latest", func(t *testing.T) {
|
||||
setMinOSVersion(latestMacOSVersion, deadline, &team.ID)
|
||||
|
||||
t.Run("sso disabled", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.machineInfo != nil {
|
||||
tc.machineInfo.Serial = devices[1].SerialNumber
|
||||
}
|
||||
var expectEnrollInfo *mdmtest.AppleEnrollInfo
|
||||
if tc.updateRequired == nil && tc.err == "" {
|
||||
expectEnrollInfo = &mdmtest.AppleEnrollInfo{
|
||||
SCEPChallenge: "scepcha/><llenge",
|
||||
SCEPURL: s.server.URL + "/mdm/apple/scep",
|
||||
MDMURL: s.server.URL + "/mdm/apple/mdm",
|
||||
}
|
||||
}
|
||||
|
||||
err := checkMDMEnrollEndpoint(tc.machineInfo, expectEnrollInfo, tc.updateRequired)
|
||||
if tc.err != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sso enabled", func(t *testing.T) {
|
||||
setEnableEndUserAuth(true, &team.ID)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.machineInfo != nil {
|
||||
tc.machineInfo.Serial = devices[0].SerialNumber
|
||||
}
|
||||
|
||||
err := checkMDMSSOEndpoint(tc.machineInfo, tc.updateRequired)
|
||||
if tc.err != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
},
|
||||
APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589",
|
||||
EnableSCEPProxy: true,
|
||||
WithDEPWebview: true,
|
||||
}
|
||||
|
||||
// ensure all our tests support challenges with invalid XML characters
|
||||
|
|
@ -11894,3 +11895,22 @@ func (s *integrationMDMTestSuite) TestSetupExperience() {
|
|||
require.NoError(t, err)
|
||||
require.True(t, awaitingConfig)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestHostsCantTurnMDMOff() {
|
||||
t := s.T()
|
||||
iOSHost, _ := s.createAppleMobileHostThenEnrollMDM("ios")
|
||||
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), iOSHost.ID, false, true, "https://foo.com", true, "", ""))
|
||||
|
||||
r := s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", iOSHost.ID), nil, http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), fleet.CantTurnOffMDMForIOSOrIPadOSMessage)
|
||||
|
||||
iPadOSHost, _ := s.createAppleMobileHostThenEnrollMDM("ipados")
|
||||
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), iPadOSHost.ID, false, true, "https://foo.com", true, "", ""))
|
||||
|
||||
r = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", iPadOSHost.ID), nil, http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), fleet.CantTurnOffMDMForIOSOrIPadOSMessage)
|
||||
|
||||
winHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
r = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", winHost.ID), nil, http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), fleet.CantTurnOffMDMForWindowsHostsMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
|
|
@ -42,3 +43,28 @@ func (svc *Service) LinuxHostDiskEncryptionStatus(ctx context.Context, host flee
|
|||
Status: &verified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (summary fleet.MDMProfilesSummary, err error) {
|
||||
if err = svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamId}, fleet.ActionRead); err != nil {
|
||||
return summary, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// Linux doesn't have configuration profiles, so if we aren't enforcing disk encryption we have nothing to report
|
||||
includeDiskEncryptionStats, err := svc.ds.GetConfigEnableDiskEncryption(ctx, teamId)
|
||||
if err != nil {
|
||||
return summary, ctxerr.Wrap(ctx, err)
|
||||
} else if !includeDiskEncryptionStats {
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
counts, err := svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamId)
|
||||
if err != nil {
|
||||
return summary, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
return fleet.MDMProfilesSummary{
|
||||
Verified: counts.Verified,
|
||||
Pending: counts.ActionRequired,
|
||||
Failed: counts.Failed,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -907,7 +907,8 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GET /mdm/profiles/summary
|
||||
// GET /mdm/profiles/summary (deprecated)
|
||||
// GET /configuration_profiles/summary
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type getMDMProfilesSummaryRequest struct {
|
||||
|
|
@ -935,10 +936,15 @@ func getMDMProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc
|
|||
return &getMDMProfilesSummaryResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
res.Verified = as.Verified + ws.Verified
|
||||
ls, err := svc.GetMDMLinuxProfilesSummary(ctx, req.TeamID)
|
||||
if err != nil {
|
||||
return &getMDMProfilesSummaryResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
res.Verified = as.Verified + ws.Verified + ls.Verified
|
||||
res.Verifying = as.Verifying + ws.Verifying
|
||||
res.Failed = as.Failed + ws.Failed
|
||||
res.Pending = as.Pending + ws.Pending
|
||||
res.Failed = as.Failed + ws.Failed + ls.Failed
|
||||
res.Pending = as.Pending + ws.Pending + ls.Pending
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
|
@ -2606,9 +2612,52 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek
|
|||
return ctxerr.Wrap(ctx, err, "retrieving app config")
|
||||
}
|
||||
|
||||
wasEnabledAndConfigured := appCfg.MDM.EnabledAndConfigured
|
||||
appCfg.MDM.EnabledAndConfigured = true
|
||||
err = svc.ds.SaveAppConfig(ctx, appCfg)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "saving app config")
|
||||
}
|
||||
|
||||
return svc.ds.SaveAppConfig(ctx, appCfg)
|
||||
// Disk encryption can be enabled prior to Apple MDM being configured, but we need MDM to be set up to escrow
|
||||
// FileVault keys. We handle the other order of operations elsewhere (on encryption enable, after checking to see
|
||||
// if Mac MDM is already enabled). We skip this step if we were just re-uploading an APNs cert when MDM was already
|
||||
// enabled.
|
||||
if wasEnabledAndConfigured {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable FileVault escrow if no-team already has disk encryption enforced
|
||||
if appCfg.MDM.EnableDiskEncryption.Value {
|
||||
if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable no-team FileVault escrow")
|
||||
}
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for enabling no-team macOS disk encryption")
|
||||
}
|
||||
}
|
||||
// Enable FileVault escrow for teams that already have disk encryption enforced
|
||||
// For later: add a data store method to avoid making an extra query per team to check whether encryption is enforced
|
||||
teams, err := svc.ds.TeamsSummary(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "listing teams")
|
||||
}
|
||||
for _, team := range teams {
|
||||
isEncryptionEnforced, err := svc.ds.GetConfigEnableDiskEncryption(ctx, &team.ID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving encryption enforcement status for team")
|
||||
}
|
||||
if isEncryptionEnforced {
|
||||
if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable FileVault escrow for team")
|
||||
}
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for enabling macOS disk encryption for team")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -235,19 +235,29 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
|
|||
}
|
||||
|
||||
if isConnectedToFleetMDM {
|
||||
// If there is no software or script configured for setup experience and this is the
|
||||
// first time orbit is calling the /config endpoint, then this host
|
||||
// will not have a row in host_mdm_apple_awaiting_configuration.
|
||||
// On subsequent calls to /config, the host WILL have a row in
|
||||
// host_mdm_apple_awaiting_configuration.
|
||||
inSetupAssistant, err := svc.ds.GetHostAwaitingConfiguration(ctx, host.UUID)
|
||||
if err != nil {
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking if host is in setup experience")
|
||||
}
|
||||
|
||||
if inSetupAssistant {
|
||||
notifs.RunSetupExperience = true
|
||||
}
|
||||
|
||||
// check if the client is running an old fleetd version that doesn't support the new
|
||||
// setup experience flow.
|
||||
if inSetupAssistant || fleet.IsNotFound(err) {
|
||||
// If the client is running a fleetd that doesn't support setup experience, or if no
|
||||
// software/script has been configured for setup experience, then we should fall back to
|
||||
// the "old way" of releasing the device. We do an additional check for
|
||||
// !inSetupAssistant to prevent enqueuing a new job every time the /config
|
||||
// endpoint is hit.
|
||||
mp, ok := capabilities.FromContext(ctx)
|
||||
if !ok || !mp.Has(fleet.CapabilitySetupExperience) {
|
||||
level.Debug(svc.logger).Log("msg", "host doesn't support Setup experience, falling back to worker-based device release", "host_uuid", host.UUID)
|
||||
if !ok || !mp.Has(fleet.CapabilitySetupExperience) || !inSetupAssistant {
|
||||
level.Debug(svc.logger).Log("msg", "host doesn't support setup experience or no setup experience configured, falling back to worker-based device release", "host_uuid", host.UUID)
|
||||
if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil {
|
||||
return fleet.OrbitConfig{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ type TestServerOpts struct {
|
|||
BootstrapPackageStore fleet.MDMBootstrapPackageStore
|
||||
KeyValueStore fleet.KeyValueStore
|
||||
EnableSCEPProxy bool
|
||||
WithDEPWebview bool
|
||||
}
|
||||
|
||||
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
|
||||
|
|
@ -413,6 +414,14 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ
|
|||
}
|
||||
}
|
||||
|
||||
if len(opts) > 0 && opts[0].WithDEPWebview {
|
||||
frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// do nothing and return 200
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
rootMux.Handle("/", frontendHandler)
|
||||
}
|
||||
|
||||
apiHandler := MakeHandler(svc, cfg, logger, limitStore, WithLoginRateLimit(throttled.PerMin(1000)))
|
||||
rootMux.Handle("/api/", apiHandler)
|
||||
var errHandler *errorstore.Handler
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ FROM rust:latest@sha256:56418f03475cf7b107f87d7fabe99ce9a4a9f9904daafa99be7c50d9
|
|||
|
||||
ARG transporter_url=https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/resources/download/public/Transporter__Linux/bin
|
||||
|
||||
RUN cargo install --version 0.16.0 apple-codesign \
|
||||
RUN cargo install --locked --version 0.16.0 apple-codesign \
|
||||
&& curl -sSf $transporter_url -o transporter_install.sh \
|
||||
&& sh transporter_install.sh --target transporter --accept --noexec
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ parasails.registerPage('new-license', {
|
|||
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
|
||||
beforeMount: function() {
|
||||
if(window.location.hash) {
|
||||
if(typeof analytics !== 'undefined') {
|
||||
if(window.analytics !== undefined) {
|
||||
if(window.location.hash === '#signup') {
|
||||
analytics.identify(this.me.id, {
|
||||
email: this.me.emailAddress,
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ parasails.registerPage('device-management-page', {
|
|||
this.modal = undefined;
|
||||
},
|
||||
clickSwagRequestCTA: function () {
|
||||
if(typeof gtag !== 'undefined') {
|
||||
if(window.gtag !== undefined){
|
||||
gtag('event','fleet_website__swag_request');
|
||||
}
|
||||
if(typeof window.lintrk !== 'undefined') {
|
||||
if(window.lintrk !== undefined) {
|
||||
window.lintrk('track', { conversion_id: 18587105 });// eslint-disable-line camelcase
|
||||
}
|
||||
if(typeof analytics !== 'undefined'){
|
||||
if(window.analytics !== undefined) {
|
||||
analytics.track('fleet_website__swag_request');
|
||||
}
|
||||
this.goto('https://kqphpqst851.typeform.com/to/ZfA3sOu0#from_page=device-managment');
|
||||
|
|
|
|||
|
|
@ -221,13 +221,13 @@ parasails.registerPage('basic-documentation', {
|
|||
methods: {
|
||||
|
||||
clickSwagRequestCTA: function () {
|
||||
if(typeof gtag !== 'undefined') {
|
||||
if(window.gtag !== undefined){
|
||||
gtag('event','fleet_website__swag_request');
|
||||
}
|
||||
if(typeof window.lintrk !== 'undefined') {
|
||||
if(window.lintrk !== undefined) {
|
||||
window.lintrk('track', { conversion_id: 18587105 });// eslint-disable-line camelcase
|
||||
}
|
||||
if(typeof analytics !== 'undefined'){
|
||||
if(window.analytics !== undefined) {
|
||||
analytics.track('fleet_website__swag_request');
|
||||
}
|
||||
this.goto('https://kqphpqst851.typeform.com/to/ZfA3sOu0#from_page=docs');
|
||||
|
|
|
|||
23
website/assets/js/pages/entrance/signup.page.js
vendored
23
website/assets/js/pages/entrance/signup.page.js
vendored
|
|
@ -64,19 +64,22 @@ parasails.registerPage('signup', {
|
|||
}
|
||||
},
|
||||
|
||||
submittedSignUpForm: async function() {
|
||||
// redirect to the /start page.
|
||||
// > (Note that we re-enable the syncing state here. This is on purpose--
|
||||
// > to make sure the spinner stays there until the page navigation finishes.)
|
||||
//
|
||||
// Naming convention: (like sails config)
|
||||
// "Website - Sign up" becomes "fleet_website__sign_up" (double-underscore representing hierarchy)
|
||||
if(typeof gtag !== 'undefined'){
|
||||
gtag('event','fleet_website__sign_up');
|
||||
submittedSignUpForm: async function() {// When the server says everything worked…
|
||||
// Track a "key event" in Google Analytics. (? but don't we do that when we call analytics.track() [segment] later on in start.page.js? TODO: eric help please – I suspect this one is either duplicate OR it's actually writing to Google Ads, and not to Google Analytics. I'm pretty sure segment's .track() is what writes to google analytics.)
|
||||
// > Naming convention: (like sails config)
|
||||
// > "Website - Sign up" becomes "fleet_website__sign_up" (double-underscore representing hierarchy)
|
||||
if(window.gtag !== undefined){
|
||||
window.gtag('event','fleet_website__sign_up');
|
||||
}
|
||||
if(typeof window.lintrk !== 'undefined') {
|
||||
|
||||
// Track a "conversion" in LinkedIn Campaign Manager.
|
||||
if(window.lintrk !== undefined) {
|
||||
window.lintrk('track', { conversion_id: 18587097 });// eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
// Redirect to the /start page.
|
||||
// > (Note that we re-enable the syncing state here. This is on purpose--
|
||||
// > to make sure the spinner stays there until the page navigation finishes.)
|
||||
this.syncing = true;
|
||||
this.goto(this.pageToRedirectToAfterRegistration);// « / start if the user came here from the start now button, or customers/new-license if the user came here from the "Get your license" link.
|
||||
}
|
||||
|
|
|
|||
2
website/assets/js/pages/start.page.js
vendored
2
website/assets/js/pages/start.page.js
vendored
|
|
@ -97,7 +97,7 @@ parasails.registerPage('start', {
|
|||
this.psychologicalStage = this.me.psychologicalStage;
|
||||
}
|
||||
if(window.location.hash) {
|
||||
if(typeof analytics !== 'undefined') {
|
||||
if(window.analytics !== undefined) {
|
||||
if(window.location.hash === '#signup') {
|
||||
analytics.identify(this.me.id, {
|
||||
email: this.me.emailAddress,
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@
|
|||
padding: 0px 32px 64px 32px;
|
||||
}
|
||||
[purpose='section-heading'] {
|
||||
padding: 40px 32px;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
[parasails-component='scrollable-tweets'] {
|
||||
[purpose='tweets'] {
|
||||
|
|
|
|||
1
website/config/routes.js
vendored
1
website/config/routes.js
vendored
|
|
@ -595,6 +595,7 @@ module.exports.routes = {
|
|||
'GET /feature-request': 'https://github.com/fleetdm/fleet/issues/new?assignees=&labels=~feature+fest%2C%3Aproduct&projects=&template=feature-request.md&title=',
|
||||
'GET /learn-more-about/policy-automation-run-script': '/guides/policy-automation-run-script',
|
||||
'GET /learn-more-about/installing-fleetctl': '/guides/fleetctl#installing-fleetctl',
|
||||
'GET /learn-more-about/mdm-disk-encryption': '/guides/enforce-disk-encryption',
|
||||
'GET /contribute-to/policies': 'https://github.com/fleetdm/fleet/edit/main/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml',
|
||||
|
||||
// Sitemap
|
||||
|
|
|
|||
3
website/views/pages/endpoint-ops.ejs
vendored
3
website/views/pages/endpoint-ops.ejs
vendored
|
|
@ -4,7 +4,7 @@
|
|||
<div purpose="hero">
|
||||
<div purpose="page-headline">
|
||||
<h4>Observability <%= ['eo-security', 'vm'].includes(pagePersonalization) ? 'for security' : ['eo-it', 'mdm'].includes(pagePersonalization) ? 'for IT' : '' %></h4>
|
||||
<h1><%= pagePersonalization==='eo-security'? 'Instrument your endpoints' : 'Talk to your computers'%></h1>
|
||||
<h1><%= pagePersonalization==='eo-security'? 'Deeper, faster visibility for every OS' : 'Talk to your computers'%></h1>
|
||||
</div>
|
||||
<div purpose="hero-content" class="d-flex flex-md-row flex-column align-items-center justify-content-between">
|
||||
<div purpose="hero-image">
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
<div purpose="quote">
|
||||
<img alt="an opening quotation mark" style="width:20px; margin-bottom: 16px;" src="/images/icon-quote-21x17@2x.png">
|
||||
<p>"We will have no, zero, blind spots in our entire infrastructure, more than 100,000 servers. It just works, it's awesome."</p>
|
||||
<!-- TODO: Fleet reduces costs through tool consolidation. Plus if you can’t confidently answer questions about the state and configuration of all your devices in seconds - regardless of operating system - you should take a look at what these guys are doing. -->
|
||||
</div>
|
||||
<a href="https://www.linkedin.com/in/charleszaffery/" target="_blank">
|
||||
<div purpose="quote-attribution" class="d-flex flex-row align-items-center">
|
||||
|
|
|
|||
18
website/views/pages/transparency.ejs
vendored
18
website/views/pages/transparency.ejs
vendored
|
|
@ -10,17 +10,17 @@
|
|||
<h3>How does it affect me?</h3>
|
||||
<p>Your IT team has to maintain your computer and keep it compliant with a bunch of security requirements. But they also realize you have a job to do. So they want to be as un-intrusive as possible.</p>
|
||||
<p>That’s why they chose Fleet. It’s compatible with everything, including Linux, so it doesn’t limit what operating system you use. And it’s open source, meaning if you want to, you can <a href="https://github.com/fleetdm/fleet" target="_blank">take it apart</a> and see what it’s doing to your computer.</p>
|
||||
<div purpose="swag-cta" v-if="showSwagForm">
|
||||
<!-- <div purpose="swag-cta" v-if="showSwagForm">
|
||||
<a purpose="swag-link" style="text-decoration: none;" class="d-inline" href="https://kqphpqst851.typeform.com/to/Whm2imZc">
|
||||
<div class="d-flex flex-row justify-content-center align-items-center">
|
||||
<div><img alt="A tumbler with a Fleet logo" src="/images/icon-fleet-tumbler-55x51@2x.png"></div>
|
||||
<div class="d-flex flex-column">
|
||||
<strong>Get a Fleet tumbler</strong>
|
||||
<animated-arrow-button target="_blank" style="font-weight: 400;">It's free</animated-arrow-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row justify-content-center align-items-center">
|
||||
<div><img alt="A tumbler with a Fleet logo" src="/images/icon-fleet-tumbler-55x51@2x.png"></div>
|
||||
<div class="d-flex flex-column">
|
||||
<strong>Get a Fleet tumbler</strong>
|
||||
<animated-arrow-button target="_blank" style="font-weight: 400;">It's free</animated-arrow-button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div purpose="feature-image">
|
||||
<img alt="Information about a users device in Fleet." src="/images/better-hero-image-468x380@2x.png">
|
||||
|
|
|
|||
Loading…
Reference in a new issue