diff --git a/.github/workflows/check-tuf-timestamps.yml b/.github/workflows/check-tuf-timestamps.yml index 1a62757cc188..b9f9720aa82e 100644 --- a/.github/workflows/check-tuf-timestamps.yml +++ b/.github/workflows/check-tuf-timestamps.yml @@ -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: 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 - - - 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 }}" + - 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") + + 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 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 "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") + + 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 + + - 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 - - - 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 + + - 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 + ] + } + 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 diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml index 487cdac1ad11..4de0e454d061 100644 --- a/.github/workflows/dogfood-gitops.yml +++ b/.github/workflows/dogfood-gitops.yml @@ -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 }} \ No newline at end of file + DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET }} + MANAGED_CHROME_ENROLLMENT_TOKEN: ${{ secrets.CLOUD_MANAGEMENT_ENROLLMENT_TOKEN }} diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index 5633467a883e..8f28f858fec5 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -19,20 +19,17 @@ defaults: shell: bash env: - FLEET_DESKTOP_VERSION: 1.35.0 + FLEET_DESKTOP_VERSION: 1.36.0 permissions: contents: read jobs: desktop-macos: - # Set macOS version to '12' (current equivalent to macos-latest) for + # Set macOS version to '13' (previously was macos-12, and it was deprecated) for # building the binary. This ensures compatibility with macOS version 13 and # later, avoiding runtime errors on systems using macOS 13 or newer. - # - # Note: Update this version to '13' once GitHub marks macOS 13 as stable - # or if we revise our minimum supported macOS version. - runs-on: macos-12 + runs-on: macos-13 steps: - name: Harden Runner diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index 084b1ea56b53..1e42bd3fbe29 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -186,9 +186,112 @@ jobs: name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-summary-test-log path: /tmp/summary.txt + # Based on https://github.com/micromdm/nanomdm/blob/main/.github/workflows/on-push-pr.yml#L87 + test-go-nanomdm: + runs-on: 'ubuntu-latest' + services: + mysql: + image: mysql:8.0.36 + env: + MYSQL_RANDOM_ROOT_PASSWORD: yes + MYSQL_DATABASE: testdb + MYSQL_USER: testuser + MYSQL_PASSWORD: testpw + ports: + - 3800:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + env: + MYSQL_PWD: testpw + PORT: 3800 + RACE_ENABLED: true + GO_TEST_TIMEOUT: 20m + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Install Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version-file: 'go.mod' + + - name: verify mysql + run: | + while ! mysqladmin ping --host=localhost --port=$PORT --protocol=TCP --silent; do + sleep 1 + done + + - name: mysql schema + run: | + mysql --version + mysql --user=testuser --host=localhost --port=$PORT --protocol=TCP testdb < ./server/mdm/nanomdm/storage/mysql/schema.sql + + - name: set test dsn + run: echo "NANOMDM_MYSQL_STORAGE_TEST_DSN=testuser:testpw@tcp(localhost:$PORT)/testdb" >> $GITHUB_ENV + + - name: Run Go tests + run: | + go test -v -parallel 8 -race=$RACE_ENABLED -timeout=$GO_TEST_TIMEOUT \ + -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/fleetdm/fleet/v4/server/mdm/nanomdm/... \ + ./server/mdm/nanomdm/storage/mysql 2>&1 | tee /tmp/gotest.log + + - name: Save coverage + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: nanomdm-coverage + path: ./coverage.txt + if-no-files-found: error + + - name: Generate summary of errors + if: failure() + run: | + c1grep() { grep "$@" || test $? = 1; } + c1grep -oP 'FAIL: .*$' /tmp/gotest.log > /tmp/summary.txt + c1grep 'test timed out after' /tmp/gotest.log >> /tmp/summary.txt + c1grep 'fatal error:' /tmp/gotest.log >> /tmp/summary.txt + c1grep -A 10 'panic: runtime error: ' /tmp/gotest.log >> /tmp/summary.txt + c1grep ' FAIL\t' /tmp/gotest.log >> /tmp/summary.txt + GO_FAIL_SUMMARY=$(head -n 5 /tmp/summary.txt | sed ':a;N;$!ba;s/\n/\\n/g') + echo "GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY" + if [[ -z "$GO_FAIL_SUMMARY" ]]; then + GO_FAIL_SUMMARY="unknown, please check the build URL" + fi + GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY envsubst < .github/workflows/config/slack_payload_template.json > ./payload.json + + - name: Slack Notification + if: github.event.schedule == '0 4 * * *' && failure() + uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 + with: + payload-file-path: ./payload.json + env: + JOB_STATUS: ${{ job.status }} + EVENT_URL: ${{ github.event.pull_request.html_url || github.event.head.html_url }} + RUN_URL: https://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + + - name: Upload test log + if: always() + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: nanomdm-test-log + path: /tmp/gotest.log + if-no-files-found: error + + - name: Upload summary test log + if: always() + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: nanomdm-summary-test-log + path: /tmp/summary.txt + # We upload all backend coverage in one step so that we're less like to end up in a situation with a partial coverage report. upload-coverage: - needs: [test-go] + needs: [test-go, test-go-nanomdm] runs-on: ubuntu-latest steps: - name: Checkout Code diff --git a/.github/workflows/tuf-update-timestamp.yaml b/.github/workflows/tuf-update-timestamp.yaml new file mode 100644 index 000000000000..692872aadd00 --- /dev/null +++ b/.github/workflows/tuf-update-timestamp.yaml @@ -0,0 +1,59 @@ +# This workflow update the timestamp of the TUF repository at https://tuf.fleetctl.com +name: Update TUF timestamp + +on: + schedule: + - cron: "0 14 * * TUE" # Every Tuesday at 2 PM UTC + workflow_dispatch: # Manual + +defaults: + run: + # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +env: + AWS_REGION: us-east-1 + AWS_IAM_ROLE: arn:aws:iam::142412512209:role/github-actions-role + +permissions: + id-token: write # This is required for aws-actions/configure-aws-credentials + +jobs: + tuf-update-timestamp: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0 + with: + role-to-assume: ${{ env.AWS_IAM_ROLE }} + aws-region: ${{ env.AWS_REGION }} + + - name: Install fleetctl + run: npm install -g fleetctl + + - name: Pull metadata files + run: | + mkdir -p keys repository staged + aws s3 cp s3://fleet-tuf-repo/timestamp.json ./repository/timestamp.json + aws s3 cp s3://fleet-tuf-repo/snapshot.json ./repository/snapshot.json + aws s3 cp s3://fleet-tuf-repo/targets.json ./repository/targets.json + aws s3 cp s3://fleet-tuf-repo/root.json ./repository/root.json + cat ./repository/timestamp.json + + - name: Update timestamp + env: + BASE64_ENCRYPTED_TIMESTAMP_KEY_CONTENTS: ${{ secrets.BASE64_ENCRYPTED_TIMESTAMP_KEY }} + FLEET_TIMESTAMP_PASSPHRASE: ${{ secrets.TUF_TIMESTAMP_PASSPHRASE }} + run: | + echo "$BASE64_ENCRYPTED_TIMESTAMP_KEY_CONTENTS" | base64 -d > ./keys/timestamp.json + fleetctl updates timestamp --path . + + - name: Push timestamp.json + run: | + cat ./repository/timestamp.json + aws s3 cp ./repository/timestamp.json s3://fleet-tuf-repo/timestamp.json diff --git a/CODEOWNERS b/CODEOWNERS index 68cb1188023c..33a8c371f23d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -69,7 +69,6 @@ go.mod @fleetdm/go /docs/REST\ API/rest-api.md @rachaelshaw # « REST API reference documentation /docs/Contributing/API-for-contributors.md @rachaelshaw # « Advanced / contributors-only API reference documentation /schema @eashaw # « Data tables (osquery/fleetd schema) documentation -/docs/Deploy/_kubernetes/ @dherder # « Kubernetes best practice /render.yaml @edwardsb ############################################################################################## diff --git a/articles/enforce-disk-encryption.md b/articles/enforce-disk-encryption.md index 8dd2b6419ff5..5500d45286b0 100644 --- a/articles/enforce-disk-encryption.md +++ b/articles/enforce-disk-encryption.md @@ -2,21 +2,19 @@ _Available in Fleet Premium_ -In Fleet, you can enforce disk encryption for your macOS and Windows hosts. +In Fleet, you can enforce disk encryption for your macOS and Windows hosts, and verify disk encryption for Ubuntu Linux and Fedora Linux hosts. -> Apple calls this [FileVault](https://support.apple.com/en-us/HT204837) and Microsoft calls this [BitLocker](https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/). +> Apple calls this [FileVault](https://support.apple.com/en-us/HT204837), Microsoft calls this [BitLocker](https://learn.microsoft.com/en-us/windows/security/operating-system-security/data-protection/bitlocker/), and Linux typically uses [LUKS](https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup) (Linux Unified Key Setup). -When disk encryption is enforced, hosts’ disk encryption keys will be stored in Fleet. +When disk encryption is enforced, hosts' disk encryption keys will be stored in Fleet. -For macOS hosts that automatically enroll, disk encryption is enforced during Setup Assistant. - -For Windows, disk encryption is enforced on the C: volume (default system/OS drive). +For macOS hosts that automatically enroll, disk encryption is enforced during Setup Assistant. For Windows, disk encryption is enforced on the C: volume (default system/OS drive). On Linux, encryption requires user interaction to encrypt the device with LUKS. ## Enforce disk encryption You can enforce disk encryption using the Fleet UI, Fleet API, or [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops). -Fleet UI: +#### Fleet UI: 1. In Fleet, head to the **Controls > OS settings > Disk encryption** page. @@ -24,7 +22,9 @@ Fleet UI: 3. Check the box next to **Turn on** and select **Save**. -Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#update-disk-encryption-enforcement). +#### Fleet API: + +API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#update-disk-encryption-enforcement). ### Disk encryption status @@ -42,10 +42,28 @@ In the Fleet UI, head to the **Controls > OS settings > Disk encryption** tab. Y * Removing enforcement (pending): the host will receive the MDM command to remove the disk encryption profile when the host comes online. -* Failed: hosts that are failed to enforce disk encryption. +* Failed: hosts that failed to enforce disk encryption. You can click each status to view the list of hosts for that status. +## Enforce disk encryption on Linux + +To enforce disk encryption on Ubuntu Linux and Fedora Linux devices, Fleet supports Linux Unified Key Setup (LUKS) for encrypting volumes. Support for Ubuntu 20.04 is coming soon. + +1. Share [this step-by-step guide](https://fleetdm.com/learn-more-about/encrypt-linux-device) with end users setting up a work computer running Ubuntu Linux or Fedora Linux. + +> Note that full disk encryption can only enabled during operating system setup. If the operating system has already been installed, the end user will be required to re-install the OS to enable disk encryption. + +2. Once the user encrypts the disk, Fleet will initiate a key escrow process through Fleet Desktop: + * Fleet Desktop prompts the user to enter their current encryption passphrase. + * A new encryption passphrase is generated and added as a LUKS keyslot for the encrypted volume. + * The new passphrase is securely stored in Fleet. + +3. Fleet verifies that the encryption is complete, and the key has been escrowed. Once successful, the host's status will be updated to "Verified" in the disk encryption status table. + +> Note: LUKS allows multiple passphrases for decrypting the volume. The original passphrase remains active along with the escrowed passphrase created by Fleet. + + ## View disk encryption key How to view the disk encryption key: @@ -54,6 +72,8 @@ How to view the disk encryption key: 2. On the **Host details** page, select **Actions > Show disk encryption key**. +> This action is logged in the activity log for security auditing purposes. + ## Migrate macOS hosts When migrating macOS hosts from another MDM solution, in order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must log out or restart their device. @@ -65,4 +85,4 @@ Share [these guided instructions](https://fleetdm.com/guides/mdm-migration#how-t - + diff --git a/articles/fleet-4.40.0.md b/articles/fleet-4.40.0.md index a532533165a7..6200a5663529 100644 --- a/articles/fleet-4.40.0.md +++ b/articles/fleet-4.40.0.md @@ -1,6 +1,8 @@ # Fleet 4.40.0 | More Data, Rapid Security Response, CIS Benchmark updates. -![Fleet 4.40.0](../website/assets/images/articles/fleet-4.40.0-1600x900@2x.png) +
+ +
Fleet 4.40.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.40.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.47.0.md b/articles/fleet-4.47.0.md index 01a2fb3e62f6..69f42984e39d 100644 --- a/articles/fleet-4.47.0.md +++ b/articles/fleet-4.47.0.md @@ -1,6 +1,8 @@ # Fleet 4.47.0 | Cross-platform remote wipe, vulnerabilities page, and scripting improvements. -![Fleet 4.47.0](../website/assets/images/articles/fleet-4.47.0-1600x900@2x.png) +
+ +
Fleet 4.47.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.47.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.48.0.md b/articles/fleet-4.48.0.md index 4bfdcb552767..16867fb73fa2 100644 --- a/articles/fleet-4.48.0.md +++ b/articles/fleet-4.48.0.md @@ -1,6 +1,8 @@ # Fleet 4.48.0 | IdP local account creation, VS Code extensions. -![Fleet 4.48.0](../website/assets/images/articles/fleet-4.48.0-1600x900@2x.png) +
+ +
Fleet 4.48.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.48.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.49.0.md b/articles/fleet-4.49.0.md index bda46c13e211..3694b32f6d60 100644 --- a/articles/fleet-4.49.0.md +++ b/articles/fleet-4.49.0.md @@ -1,6 +1,8 @@ # Fleet 4.49.0 | VulnCheck's NVD++, device health API, `fleetd` data parsing. -![Fleet 4.49.0](../website/assets/images/articles/fleet-4.49.0-1600x900@2x.png) +
+ +
Fleet 4.49.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.49.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.50.0.md b/articles/fleet-4.50.0.md index 661b08cc38b5..f33119474997 100644 --- a/articles/fleet-4.50.0.md +++ b/articles/fleet-4.50.0.md @@ -1,6 +1,8 @@ # Fleet 4.50.0 | Security agent deployment, AI descriptions, and Mac Admins SOFA support. -![Fleet 4.50.0](../website/assets/images/articles/fleet-4.50.0-1600x900@2x.png) +
+ +
Fleet 4.50.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.50.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.51.0.md b/articles/fleet-4.51.0.md index 7f43b9f66be3..4d7e046ae4ff 100644 --- a/articles/fleet-4.51.0.md +++ b/articles/fleet-4.51.0.md @@ -1,6 +1,8 @@ # Fleet 4.51.0 | Global activity webhook, macOS TCC table, and software self-service. -![Fleet 4.51.0](../website/assets/images/articles/fleet-4.51.0-1600x900@2x.png) +
+ +
Fleet 4.51.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.51.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.53.0.md b/articles/fleet-4.53.0.md index 3a238a69aac5..88c2cf6158ba 100644 --- a/articles/fleet-4.53.0.md +++ b/articles/fleet-4.53.0.md @@ -1,6 +1,8 @@ # Fleet 4.53.0 | Better vuln matching, multi-issue hosts, & `fleetd` logs as tables. -![Fleet 4.53.0](../website/assets/images/articles/fleet-4.53.0-1600x900@2x.png) +
+ +
Fleet 4.53.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.53.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.54.0.md b/articles/fleet-4.54.0.md index 798090428f69..4f2f2b003a50 100644 --- a/articles/fleet-4.54.0.md +++ b/articles/fleet-4.54.0.md @@ -1,6 +1,8 @@ # Fleet 4.54.0 | Target hosts via label exclusion, arm64 support, script execution time. -![Fleet 4.54.0](../website/assets/images/articles/fleet-4.54.0-1600x900@2x.png) +
+ +
Fleet 4.54.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.54.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.55.0.md b/articles/fleet-4.55.0.md index 309e0e70df7f..8f76ef577216 100644 --- a/articles/fleet-4.55.0.md +++ b/articles/fleet-4.55.0.md @@ -1,6 +1,8 @@ # Fleet 4.55.0 | MySQL 8, arm64 support, FileVault improvements, VPP support. -![Fleet 4.55.0](../website/assets/images/articles/fleet-4.55.0-1600x900@2x.png) +
+ +
Fleet 4.55.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.55.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.56.0.md b/articles/fleet-4.56.0.md index 14089ad9d6e6..152347f621be 100644 --- a/articles/fleet-4.56.0.md +++ b/articles/fleet-4.56.0.md @@ -1,6 +1,8 @@ # Fleet 4.56.0 | Enhanced MDM migration, Exact CVE Search, and Self-Service VPP Apps. -![Fleet 4.56.0](../website/assets/images/articles/fleet-4.56.0-1600x900@2x.png) +
+ +
Fleet 4.56.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.56.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.57.0.md b/articles/fleet-4.57.0.md index 4c9f959f9a44..8fa3366ae7c9 100644 --- a/articles/fleet-4.57.0.md +++ b/articles/fleet-4.57.0.md @@ -1,6 +1,8 @@ # Fleet 4.57.0 | Software improvements, policy automation, GitLab support. -![Fleet 4.57.0](../website/assets/images/articles/fleet-4.57.0-1600x900@2x.png) +
+ +
Fleet 4.57.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.57.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.58.0.md b/articles/fleet-4.58.0.md index cfd2b7f9f644..05efdcc6917c 100644 --- a/articles/fleet-4.58.0.md +++ b/articles/fleet-4.58.0.md @@ -1,6 +1,8 @@ # Fleet 4.58.0 | Run script on policy failure, Fleet-maintained apps, Sequoia firewall status. -![Fleet 4.58.0](../website/assets/images/articles/fleet-4.58.0-1600x900@2x.png) +
+ +
Fleet 4.58.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.58.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/fleet-4.59.0.md b/articles/fleet-4.59.0.md index 749c98113672..49549ed374d9 100644 --- a/articles/fleet-4.59.0.md +++ b/articles/fleet-4.59.0.md @@ -96,7 +96,7 @@ SET i.software_title_name = COALESCE(a.details->>"$.software_title", i.software_ ## Ready to upgrade? -Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.58.0. +Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.59.0. diff --git a/articles/fleet-4.60.0.md b/articles/fleet-4.60.0.md new file mode 100644 index 000000000000..a54b3dca6249 --- /dev/null +++ b/articles/fleet-4.60.0.md @@ -0,0 +1,83 @@ +# Fleet 4.60.0 | Escrow Linux disk encryption keys, custom targets for OS settings, scripts preview + +![Fleet 4.60.0](../website/assets/images/articles/fleet-4.60.0-1600x900@2x.png) + +Fleet 4.60.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.60.0) or continue reading to get the highlights. +For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. + +## Highlights +- Escrow Linux disk encryption keys +- Custom targets for OS settings +- Preview scripts before run + +### Escrow Linux disk encryption keys + +Fleet now supports escrowing the disk encryption keys for Linux (Ubuntu and Fedora) workstations. This means teams can access encrypted data without needing the local password when an employee leaves, simplifying handoffs and ensuring critical data remains accessible while protected. Learn more in the guide [here](https://fleetdm.com/guides/enforce-disk-encryption). + +### Custom targets for OS settings + +With Fleet, you can now use a new "include any" label option to target OS settings (configuration profiles) to specific hosts within a team. This added flexibility allows for finer control over which OS settings apply to which hosts, making it easier to tweak configurations without disrupting broader baselines (Fleet [teams](https://fleetdm.com/guides/teams)). + +### Preview scripts before run + +Fleet now provides the ability to preview scripts directly on the **Host details** or **Scripts** page. This quick-view feature reduces the risk of errors by letting you verify the script is correct before running it, saving time and ensuring smoother operations. + +## Changes + +### Endpoint operations +- Added support for `labels_include_any` to gitops. +- Added major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation). +- Added activity item for `fleetd` enrollment with host serial and display name. +- Added capability for Fleet to serve YARA rules to agents over HTTPS authenticated via node key (requires osquery 5.14+). +- Added a query to allow users to turn on/off automations while being transparent of the current log destination. +- Updated UI to allow users to view scripts (from both the scripts page and host details page) without downloading them. +- Updated activity feed to generate an activity when activity automations are enabled, edited, or disabled. +- Cancelled pending script executions when a script is edited or deleted. + +### Device management (MDM) +- Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy. +- Added info banner for cloud customers to help with their windows autoenrollment setup. +- Added DB support for "include any" label profile deployment. +- Added support for "include any" label/profile relationships to the profile reconciliation machinery. +- Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. +- Added indicator of how fresh a software title's host and version counts are on the title's details page. +- Added UI for allowing users to install custom profiles on hosts that include any of the defined labels. +- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. +- Added support for deb packages compressed with zstd. + +### Vulnerability management +- Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`). + +### Bug fixes and improvements +- Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required. +- Improved performance of adding and removing profiles to large teams by an order of magnitude. +- Disabled accessibility via keyboard for forms that are disabled via a slider. +- Updated software batch endpoint status code from 200 (OK) to 202 (Accepted). +- Updated a package used for testing (msw) to improve security. +- Updated to reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04. +- Updated GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there are more than 1 ABM tokens in Fleet. +- Dismissed error flash on the my device page when navigating to another URL. +- Modified the Fleet setup experience feature to not run if there is no software or script configured for the setup experience. +- Set a more accurate minimum height for the Add hosts > ChromeOS > Policy for extension field, avoiding a scrollbar. +- Added UI prompt for user to reenter the password if SCEP/NDES url or username has changed. +- Updated ABM public key to download as as PEM format instead of CRT. +- Fixed issue with uploading macOS software packages that do not have a top level `Distribution.xml`, but do have a top level `PackageInfo.xml`. For example, Okta Verify.app. +- Fixed some cases where Fleet Maintained Apps generated incorrect uninstall scripts. +- Fixed a bug where a device that was removed from ABM and then added back wouldn't properly re-enroll in Fleet MDM. +- Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. +- Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica. +- Fixed the MDM configuration profiles deployment when based on excluded labels. +- Fixed gitops path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere). +- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM IdP integration was enabled. +- Fixed a bug where users would be allowed to attempt an install of an App Store app on a host that was not MDM enrolled. + +## Ready to upgrade? + +Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.60.0. + + + + + + + \ No newline at end of file diff --git a/articles/fleet-usage-statistics.md b/articles/fleet-usage-statistics.md index 1db24222dee1..80e3e6328433 100644 --- a/articles/fleet-usage-statistics.md +++ b/articles/fleet-usage-statistics.md @@ -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, diff --git a/articles/install-fleet-maintained-apps-on-macos-hosts.md b/articles/install-fleet-maintained-apps-on-macos-hosts.md index b0ac7a0b6488..8dc8eecccd1d 100644 --- a/articles/install-fleet-maintained-apps-on-macos-hosts.md +++ b/articles/install-fleet-maintained-apps-on-macos-hosts.md @@ -4,7 +4,9 @@ _Available in Fleet Premium_ In Fleet, you can install Fleet-maintained apps on macOS hosts without the need for manual uploads or extra configuration. This simplifies the process and adds another source of applications for your fleet. -Fleet starts with some of the most common and popular apps, enabling you to pull directly from this curated list and install them on your hosts without any additional configuration. +Fleet maintains these [celebrity apps](https://github.com/fleetdm/fleet/blob/main/server/mdm/maintainedapps/apps.json), enabling you to pull directly from this curated list and install them on your hosts without any additional configuration. + +> Currently, these apps are only supported for Apple Silicon Macs: 1Password, Brave, Docker Desktop, Figma, Microsoft Visual Studio (VS) Code, Notion, Postman, Slack, and Zoom. ## Add a Fleet-maintained app diff --git a/articles/introducing-workbrew.md b/articles/introducing-workbrew.md new file mode 100644 index 000000000000..218c9c1d3a47 --- /dev/null +++ b/articles/introducing-workbrew.md @@ -0,0 +1,42 @@ +# Introducing Workbrew: bringing enterprise control to Homebrew deployments + +![Fleet and Workbrew](../website/assets/images/articles/fleet-and-workbrew-1600x900@2x.png) + +[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 + +Homebrew has become an essential part of the developer’s toolkit by simplifying the installation and maintenance of software packages. However, as organizations grow, managing Homebrew installations across an entire fleet of devices introduces complexity, security risks, and compliance challenges. + +Enter Workbrew. Their platform provides a centralized way for IT and security teams to manage and deploy Homebrew across their organizations. With features like: + +- **Fleet-wide dashboards** to monitor devices, packages, and licenses. +- **MDM integrations (including Fleet!)** for automated synchronization of device data. +- **Vulnerability detection** and policy enforcement to enhance security. +- **Remote management** to install, upgrade, and remove any of the tens of thousands of packages in the `brew` ecosystem with ease. + +Workbrew enables companies to maintain the agility developers love while ensuring security and compliance standards are met. + +As noted in the article, Workbrew brings a customizable solution to organizations struggling with “[shadow IT](https://techcrunch.com/2015/09/25/its-time-to-embrace-not-fear-shadow-it/)” risks. By offering a fleet dashboard, vulnerability detection, and deep integrations with tools like [Fleet](https://fleetdm.com/device-management), Workbrew helps companies maintain visibility and control over Homebrew deployments at scale. Whether it’s ensuring compliance in regulated industries or automating package installations for remote teams, Workbrew is paving the way for safer, smarter IT management. + +![Workbrew console](../website/assets/images/articles/workbrew-console-3412x2020px.png) + +At Fleet, we’re excited to support Workbrew’s efforts. Our [integration](https://fleetdm.com/integrations) ensures that Workbrew users can easily sync device data, enabling seamless management across teams. Workbrew’s approach resonates with our belief in open-source and transparent tools for IT and security. + +## Why this matters for IT teams + +Managing Homebrew deployments used to be a manual process fraught with unknowns. IT professionals often found themselves asking: +- What’s actually installed on our devices? +- Are there unpatched vulnerabilities in our software? +- How do we enforce security policies without stifling developer productivity? + +With Workbrew, these questions have answers. And for organizations already using Fleet, the integration creates a powerful synergy that brings even more value to your existing workflows. Together, Fleet and Workbrew give you the tools to confidently oversee and manage every device, app, and package across your organization. It’s an essential step for any organization looking to balance developer flexibility with operational controls. + +To learn more about how Workbrew and Fleet can work together, visit [Workbrew’s website](https://www.workbrew.com) + + + + + + + diff --git a/articles/linux-disk-encryption-end-user.md b/articles/linux-disk-encryption-end-user.md new file mode 100644 index 000000000000..0fd8ffb91389 --- /dev/null +++ b/articles/linux-disk-encryption-end-user.md @@ -0,0 +1,56 @@ +# Encrypt your Fleet-managed Linux device + +> This guide is intended for new device setup. If the operating system has already been installed without enabling disk encryption, you will need to re-install in order to turn on full disk encryption. + + +LUKS (Linux Unified Key Setup) is a standard tool for encrypting Linux disks. It uses a "volume key" to encrypt your data, and this key is protected by passphrases. LUKS supports multiple passphrases, allowing you to securely share access or recover encrypted data. Fleet uses LUKS to ensure that only authorized users can access the data on your work computer. + +Fleet securely stores a passphrase to ensure that the data on your work computer is always recoverable. To get your computer set up for key escrow, you will first need to enable disk encryption on your end, then provide your encryption passphrase to Fleet. + +Follow the steps below to get set up. + + +## 1. Enable encryption during installation + + #### Ubuntu Linux + + - When installing Ubuntu, choose the option to "Use LVM with encryption." + - Set a strong passphrase when prompted. This passphrase will be used to encrypt your disk and is separate from your login password. + + ![Ubuntu setup "How do you want to install Ubuntu?" screen](../website/assets/images/articles/ubuntu-1-1200x675@2x.png) + + ![Ubuntu setup: Advanced features > Use LVM and encryption](../website/assets/images/articles/ubuntu-2-1200x675@2x.png) + + #### Fedora Linux + + - During Fedora installation, under **Installation destination** > **Encryption** select the "Encrypt my data" checkbox. + - Enter a secure passphrase when prompted. + + ![Fedora setup "Installation summary" screen](../website/assets/images/articles/fedora-1-1200x675@2x.png) + ![Fedora setup: Installation destination > Encryption > Encrypt my data](../website/assets/images/articles/fedora-2-1200x675@2x.png) + +## 2. Verify encryption + + - Once installation is complete, verify that your disk is encrypted by running: + ```bash + lsblk -o NAME,MOUNTPOINT,TYPE,SIZE,FSUSED,FSTYPE,ENCRYPTED + ``` + - **Ubuntu Linux**: Look for the root (`/`) partition, and confirm it is marked as encrypted. + - **Fedora Linux**: Ensure the `/` (root) and `/home` partitions are encrypted. + +## 3. Escrow your key with Fleet + + - Open Fleet Desktop. If your device is encrypted, you'll see a banner prompting you to escrow the key. + - Click **Create key**. Enter your existing encryption passphrase when prompted. + - Fleet will generate and securely store a new passphrase for recovery. This may take several minutes. A popup will appear when Fleet is done. + +Now, your encryption status will update to "verified" in Fleet Desktop, meaning that your recovery key has been successfully stored. + + + + + + + + + \ No newline at end of file diff --git a/articles/lock-wipe-hosts.md b/articles/lock-wipe-hosts.md index 519640b173e1..57e61ec8d2e7 100644 --- a/articles/lock-wipe-hosts.md +++ b/articles/lock-wipe-hosts.md @@ -18,7 +18,7 @@ where a host might have been lost or stolen, or to remotely prepare a device to ## Wipe a host -1. Navigate to the **Hosts** page by clicking the "Hosts" tab in the main navigation header. Find the device you want to lock. You can search by name, hostname, UUID, serial number, or private IP address in the search box in the upper right corner. +1. Navigate to the **Hosts** page by clicking the "Hosts" tab in the main navigation header. Find the device you want to wipe. You can search by name, hostname, UUID, serial number, or private IP address in the search box in the upper right corner. 2. Click the host to open the **Host Overview** page. 3. Click the **Actions** dropdown, then click **Wipe**. 4. Confirm that you want to wipe the device in the dialog. The host will now be marked with a "Wipe pending" badge. Once the wipe command is acknowledged by the host, the badge will update to "Wiped". @@ -29,12 +29,12 @@ where a host might have been lost or stolen, or to remotely prepare a device to To unlock a locked host: -1. Navigate to the **Hosts** page by clicking the "Hosts" tab in the main navigation header. Find the device you want to lock. You can search by name, hostname, UUID, serial number, or private IP address in the search box in the upper right corner. +1. Navigate to the **Hosts** page by clicking the "Hosts" tab in the main navigation header. Find the device you want to unlock. You can search by name, hostname, UUID, serial number, or private IP address in the search box in the upper right corner. 2. Click the host to open the **Host Overview** page. 3. Click the **Actions** menu, then click **Unlock**. - **macOS**: A dialog with the PIN will appear. Type the PIN into the device to unlock it. - **Windows and Linux**: The command to unlock the host will be queued and the host will unlock once it receives the command (no PIN needed). -5. When you click **Unlock**, the host will be marked with an "Unlock pending" badge. Once the host is unlocked and checks back in with Fleet, the "Unlock pending" badge will be removed. +5. When you click **Unlock**, Windows and Linux hosts will be marked with an "Unlock pending" badge. Once the host is unlocked and checks back in with Fleet, the "Unlock pending" badge will be removed. macOS hosts do not have an "Unlock pending" badge as they cannot be remotely unlocked (the PIN has to be typed into the device). ## Lock and wipe using `fleetctl` diff --git a/assets/fonts/inter/Inter-Bold.woff2 b/assets/fonts/inter/Inter-Bold.woff2 index 158cfcfc1d5d..b9e3cb3b1fde 100644 Binary files a/assets/fonts/inter/Inter-Bold.woff2 and b/assets/fonts/inter/Inter-Bold.woff2 differ diff --git a/assets/fonts/inter/Inter-Regular-Italic.woff2 b/assets/fonts/inter/Inter-Regular-Italic.woff2 index 490c1a83431d..9a1ad2167a9a 100644 Binary files a/assets/fonts/inter/Inter-Regular-Italic.woff2 and b/assets/fonts/inter/Inter-Regular-Italic.woff2 differ diff --git a/assets/fonts/inter/Inter-Regular.woff2 b/assets/fonts/inter/Inter-Regular.woff2 index 9f3b53d398c7..2bcd222ecfae 100644 Binary files a/assets/fonts/inter/Inter-Regular.woff2 and b/assets/fonts/inter/Inter-Regular.woff2 differ diff --git a/assets/fonts/inter/Inter-Semibold.woff2 b/assets/fonts/inter/Inter-Semibold.woff2 index 8be198d44466..fbae113d2855 100644 Binary files a/assets/fonts/inter/Inter-Semibold.woff2 and b/assets/fonts/inter/Inter-Semibold.woff2 differ diff --git a/changes/18539-font-bug b/changes/18539-font-bug new file mode 100644 index 000000000000..682746606853 --- /dev/null +++ b/changes/18539-font-bug @@ -0,0 +1 @@ +* Update Inter font to latest version for woff2 files \ No newline at end of file diff --git a/changes/21340-improve-nano-enrollments-last-seen-at-update b/changes/21340-improve-nano-enrollments-last-seen-at-update new file mode 100644 index 000000000000..55a978a05b4f --- /dev/null +++ b/changes/21340-improve-nano-enrollments-last-seen-at-update @@ -0,0 +1 @@ +* Improve performance of updating the `nano_enrollments.last_seen_at` timestamp of Apple MDM devices by an order of magnitude under load. diff --git a/changes/21795-resend-config-profile-api b/changes/21795-resend-config-profile-api new file mode 100644 index 000000000000..0612554c37c9 --- /dev/null +++ b/changes/21795-resend-config-profile-api @@ -0,0 +1 @@ +* Update resend config profile API from hosts/[hostid}/configuration_profiles/resend/{uuid} to hosts/{hostid}/configuration_profiles/{uuid}/resend \ No newline at end of file diff --git a/changes/22361-os-update-ade-sso b/changes/22361-os-update-ade-sso new file mode 100644 index 000000000000..40221866fb93 --- /dev/null +++ b/changes/22361-os-update-ade-sso @@ -0,0 +1,2 @@ +- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM + IdP integration was enabled. diff --git a/changes/22527-policy-automation-ui-improvements b/changes/22527-policy-automation-ui-improvements new file mode 100644 index 000000000000..6d56f7efa646 --- /dev/null +++ b/changes/22527-policy-automation-ui-improvements @@ -0,0 +1 @@ +- Update help text for policy automation Install software and Run script modals diff --git a/changes/22702-linux-encryption-frontend b/changes/22702-linux-encryption-frontend new file mode 100644 index 000000000000..a35d2423751b --- /dev/null +++ b/changes/22702-linux-encryption-frontend @@ -0,0 +1 @@ +- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. diff --git a/changes/22819-delete-modal b/changes/22819-delete-modal new file mode 100644 index 000000000000..a1dc4e5b610f --- /dev/null +++ b/changes/22819-delete-modal @@ -0,0 +1 @@ +- Fleet UI: Better information on what deleting a host does diff --git a/changes/23027-settings-empty-states b/changes/23027-settings-empty-states new file mode 100644 index 000000000000..ecc6736d0512 --- /dev/null +++ b/changes/23027-settings-empty-states @@ -0,0 +1 @@ +* Improve side nav empty state UI under `/settings` \ No newline at end of file diff --git a/changes/23158-turn-off-windows-mdm-err b/changes/23158-turn-off-windows-mdm-err new file mode 100644 index 000000000000..4bc7d28c8eef --- /dev/null +++ b/changes/23158-turn-off-windows-mdm-err @@ -0,0 +1 @@ +- Adds a clearer error message when users attempt to turn MDM off on a Windows host. \ No newline at end of file diff --git a/changes/23458-additional-stats b/changes/23458-additional-stats new file mode 100644 index 000000000000..73587d4def28 --- /dev/null +++ b/changes/23458-additional-stats @@ -0,0 +1 @@ +- Added additional statistics item for number of saved queries diff --git a/changes/23621-unlock-text b/changes/23621-unlock-text new file mode 100644 index 000000000000..6715062fdff8 --- /dev/null +++ b/changes/23621-unlock-text @@ -0,0 +1 @@ +- Fixes an issue with the copy for the activity generated by viewing a locked macOS host's PIN. \ No newline at end of file diff --git a/changes/23733-apple-app-store-icons b/changes/23733-apple-app-store-icons new file mode 100644 index 000000000000..f9b062ff8232 --- /dev/null +++ b/changes/23733-apple-app-store-icons @@ -0,0 +1 @@ +- Fleet UI: Remove image borders that are included in Apple's app store icons diff --git a/changes/23749-fix-learn-more-link b/changes/23749-fix-learn-more-link new file mode 100644 index 000000000000..d10d50f7010a --- /dev/null +++ b/changes/23749-fix-learn-more-link @@ -0,0 +1 @@ +- Fleet UI: Fix learn more about JIT provisioning link diff --git a/changes/23758-use-fleethttp-client-for-apns-push-notifications b/changes/23758-use-fleethttp-client-for-apns-push-notifications new file mode 100644 index 000000000000..08a6eebba689 --- /dev/null +++ b/changes/23758-use-fleethttp-client-for-apns-push-notifications @@ -0,0 +1 @@ +* Fixed a bug where the HTTP client used for MDM APNs push notifications did not support using a configured proxy. diff --git a/changes/23787-script-name b/changes/23787-script-name new file mode 100644 index 000000000000..af50855badf3 --- /dev/null +++ b/changes/23787-script-name @@ -0,0 +1,2 @@ +- Fixes a bug where the name of the setup experience script was not showing up in the activity for + that script execution. \ No newline at end of file diff --git a/changes/23834-improve-label-flag-validation b/changes/23834-improve-label-flag-validation new file mode 100644 index 000000000000..5d8d8e4b0ac5 --- /dev/null +++ b/changes/23834-improve-label-flag-validation @@ -0,0 +1 @@ +* Improved label validation when running live queries. Previously, when passing label(s) that do not exist, the labels were ignored. Now, an error is returned indicating which labels were not found. This change affects both the API and `fleetctl query` command. \ No newline at end of file diff --git a/changes/23905-update-nanomdm b/changes/23905-update-nanomdm new file mode 100644 index 000000000000..5399590bdc9b --- /dev/null +++ b/changes/23905-update-nanomdm @@ -0,0 +1 @@ +Update nanomdm dependency with latest bug fixes and improvements. diff --git a/changes/23942-wrong-link b/changes/23942-wrong-link new file mode 100644 index 000000000000..f7ac16758264 --- /dev/null +++ b/changes/23942-wrong-link @@ -0,0 +1 @@ +- Updates a link in the Fleet-maintained apps UI to point to the correct place. \ No newline at end of file diff --git a/changes/23967-doc-firefox_preferences-linux-windows b/changes/23967-doc-firefox_preferences-linux-windows new file mode 100644 index 000000000000..3faa0f6980c0 --- /dev/null +++ b/changes/23967-doc-firefox_preferences-linux-windows @@ -0,0 +1 @@ +* doc: document firefox_preferences table for Linux and Windows platforms diff --git a/changes/24009-gh-translation b/changes/24009-gh-translation new file mode 100644 index 000000000000..103bd7b6ebfc --- /dev/null +++ b/changes/24009-gh-translation @@ -0,0 +1 @@ +* Fixed an issue where the github cli software name was not matching against the cpe vulnerability name \ No newline at end of file diff --git a/changes/24024-bypass-setup-experience-if-empty b/changes/24024-bypass-setup-experience-if-empty new file mode 100644 index 000000000000..319df88c1c91 --- /dev/null +++ b/changes/24024-bypass-setup-experience-if-empty @@ -0,0 +1,2 @@ +* Bypass the setup experience UI if there is no setup experience item to process (no software to install, no script to execute), so that releasing the device is done without going through that window. +* Fixed releasing a DEP-enrolled macOS device if mTLS is configured for `fleetd`. diff --git a/changes/24024-no-setup-exp b/changes/24024-no-setup-exp new file mode 100644 index 000000000000..44ab42bcf059 --- /dev/null +++ b/changes/24024-no-setup-exp @@ -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. \ No newline at end of file diff --git a/changes/24093-clear-policy-automation b/changes/24093-clear-policy-automation new file mode 100644 index 000000000000..4d77791615e3 --- /dev/null +++ b/changes/24093-clear-policy-automation @@ -0,0 +1 @@ +- Fleet UI: Fix ability to clear policy automation that empties webhook URL diff --git a/changes/24109-drop-duplicate-indexes b/changes/24109-drop-duplicate-indexes new file mode 100644 index 000000000000..df813981a494 --- /dev/null +++ b/changes/24109-drop-duplicate-indexes @@ -0,0 +1 @@ +Removed duplicate indexes from the database schema. diff --git a/changes/24248-host-details-encryption-banner b/changes/24248-host-details-encryption-banner new file mode 100644 index 000000000000..7de593417708 --- /dev/null +++ b/changes/24248-host-details-encryption-banner @@ -0,0 +1,2 @@ +* Only show the "follow instructions on My device" banner for Linux hosts whose disks are encrypted +but for which Fleet hasn't escrowed a valid key. diff --git a/changes/jve-fix-typo b/changes/jve-fix-typo new file mode 100644 index 000000000000..79379dadc57b --- /dev/null +++ b/changes/jve-fix-typo @@ -0,0 +1 @@ +- Fixes a typo in the loading modal when adding a Fleet-maintained app. \ No newline at end of file diff --git a/cmd/cpe/generate.go b/cmd/cpe/generate.go index b2ed44ced76a..5018f0200367 100644 --- a/cmd/cpe/generate.go +++ b/cmd/cpe/generate.go @@ -22,10 +22,10 @@ import ( ) const ( - httpClientTimeout = 2 * time.Minute + httpClientTimeout = 3 * time.Minute waitTimeBetweenRequests = 6 * time.Second - waitTimeForRetry = 30 * time.Second - maxRetryAttempts = 10 + waitTimeForRetry = 10 * time.Second + maxRetryAttempts = 20 apiKeyEnvVar = "NVD_API_KEY" //nolint:gosec ) diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 90dc3fffa969..75d6f3ba83bf 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -1182,7 +1182,7 @@ func appleMDMDEPSyncerJob( } } -func newMDMProfileManager( +func newAppleMDMProfileManagerSchedule( ctx context.Context, instanceID string, ds fleet.Datastore, @@ -1207,6 +1207,29 @@ func newMDMProfileManager( schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error { return service.ReconcileAppleDeclarations(ctx, ds, commander, logger) }), + ) + + return s, nil +} + +func newWindowsMDMProfileManagerSchedule( + ctx context.Context, + instanceID string, + ds fleet.Datastore, + logger kitlog.Logger, +) (*schedule.Schedule, error) { + const ( + name = string(fleet.CronMDMWindowsProfileManager) + // Note: per a request from #g-product we are running this cron + // every 30 seconds, we should re-evaluate how we handle the + // cron interval as we scale to more hosts. + defaultInterval = 30 * time.Second + ) + + logger = kitlog.With(logger, "cron", name) + s := schedule.New( + ctx, name, instanceID, defaultInterval, ds, ds, + schedule.WithLogger(logger), schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error { return service.ReconcileWindowsProfiles(ctx, ds, logger) }), diff --git a/cmd/fleet/cron_test.go b/cmd/fleet/cron_test.go index 789b38c40565..2f051d9f17c1 100644 --- a/cmd/fleet/cron_test.go +++ b/cmd/fleet/cron_test.go @@ -23,14 +23,24 @@ import ( kitlog "github.com/go-kit/log" ) -func TestNewMDMProfileManagerWithoutConfig(t *testing.T) { +func TestNewAppleMDMProfileManagerWithoutConfig(t *testing.T) { ctx := context.Background() mdmStorage := &mdmmock.MDMAppleStore{} ds := new(mock.Store) cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, nil) logger := kitlog.NewNopLogger() - sch, err := newMDMProfileManager(ctx, "foo", ds, cmdr, logger) + sch, err := newAppleMDMProfileManagerSchedule(ctx, "foo", ds, cmdr, logger) + require.NotNil(t, sch) + require.NoError(t, err) +} + +func TestNewWindowsMDMProfileManagerWithoutConfig(t *testing.T) { + ctx := context.Background() + ds := new(mock.Store) + logger := kitlog.NewNopLogger() + + sch, err := newWindowsMDMProfileManagerSchedule(ctx, "foo", ds, logger) require.NotNil(t, sch) require.NoError(t, err) } diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 4923cb95ad59..0b770ca43a12 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -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 { @@ -919,7 +924,7 @@ the way that the Fleet server works. } if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { - return newMDMProfileManager( + return newAppleMDMProfileManagerSchedule( ctx, instanceID, ds, @@ -930,6 +935,17 @@ the way that the Fleet server works. initFatal(err, "failed to register mdm_apple_profile_manager schedule") } + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newWindowsMDMProfileManagerSchedule( + ctx, + instanceID, + ds, + logger, + ) + }); err != nil { + initFatal(err, "failed to register mdm_windows_profile_manager schedule") + } + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { return newMDMAPNsPusher( ctx, @@ -1029,6 +1045,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) diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 24175ef1ba56..f18bfe391e90 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -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) { diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index ba5ba641f754..3c2bbee35640 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -3781,7 +3781,9 @@ spec: macos_settings: enable_disk_encryption: true `, - wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`, + + // Since Linux disk encryption does not use MDM, we allow enabling it even without MDM enabled and configured + wantOutput: `[+] applied fleet config`, }, { desc: "app config macos_settings.enable_disk_encryption false", diff --git a/cmd/fleetctl/query.go b/cmd/fleetctl/query.go index 3c64d2c19168..e54fbee2bc08 100644 --- a/cmd/fleetctl/query.go +++ b/cmd/fleetctl/query.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "regexp" "strings" "time" @@ -138,6 +139,12 @@ func queryCommand() *cli.Command { if strings.Contains(err.Error(), "no hosts targeted") { return errors.New(fleet.NoHostsTargetedErrMsg) } + if strings.Contains(err.Error(), fleet.InvalidLabelSpecifiedErrMsg) { + pattern := fmt.Sprintf("(%s.*)$", regexp.QuoteMeta(fleet.InvalidLabelSpecifiedErrMsg)) + regex := regexp.MustCompile(pattern) + match := regex.FindString(err.Error()) + return errors.New(match) + } return err } diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index 2ee7f8b7c182..8e5441b58ad5 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -219,8 +219,9 @@ func TestAdHocLiveQuery(t *testing.T) { return []uint{1234}, nil } ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) { - return nil, nil + return map[string]uint{"label1": uint(1)}, nil } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil } @@ -299,6 +300,14 @@ func TestAdHocLiveQuery(t *testing.T) { ) }() + // test label not found + _, err = runAppNoChecks([]string{"query", "--hosts", "1234", "--labels", "iamnotalabel", "--query", "select 42, * from time"}) + assert.ErrorContains(t, err, "Invalid label name(s): iamnotalabel.") + + // test if some labels were not found + _, err = runAppNoChecks([]string{"query", "--labels", "label1, mac, windows", "--hosts", "1234", "--query", "select 42, * from time"}) + assert.ErrorContains(t, err, "Invalid label name(s): mac, windows.") + expected := `{"host":"somehostname","rows":[{"bing":"fds","host_display_name":"somehostname","host_hostname":"somehostname"}]} ` assert.Equal(t, expected, runAppForTest(t, []string{"query", "--hosts", "1234", "--query", "select 42, * from time"})) diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index b8a5a82fb88f..18cfc569f63b 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -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 diff --git a/docs/Deploy/Reference-Architectures.md b/docs/Deploy/Reference-Architectures.md index eecc179c3650..54f4aac43de7 100644 --- a/docs/Deploy/Reference-Architectures.md +++ b/docs/Deploy/Reference-Architectures.md @@ -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. diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md index a113002f28de..97ed84bfabec 100644 --- a/docs/Deploy/deploy-fleet.md +++ b/docs/Deploy/deploy-fleet.md @@ -143,3 +143,4 @@ This workflow takes about 30 minutes to complete and supports between 10 and 350 + diff --git a/docs/Get started/FAQ.md b/docs/Get started/FAQ.md index 1dc2856dc4d2..d5da69c95996 100644 --- a/docs/Get started/FAQ.md +++ b/docs/Get started/FAQ.md @@ -4,7 +4,7 @@ Fleet offers managed cloud hosting for [Fleet Premium](https://fleetdm.com/pricing) customers with large deployments. -> While organizations of all kinds use Fleet, from Fortune 500 companies to school districts to hobbyists, we are only currently able to provide cost-effective hosting for deployments larger than 1000 hosts. (Instead, you can [buy a license](https://fleetdm.com/customers/register) and self-host Fleet Premium with support.) +> While organizations of all kinds use Fleet, from Fortune 500 companies to school districts to hobbyists, today we are only currently able to provide fully-managed hosting for deployments larger than 300 hosts. (Instead, you can [buy a license](https://fleetdm.com/customers/register) and self-host Fleet Premium with support.) Fleet is simple enough to [spin up for yourself](https://fleetdm.com/docs/deploy/introduction). Premium features are [available](https://fleetdm.com/pricing) either way. diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 579dd9772d11..0e25be55615d 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -500,6 +500,19 @@ for pagination. For a comprehensive list of activity types and detailed informat "status": "failed_install" } }, + { + "created_at": "2021-07-29T14:40:27Z", + "id": 21, + "actor_full_name": "name", + "actor_id": 1, + "actor_gravatar": "", + "actor_email": "name@example.com", + "type": "created_team", + "details": { + "team_id": 2, + "team_name": "Apples" + } + }, { "created_at": "2021-07-30T13:41:07Z", "id": 24, @@ -541,80 +554,6 @@ for pagination. For a comprehensive list of activity types and detailed informat "team_name": "Oranges" } }, - { - "created_at": "2021-07-29T14:40:27Z", - "id": 21, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "created_team", - "details": { - "team_id": 2, - "team_name": "Apples" - } - }, - { - "created_at": "2021-07-27T14:35:08Z", - "id": 20, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "created_pack", - "details": { - "pack_id": 2, - "pack_name": "New pack" - } - }, - { - "created_at": "2021-07-27T13:25:21Z", - "id": 19, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "live_query", - "details": { - "targets_count": 14 - } - }, - { - "created_at": "2021-07-27T13:25:14Z", - "id": 18, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "live_query", - "details": { - "targets_count": 14 - } - }, - { - "created_at": "2021-07-26T19:28:24Z", - "id": 17, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "live_query", - "details": { - "target_counts": 1 - } - }, - { - "created_at": "2021-07-26T17:27:37Z", - "id": 16, - "actor_full_name": "name", - "actor_id": 1, - "actor_gravatar": "", - "actor_email": "name@example.com", - "type": "live_query", - "details": { - "target_counts": 14 - } - }, { "created_at": "2021-07-26T17:27:08Z", "id": 15, @@ -2543,11 +2482,13 @@ the `software` table. | bootstrap_package | string | query | _Available in Fleet Premium_. Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. | | os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** | | os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** | -| populate_software | boolean | query | If `true`, the response will include a list of installed software for each host, including vulnerability data. (Note that software lists can be large, so this may cause significant CPU and RAM usage depending on page size and request concurrency.) | +| populate_software | string | query | If `false` (or omitted), omits installed software details for each host. If `"without_vulnerability_details"`, include a list of installed software for each host, including which CVEs apply to the installed software versions. `true` adds vulnerability description, CVSS score, and other details when using Fleet Premium. See notes below on performance. | | populate_policies | boolean | query | If `true`, the response will include policy data for each host. | > `software_id` is deprecated as of Fleet 4.42. It is maintained for backwards compatibility. Please use the `software_version_id` instead. +> `populate_software` returns a lot of data per host when set, and drastically more data when set to `true` on Fleet Premium. If you need vulnerability details for a large number of hosts, consider setting `populate_software` to `without_vulnerability_details` and pulling vulnerability details from the [Get vulnerability](#get-vulnerability) endpoint, as this returns details once per vulnerability rather than once per vulnerability per host. + If `software_title_id` is specified, an additional top-level key `"software_title"` is returned with the software title object corresponding to the `software_title_id`. See [List software](#list-software) response payload for details about this object. If `software_version_id` is specified, an additional top-level key `"software"` is returned with the software object corresponding to the `software_version_id`. See [List software versions](#list-software-versions) response payload for details about this object. @@ -5729,12 +5670,12 @@ Get aggregate disk encryption status counts of macOS and Windows hosts enrolled ```json { - "verified": {"macos": 123, "windows": 123}, - "verifying": {"macos": 123, "windows": 0}, - "action_required": {"macos": 123, "windows": 0}, - "enforcing": {"macos": 123, "windows": 123}, - "failed": {"macos": 123, "windows": 123}, - "removing_enforcement": {"macos": 123, "windows": 0}, + "verified": {"macos": 123, "windows": 123, "linux": 13}, + "verifying": {"macos": 123, "windows": 0, "linux": 0}, + "action_required": {"macos": 123, "windows": 0, "linux": 37}, + "enforcing": {"macos": 123, "windows": 123, "linux": 0}, + "failed": {"macos": 123, "windows": 123, "linux": 0}, + "removing_enforcement": {"macos": 123, "windows": 0, "linux": 0} } ``` @@ -5838,6 +5779,8 @@ Sets the custom MDM setup enrollment profile for a team or no team. } ``` +> NOTE: The `ConfigurationWebURL` and `URL` values in the custom MDM setup enrollment profile are automatically populated. Attempting to populate them with custom values may generate server response errors. + ### Get custom MDM setup enrollment profile _Available in Fleet Premium_ @@ -7656,6 +7599,9 @@ Returns a list of global queries or team queries. | team_id | integer | query | _Available in Fleet Premium_. The ID of the parent team for the queries to be listed. When omitted, returns global queries. | | query | string | query | Search query keywords. Searchable fields include `name`. | | merge_inherited | boolean | query | _Available in Fleet Premium_. If `true`, will include global queries in addition to team queries when filtering by `team_id`. (If no `team_id` is provided, this parameter is ignored.) | +| compatible_platform | string | query | Return queries that only reference tables compatible with this platform (not a strict compatibility check). One of: `"macos"`, `"windows"`, `"linux"`, `"chrome"` (case-insensitive). | +| page | integer | query | Page number of the results to fetch. | +| per_page | integer | query | Results per page. | #### Example @@ -7744,7 +7690,12 @@ Returns a list of global queries or team queries. "total_executions": null } } - ] + ], + "meta": { + "has_next_results": true, + "has_previous_results": false + }, + "count": 200 } ``` @@ -9367,6 +9318,7 @@ Returns information about the specified software. By default, `versions` are sor } }, "app_store_app": null, + "counts_updated_at": "2024-11-03T22:39:36Z", "source": "apps", "browser": "", "hosts_count": 48, diff --git a/ee/bulk-operations-dashboard/api/controllers/software/delete-software.js b/ee/bulk-operations-dashboard/api/controllers/software/delete-software.js index c60f71e7ec76..539d8c46761b 100644 --- a/ee/bulk-operations-dashboard/api/controllers/software/delete-software.js +++ b/ee/bulk-operations-dashboard/api/controllers/software/delete-software.js @@ -16,7 +16,10 @@ module.exports = { exits: { - + softwareDeletionFailed: { + description: 'The specified software could not be deleted from the Fleet instance.', + statusCode: 409, + } }, @@ -34,6 +37,11 @@ module.exports = { headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, } + }) + .intercept({raw:{statusCode: 409}}, (error)=>{ + // If the Fleet instance's returns a 409 response, then the software is configured to be installed as + // part of the macOS setup experience, and must be removed before it can be deleted via API requests. + return {softwareDeletionFailed: error}; }); } } diff --git a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js index abe14930ea66..d97b0366a83c 100644 --- a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js +++ b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js @@ -46,9 +46,23 @@ module.exports = { description: 'The provided replacement software\'s has the wrong extension.', statusCode: 400, }, + softwareUploadFailed: { description: 'The software upload failed' - } + }, + + softwareAlreadyExistsOnThisTeam: { + description: 'A software installer with this name already exists on the Fleet Instance', + }, + + couldNotReadVersion: { + description:'Fleet could not read version information from the provided software installer.' + }, + + softwareDeletionFailed: { + description: 'The specified software could not be deleted from the Fleet instance.', + statusCode: 409, + }, }, @@ -84,6 +98,9 @@ module.exports = { headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, } + }) + .intercept('non200Response', (error)=>{ + return new Error(`When attempting to transfer the installer for ${software.name} to a new team on the Fleet instance, the Fleet isntance returned a non-200 response when a request was sent to get a download stream of the installer on team_id ${teamIdToGetInstallerFrom}. Full Error: ${require('util').inspect(error, {depth: 1})}`); }); let tempUploadedSoftware = await sails.uploadOne(softwareStream, {bucket: sails.config.uploads.bucketWithPostfix}); softwareFd = tempUploadedSoftware.fd; @@ -164,7 +181,35 @@ module.exports = { } }; }, - }) + } + ) + .intercept({response: {status: 409}}, async (error)=>{// handles errors related to duplicate software items. + if(!software.id) {// If the software does not have an ID, it not stored in the app's database/s3 bucket, so we can safely delete the file in s3. + await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); + } + return {'softwareAlreadyExistsOnThisTeam': error}; + }) + .intercept({name: 'AxiosError', response: {status: 400}}, async (error)=>{// Handles errors related to malformed installer packages + if(!software.id) {// If the software does not have an ID, it not stored in the app's database/s3 bucket, so we can safely delete the file in s3. + await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); + } + let axiosError = error; + if(axiosError.response.data) { + if(axiosError.response.data.errors && _.isArray(axiosError.response.data.errors)){ + if(axiosError.response.data.errors[0] && axiosError.response.data.errors[0].reason) { + let errorMessageFromFleetInstance = axiosError.response.data.errors[0].reason; + if(_.startsWith(errorMessageFromFleetInstance, `Couldn't add. Fleet couldn't read the version`)){ + return 'couldNotReadVersion'; + } else { + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API. Error returned from Fleet API: ${errorMessageFromFleetInstance}`); + return {'softwareUploadFailed': error}; + } + } + } + } + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 3})}`); + return {'softwareUploadFailed': error}; + }) .intercept(async (error)=>{ // Note: with this current behavior, all errors from this upload are currently swallowed and a softwareUploadFailed response is returned. // FUTURE: Test to make sure that uploading duplicate software to a team results in a 409 response. @@ -173,7 +218,7 @@ module.exports = { await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); } // Log a warning containing an error - sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 0})}`); + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, Full error: ${require('util').inspect(error, {depth: 2})}`); return {'softwareUploadFailed': error}; }); // console.timeEnd(`transfering ${software.name} to fleet instance for team id ${team}`); @@ -183,15 +228,6 @@ module.exports = { // If a new installer package was provided, send patch requests to update the installer package on teams that it is already deployed to. await sails.helpers.flow.simultaneouslyForEach(unchangedTeamIds, async (teamApid)=>{ // console.log(`Adding new version of ${softwareName} to teamId ${teamApid}`); - await sails.helpers.http.sendHttpRequest.with({ - method: 'DELETE', - baseUrl: sails.config.custom.fleetBaseUrl, - url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${teamApid}`, - headers: { - Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, - } - }); - // console.log(`transfering the changed installer ${software.name} to fleet instance for team id ${teamApid}`); // console.time(`transfering ${software.name} to fleet instance for team id ${teamApid}`); await sails.cp(softwareFd, {bucket: sails.config.uploads.bucketWithPostfix}, { @@ -220,7 +256,7 @@ module.exports = { contentType: 'application/octet-stream' }); (async ()=>{ - await axios.post(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/package`, form, { + await axios.patch(`${sails.config.custom.fleetBaseUrl}/api/v1/fleet/software/titles/${software.fleetApid}/package`, form, { headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, ...form.getHeaders() @@ -240,6 +276,33 @@ module.exports = { }; }, }) + .intercept({response: {status: 409}}, async (error)=>{// handles errors related to duplicate software items. + if(!software.id) {// If the software does not have an ID, it not stored in the app's database/s3 bucket, so we can safely delete the file in s3. + await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); + } + return {'softwareAlreadyExistsOnThisTeam': error}; + }) + .intercept({name: 'AxiosError', response: {status: 400}}, async (error)=>{// Handles errors related to malformed installer packages + if(!software.id) {// If the software does not have an ID, it not stored in the app's database/s3 bucket, so we can safely delete the file in s3. + await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); + } + let axiosError = error; + if(axiosError.response.data) { + if(axiosError.response.data.errors && _.isArray(axiosError.response.data.errors)){ + if(axiosError.response.data.errors[0] && axiosError.response.data.errors[0].reason) { + let errorMessageFromFleetInstance = axiosError.response.data.errors[0].reason; + if(_.startsWith(errorMessageFromFleetInstance, `Couldn't add. Fleet couldn't read the version`)){ + return 'couldNotReadVersion'; + } else { + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API. Error returned from Fleet API: ${errorMessageFromFleetInstance}`); + return {'softwareUploadFailed': error}; + } + } + } + } + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 3})}`); + return {'softwareUploadFailed': error}; + }) .intercept(async (error)=>{ // Note: with this current behavior, all errors from this upload are currently swallowed and a softwareUploadFailed response is returned. // FUTURE: Test to make sure that uploading duplicate software to a team results in a 409 response. @@ -248,7 +311,7 @@ module.exports = { await sails.rm(sails.config.uploads.prefixForFileDeletion+softwareFd); } // Log a warning containing an error - sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 0})}`); + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 2})}`); return {'softwareUploadFailed': error}; }); // console.timeEnd(`transfering ${software.name} to fleet instance for team id ${teamApid}`); @@ -285,6 +348,11 @@ module.exports = { headers: { Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, } + }) + .intercept({raw:{statusCode: 409}}, (error)=>{ + // If the Fleet instance's returns a 409 response, then the software is configured to be installed as + // part of the macOS setup experience, and must be removed before it can be deleted via API requests. + return {softwareDeletionFailed: error}; }); } // If the software had been previously undeployed, delete the installer in s3 and the db record. @@ -295,9 +363,23 @@ module.exports = { } else if(software.teams && newTeamIds.length === 0) { // If this is a deployed software that is being unassigned, save information about the uploaded file in our s3 bucket. + for(let team of software.teams) { + // Now delete the software on the Fleet instance. + await sails.helpers.http.sendHttpRequest.with({ + method: 'DELETE', + baseUrl: sails.config.custom.fleetBaseUrl, + url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`, + headers: { + Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, + } + }) + .intercept({raw:{statusCode: 409}}, (error)=>{ + // If the Fleet instance's returns a 409 response, then the software is configured to be installed as + // part of the macOS setup experience, and must be removed before it can be deleted via API requests. + return {softwareDeletionFailed: error}; + }); + } if(newSoftware) { - // remove the old copy. - // console.log('Removing old package for ',softwareName); await UndeployedSoftware.create({ uploadFd: softwareFd, uploadMime: softwareMime, @@ -321,17 +403,6 @@ module.exports = { uninstallScript, }); } - // Now delete the software on the Fleet instance. - for(let team of software.teams) { - await sails.helpers.http.sendHttpRequest.with({ - method: 'DELETE', - baseUrl: sails.config.custom.fleetBaseUrl, - url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`, - headers: { - Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, - } - }); - } } else { // console.log('updating existing db record!'); diff --git a/ee/bulk-operations-dashboard/api/controllers/software/upload-software.js b/ee/bulk-operations-dashboard/api/controllers/software/upload-software.js index 29b4817b3bd9..987bfe8792ed 100644 --- a/ee/bulk-operations-dashboard/api/controllers/software/upload-software.js +++ b/ee/bulk-operations-dashboard/api/controllers/software/upload-software.js @@ -35,6 +35,10 @@ module.exports = { softwareUploadFailed: { description:'An unexpected error occurred communicating with the Fleet API' + }, + + couldNotReadVersion: { + description:'Fleet could not read version information from the provided software installer.' } }, @@ -100,13 +104,32 @@ module.exports = { }; } }) - .intercept({response: {status: 409}}, async (error)=>{ + .intercept({response: {status: 409}}, async (error)=>{// handles errors related to duplicate software items. await sails.rm(sails.config.uploads.prefixForFileDeletion+uploadedSoftware.fd); return {'softwareAlreadyExistsOnThisTeam': error}; }) - .intercept({name: 'AxiosError'}, async (error)=>{ + .intercept({name: 'AxiosError', response: {status: 400}}, async (error)=>{// Handles errors related to malformed installer packages + await sails.rm(sails.config.uploads.prefixForFileDeletion+uploadedSoftware.fd); + let axiosError = error; + if(axiosError.response.data) { + if(axiosError.response.data.errors && _.isArray(axiosError.response.data.errors)){ + if(axiosError.response.data.errors[0] && axiosError.response.data.errors[0].reason) { + let errorMessageFromFleetInstance = axiosError.response.data.errors[0].reason; + if(_.startsWith(errorMessageFromFleetInstance, `Couldn't add. Fleet couldn't read the version`)){ + return 'couldNotReadVersion'; + } else { + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API. Error returned from Fleet API: ${errorMessageFromFleetInstance}`); + return {'softwareUploadFailed': error}; + } + } + } + } + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 3})}`); + return {'softwareUploadFailed': error}; + }) + .intercept({name: 'AxiosError'}, async (error)=>{// Handles any other error. await sails.rm(sails.config.uploads.prefixForFileDeletion+uploadedSoftware.fd); - sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 0})}`); + sails.log.warn(`When attempting to upload a software installer, an unexpected error occurred communicating with the Fleet API, ${require('util').inspect(error, {depth: 3})}`); return {'softwareUploadFailed': error}; }); } diff --git a/ee/bulk-operations-dashboard/api/controllers/software/view-software.js b/ee/bulk-operations-dashboard/api/controllers/software/view-software.js index 8a8a8ccb5e20..6b06dabeb200 100644 --- a/ee/bulk-operations-dashboard/api/controllers/software/view-software.js +++ b/ee/bulk-operations-dashboard/api/controllers/software/view-software.js @@ -104,7 +104,7 @@ module.exports = { let undeployedSoftware = await UndeployedSoftware.find(); allSoftware = allSoftware.concat(undeployedSoftware); - return {software: allSoftware, teams}; + return {software: allSoftware, teams, fleetBaseUrl: sails.config.custom.fleetBaseUrl}; } diff --git a/ee/bulk-operations-dashboard/views/pages/software/software.ejs b/ee/bulk-operations-dashboard/views/pages/software/software.ejs index 0467e7f6062b..dcc8b823aaf1 100644 --- a/ee/bulk-operations-dashboard/views/pages/software/software.ejs +++ b/ee/bulk-operations-dashboard/views/pages/software/software.ejs @@ -145,8 +145,11 @@

Teams

- {{cloudError.responseInfo.body}} - + {{cloudError.responseInfo.body}} + The Fleet instance could not read version information from the provided software installer. + This software has been configured to be installed as part of the macOS setup experience and cannot be removed from a team. Please remove this software from any teams you want to remove this from in the "Setup experience" tab of the Controls page on your Fleet instance and try again + An error occured when transfering this software to a new team. A software installer with the same name as this software already exists on one or more of the selected teams. +
Save
@@ -163,7 +166,8 @@

{{formData.software.name}} will be removed from your library.

- + This software has been configured to be installed as part of the macOS setup experience and cannot be deleted. Please remove this software from all teams the "Setup experience" tab of the Controls page on your Fleet instance and try again +
Cancel Delete @@ -188,6 +192,7 @@
Please select the teams you want to deploy this software to.
A software with the same name as the uploaded software already exists on one or more of the selected teams. + The Fleet instance could not read version information from the provided software installer.
Cancel diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 9fa82d0e556c..77a6c7ce46fe 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -182,7 +182,7 @@ func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host * func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet.Host) error { if !host.IsLUKSSupported() { - return &fleet.BadRequestError{Message: "Host platform does not support key escrow"} + return &fleet.BadRequestError{Message: "Fleet does not yet support creating LUKS disk encryption keys on this platform."} } ac, err := svc.ds.AppConfig(ctx) @@ -192,7 +192,7 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet if host.TeamID == nil { if !ac.MDM.EnableDiskEncryption.Value { - return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team"} + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team."} } } else { tc, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID) @@ -200,16 +200,22 @@ func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet return err } if !tc.EnableDiskEncryption { - return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team"} + return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team."} } } if host.DiskEncryptionEnabled == nil || !*host.DiskEncryptionEnabled { - return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please enable disk encryption for this host."} + return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please encrypt your disk first."} } - if host.OrbitVersion == nil || !fleet.IsAtLeastVersion(*host.OrbitVersion, fleet.MinOrbitLUKSVersion) { - return &fleet.BadRequestError{Message: "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version."} + // We have to pull Orbit info because the auth context doesn't fill in host.OrbitVersion + orbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) + if err != nil { + return err + } + + if orbitInfo == nil || !fleet.IsAtLeastVersion(orbitInfo.Version, fleet.MinOrbitLUKSVersion) { + return &fleet.BadRequestError{Message: "Your version of fleetd does not support creating disk encryption keys on Linux. Please upgrade fleetd, then click Refetch, then try again."} } return svc.ds.AssertHasNoEncryptionKeyStored(ctx, host.ID) diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 9cb9aa46f0cf..71272b08af65 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -45,6 +45,9 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser AppleSCEPCertBytes: eeservice.TestCert, AppleSCEPKeyBytes: eeservice.TestKey, }, + Server: config.ServerConfig{ + PrivateKey: "foo", + }, } depStorage := &nanodep_mock.Storage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/ee/server/service/setup_experience.go b/ee/server/service/setup_experience.go index 077630d3c46a..7bece3103e03 100644 --- a/ee/server/service/setup_experience.go +++ b/ee/server/service/setup_experience.go @@ -220,9 +220,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string return false, ctxerr.Errorf(ctx, "setup experience script missing content id: %d", *script.SetupExperienceScriptID) } req := &fleet.HostScriptRequestPayload{ - HostID: host.ID, - ScriptName: script.Name, - ScriptContentID: *script.ScriptContentID, + HostID: host.ID, + ScriptName: script.Name, + ScriptContentID: *script.ScriptContentID, + SetupExperienceScriptID: script.SetupExperienceScriptID, } res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req) if err != nil { diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 34d3b1f76c2c..eded9b4788ac 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1046,13 +1046,9 @@ func (svc *Service) createTeamFromSpec( } invalid := &fleet.InvalidArgumentError{} - if enableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() { - invalid.Append( - "mdm", - `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`, - ) + if enableDiskEncryption && 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") } - validateTeamCustomSettings(invalid, "macos", macOSSettings.CustomSettings) validateTeamCustomSettings(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value) @@ -1210,11 +1206,10 @@ func (svc *Service) editTeamFromSpec( team.Config.MDM.EnableDiskEncryption = *de } didUpdateDiskEncryption := team.Config.MDM.EnableDiskEncryption != oldEnableDiskEncryption - if !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() && didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", - `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) - } + if didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption && 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 !team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid { team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false) } @@ -1521,12 +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 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 } } @@ -1539,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} diff --git a/frontend/components/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx index 1a78e0a486d3..bd49e3d20645 100644 --- a/frontend/components/InfoBanner/InfoBanner.tsx +++ b/frontend/components/InfoBanner/InfoBanner.tsx @@ -15,10 +15,11 @@ export interface IInfoBannerProps { /** default 4px */ borderRadius?: "large" | "xlarge"; pageLevel?: boolean; - /** cta and link are mutually exclusive */ + /** Add this element to the end of the banner message. Mutually exclusive with `link`. */ cta?: JSX.Element; /** closable and link are mutually exclusive */ closable?: boolean; + /** Makes the entire banner clickable */ link?: string; icon?: IconNames; } diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index d3947370a243..8049b474419b 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -37,7 +37,6 @@ export interface IModalProps { * */ disableClosingModal?: boolean; className?: string; - actionsFooter?: JSX.Element; } const Modal = ({ @@ -51,7 +50,6 @@ const Modal = ({ isContentDisabled = false, disableClosingModal = false, className, - actionsFooter, }: IModalProps): JSX.Element => { useEffect(() => { const closeWithEscapeKey = (e: KeyboardEvent) => { @@ -127,9 +125,6 @@ const Modal = ({ )}
{children}
- {actionsFooter && ( -
{actionsFooter}
- )} ); diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index e12ba664092f..747f6e58f2f2 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -25,17 +25,9 @@ &__content-wrapper { margin-top: $pad-large; font-size: $x-small; - max-height: 800px; - overflow-y: auto; .input-field { width: 100%; - - &::placeholder { - font-size: $x-small; - font-style: italic; - line-height: 24px; - } } form .modal-cta-wrap, @@ -142,6 +134,11 @@ } } +.modal-scrollable-content { + overflow-y: auto; + max-height: 705px; +} + .modal-cta-wrap { align-self: flex-end; display: flex; diff --git a/frontend/components/ModalFooter/ModalFooter.stories.tsx b/frontend/components/ModalFooter/ModalFooter.stories.tsx new file mode 100644 index 000000000000..802f76f9f0d0 --- /dev/null +++ b/frontend/components/ModalFooter/ModalFooter.stories.tsx @@ -0,0 +1,63 @@ +/* eslint-disable no-alert */ +import React from "react"; +import { Meta, Story } from "@storybook/react"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; +import ActionsDropdown from "components/ActionsDropdown"; +import ModalFooter from "./ModalFooter"; + +export default { + title: "Components/ModalFooter", + component: ModalFooter, +} as Meta; + +const Template: Story = (args) => ( + } {...args} /> +); + +export const Default = Template.bind({}); +Default.args = { + primaryButtons: ( + <> + alert(`Selected action: ${value}`)} + placeholder="More actions" + isSearchable={false} + options={[ + { value: "action1", label: "Action 1" }, + { value: "action2", label: "Action 2" }, + ]} + menuPlacement="top" + /> + + + ), + secondaryButtons: ( + <> + + + + ), + isTopScrolling: false, +}; + +export const WithTopScrolling = Template.bind({}); +WithTopScrolling.args = { + ...Default.args, + isTopScrolling: true, +}; + +export const WithoutSecondaryButtons = Template.bind({}); +WithoutSecondaryButtons.args = { + primaryButtons: Default.args.primaryButtons, + secondaryButtons: undefined, + isTopScrolling: false, +}; diff --git a/frontend/components/ModalFooter/ModalFooter.tsx b/frontend/components/ModalFooter/ModalFooter.tsx new file mode 100644 index 000000000000..4b24cc6db0ad --- /dev/null +++ b/frontend/components/ModalFooter/ModalFooter.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import classnames from "classnames"; +import { COLORS } from "styles/var/colors"; + +const baseClass = "modal-footer"; + +interface IModalFooterProps { + primaryButtons: JSX.Element; + secondaryButtons?: JSX.Element; + className?: string; + /** Renders a line above action buttons to indicate scrollability */ + isTopScrolling?: boolean; +} + +const ModalFooter = ({ + primaryButtons, + secondaryButtons, + className, + isTopScrolling = false, +}: IModalFooterProps): JSX.Element => { + const classes = classnames(className, `${baseClass}__content-wrapper`); + + return ( +
+
+ {primaryButtons} +
+ {secondaryButtons && ( +
+ {secondaryButtons} +
+ )} +
+ ); +}; + +export default ModalFooter; diff --git a/frontend/components/ModalFooter/_styles.scss b/frontend/components/ModalFooter/_styles.scss new file mode 100644 index 000000000000..679551e1d8ac --- /dev/null +++ b/frontend/components/ModalFooter/_styles.scss @@ -0,0 +1,27 @@ +.modal-footer { + &__content-wrapper { + align-self: flex-end; + display: flex; + flex-direction: row-reverse; + padding-top: $pad-medium; + justify-content: space-between; + } + + // Styles both primary-actions and secondary-actions + &__primary-buttons-wrapper, + &__secondary-buttons_wrapper { + display: flex; + justify-content: space-between; + gap: $pad-medium; + align-items: center; + } + + // Align primary actions right if no secondary actions + > :last-child { + margin-left: auto; + } + + .button__text-icon { + padding: 11px; + } +} diff --git a/frontend/components/ModalFooter/index.ts b/frontend/components/ModalFooter/index.ts new file mode 100644 index 000000000000..6a36137070f1 --- /dev/null +++ b/frontend/components/ModalFooter/index.ts @@ -0,0 +1 @@ +export { default } from "./ModalFooter"; diff --git a/frontend/components/SectionHeader/SectionHeader.tsx b/frontend/components/SectionHeader/SectionHeader.tsx index f540ae821cce..c3ebe5e76888 100644 --- a/frontend/components/SectionHeader/SectionHeader.tsx +++ b/frontend/components/SectionHeader/SectionHeader.tsx @@ -7,24 +7,32 @@ interface ISectionHeaderProps { title: string; subTitle?: React.ReactNode; details?: JSX.Element; - className?: string; + wrapperCustomClass?: string; + alignLeftHeaderVertically?: boolean; + greySubtitle?: boolean; } const SectionHeader = ({ title, subTitle, details, - className, + wrapperCustomClass, + alignLeftHeaderVertically, + greySubtitle, }: ISectionHeaderProps) => { - const classNames = classnames(baseClass, className); + const wrapperClassnames = classnames(baseClass, wrapperCustomClass); + const leftHeaderClassnames = classnames(`${baseClass}__left-header`, { + [`${baseClass}__left-header--vertical`]: alignLeftHeaderVertically, + }); + const subTitleClassnames = classnames(`${baseClass}__sub-title`, { + [`${baseClass}__sub-title--grey`]: greySubtitle, + }); return ( -
-
+
+

{title}

- {subTitle && ( -
{subTitle}
- )} + {subTitle &&
{subTitle}
}
{details &&
{details}
}
diff --git a/frontend/components/SectionHeader/_styles.scss b/frontend/components/SectionHeader/_styles.scss index c9f1b6412ecb..943ea5c9e163 100644 --- a/frontend/components/SectionHeader/_styles.scss +++ b/frontend/components/SectionHeader/_styles.scss @@ -7,6 +7,15 @@ display: flex; align-items: center; gap: $pad-small; + &--vertical { + flex-direction: column; + } + } + + &__sub-title { + &--grey { + @include grey-text; + } } h2 { diff --git a/frontend/components/StatusIndicator/StatusIndicator.tests.tsx b/frontend/components/StatusIndicator/StatusIndicator.tests.tsx index 73e4b665e404..418b6ce96dc8 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.tests.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.tests.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { render, screen, fireEvent, act } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; import StatusIndicator from "./StatusIndicator"; @@ -16,9 +16,7 @@ describe("Status indicator", () => { ); - await act(async () => { - fireEvent.mouseEnter(screen.getByText("Online")); - }); + await fireEvent.mouseEnter(screen.getByText("Online")); expect(screen.getByText(TOOLTIP_TEXT)).toBeInTheDocument(); }); diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index e38b1c1b13f3..bc59c6fb0c67 100644 --- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx +++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx @@ -18,7 +18,7 @@ import { import Icon from "components/Icon"; -interface INumberDropdownOption extends Omit { +export interface INumberDropdownOption extends Omit { value: number; // Redefine the value property to be just number } diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss index 7fcd24ad33ed..23bf82f07066 100644 --- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss @@ -1,7 +1,8 @@ // Used with old react-select dropdown and -// New react-select-5 ActionsDropdown.tsx +// New react-select-5: ActionsDropdown.tsx, DropdownWrapper.tsx .Select > .Select-menu-outer, -.actions-dropdown { +.actions-dropdown, +.react-select__option { .is-disabled * { color: $ui-fleet-black-50; } diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index c86dc61bf62c..dc7a763f7c3e 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -225,6 +225,10 @@ animation: fade-in 150ms ease-out; } + .Select-menu { + max-height: 190px; + } + .Select-noresults { font-size: $x-small; } diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx new file mode 100644 index 000000000000..ee0d223c58ca --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx @@ -0,0 +1,62 @@ +// stories/DropdownWrapper.stories.tsx + +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import DropdownWrapper, { + IDropdownWrapper, + CustomOptionType, +} from "./DropdownWrapper"; + +// Define metadata for the story +export default { + title: "Components/DropdownWrapper", + component: DropdownWrapper, + argTypes: { + onChange: { action: "changed" }, + }, +} as Meta; + +// Define a template for the stories +const Template: Story = (args) => ( + +); + +// Sample options to be used in the dropdown +const sampleOptions: CustomOptionType[] = [ + { label: "Option 1", value: "option1", helpText: "Help text for option 1" }, + { + label: "Option 2", + value: "option2", + tooltipContent: "Tooltip for option 2", + }, + { label: "Option 3", value: "option3", isDisabled: true }, +]; + +// Default story +export const Default = Template.bind({}); +Default.args = { + options: sampleOptions, + name: "dropdown-example", + label: "Select an option", +}; + +// Disabled story +export const Disabled = Template.bind({}); +Disabled.args = { + ...Default.args, + isDisabled: true, +}; + +// With Help Text story +export const WithHelpText = Template.bind({}); +WithHelpText.args = { + ...Default.args, + helpText: "This is some help text for the dropdown", +}; + +// With Error story +export const WithError = Template.bind({}); +WithError.args = { + ...Default.args, + error: "This is an error message", +}; diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx new file mode 100644 index 000000000000..2715876f8ba4 --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import DropdownWrapper, { CustomOptionType } from "./DropdownWrapper"; + +const sampleOptions: CustomOptionType[] = [ + { + label: "Option 1", + value: "option1", + tooltipContent: "Tooltip 1", + helpText: "Help text 1", + }, + { + label: "Option 2", + value: "option2", + tooltipContent: "Tooltip 2", + helpText: "Help text 2", + }, +]; + +describe("DropdownWrapper Component", () => { + const mockOnChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders with help text", () => { + render( + + ); + + expect(screen.getByText(/test dropdown/i)).toBeInTheDocument(); + expect(screen.getByText(/this is a help text/i)).toBeInTheDocument(); + }); + + test("calls onChange when an option is selected", async () => { + render( + + ); + + // Open the dropdown + await userEvent.click(screen.getByText(/option 1/i)); + + // Select Option 2 + await userEvent.click(screen.getByText(/option 2/i)); + + expect(mockOnChange).toHaveBeenCalledWith({ + helpText: "Help text 2", + label: "Option 2", + tooltipContent: "Tooltip 2", + value: "option2", + }); + }); + + test("renders error message when provided", () => { + render( + + ); + + expect(screen.getByText(/this is an error message/i)).toBeInTheDocument(); + }); + + test("displays no options message when no options are available", async () => { + render( + + ); + + // Open dropdown + await userEvent.click(screen.getByText(/choose option/i)); + + expect(screen.getByText(/no results found/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx new file mode 100644 index 000000000000..51c5c3cd695f --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx @@ -0,0 +1,340 @@ +/** + * This is a new component built off react-select 5.4 + * meant to replace Dropdown.jsx built off react-select 1.3 + * + * See storybook component for current functionality + * + * Prototyped on UserForm.tsx but added and tested the following: + * Options: text, disabled, option helptext, option tooltip + * Other: label text, dropdown help text, dropdown error + */ + +import classnames from "classnames"; +import React from "react"; +import Select, { + StylesConfig, + DropdownIndicatorProps, + OptionProps, + components, + PropsValue, + SingleValue, +} from "react-select-5"; + +import { COLORS } from "styles/var/colors"; +import { PADDING } from "styles/var/padding"; + +import FormField from "components/forms/FormField"; +import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper"; +import Icon from "components/Icon"; + +const getOptionBackgroundColor = (state: any) => { + return state.isSelected || state.isFocused + ? COLORS["ui-vibrant-blue-10"] + : "transparent"; +}; + +export interface CustomOptionType { + label: string; + value: string; + tooltipContent?: string; + helpText?: string; + isDisabled?: boolean; +} + +export interface IDropdownWrapper { + options: CustomOptionType[]; + value?: PropsValue | string; + onChange: (newValue: SingleValue) => void; + name: string; + className?: string; + labelClassname?: string; + error?: string; + label?: JSX.Element | string; + helpText?: JSX.Element | string; + isSearchable?: boolean; + isDisabled?: boolean; + placeholder?: string; + menuPortalTarget?: HTMLElement | null; +} + +const baseClass = "dropdown-wrapper"; + +const DropdownWrapper = ({ + options, + value, + onChange, + name, + className, + labelClassname, + error, + label, + helpText, + isSearchable, + isDisabled = false, + placeholder, + menuPortalTarget, +}: IDropdownWrapper) => { + const wrapperClassNames = classnames(baseClass, className); + + const handleChange = (newValue: SingleValue) => { + onChange(newValue); + }; + + // Ability to handle value of type string or CustomOptionType + const getCurrentValue = () => { + if (typeof value === "string") { + return options.find((option) => option.value === value) || null; + } + return value; + }; + + interface CustomOptionProps + extends Omit, "data"> { + data: CustomOptionType; + } + + const CustomOption = (props: CustomOptionProps) => { + const { data, ...rest } = props; + + const optionContent = ( +
+ {data.label} + {data.helpText && ( + {data.helpText} + )} +
+ ); + + return ( + + {data.tooltipContent ? ( + + {optionContent} + + ) : ( + optionContent + )} + + ); + }; + + const CustomDropdownIndicator = ( + props: DropdownIndicatorProps + ) => { + const { isFocused, selectProps } = props; + const color = + isFocused || selectProps.menuIsOpen + ? "core-fleet-blue" + : "core-fleet-black"; + + return ( + + + + ); + }; + + const customStyles: StylesConfig = { + container: (provided) => ({ + ...provided, + width: "100%", + height: "40px", + }), + control: (provided, state) => ({ + ...provided, + display: "flex", + flexDirection: "row", + width: "100%", + backgroundColor: COLORS["ui-off-white"], + paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px) + paddingRight: "8px", + cursor: "pointer", + boxShadow: "none", + borderRadius: "4px", + borderColor: state.isFocused + ? COLORS["core-fleet-blue"] + : COLORS["ui-fleet-black-10"], + "&:hover": { + boxShadow: "none", + borderColor: COLORS["core-fleet-blue"], + ".dropdown-wrapper__single-value": { + color: COLORS["core-vibrant-blue-over"], + }, + ".dropdown-wrapper__indicator path": { + stroke: COLORS["core-vibrant-blue-over"], + }, + }, + // When tabbing + // Relies on --is-focused for styling as &:focus-visible cannot be applied + "&.dropdown-wrapper__control--is-focused": { + ".dropdown-wrapper__single-value": { + color: COLORS["core-vibrant-blue-over"], + }, + ".dropdown-wrapper__indicator path": { + stroke: COLORS["core-vibrant-blue-over"], + }, + }, + ...(state.isDisabled && { + ".dropdown-wrapper__single-value": { + color: COLORS["ui-fleet-black-50"], + }, + ".dropdown-wrapper__indicator path": { + stroke: COLORS["ui-fleet-black-50"], + }, + }), + "&:active": { + ".dropdown-wrapper__single-value": { + color: COLORS["core-vibrant-blue-down"], + }, + ".dropdown-wrapper__indicator path": { + stroke: COLORS["core-vibrant-blue-down"], + }, + }, + ...(state.menuIsOpen && { + ".dropdown-wrapper__indicator svg": { + transform: "rotate(180deg)", + transition: "transform 0.25s ease", + }, + }), + }), + singleValue: (provided) => ({ + ...provided, + fontSize: "16px", + margin: 0, + padding: 0, + }), + dropdownIndicator: (provided) => ({ + ...provided, + display: "flex", + padding: "2px", + svg: { + transition: "transform 0.25s ease", + }, + }), + menu: (provided) => ({ + ...provided, + boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)", + borderRadius: "4px", + zIndex: 6, + overflow: "hidden", + border: 0, + marginTop: 0, + maxHeight: "none", + position: "absolute", + left: "0", + animation: "fade-in 150ms ease-out", + }), + menuList: (provided) => ({ + ...provided, + padding: PADDING["pad-small"], + }), + valueContainer: (provided) => ({ + ...provided, + padding: 0, + }), + option: (provided, state) => ({ + ...provided, + padding: "10px 8px", + fontSize: "14px", + backgroundColor: getOptionBackgroundColor(state), + color: COLORS["core-fleet-black"], + "&:hover": { + backgroundColor: state.isDisabled + ? "transparent" + : COLORS["ui-vibrant-blue-10"], + }, + "&:active": { + backgroundColor: state.isDisabled + ? "transparent" + : COLORS["ui-vibrant-blue-10"], + }, + ...(state.isDisabled && { + color: COLORS["ui-fleet-black-50"], + fontStyle: "italic", + cursor: "not-allowed", + pointerEvents: "none", + }), + // Styles for custom option + ".dropdown-wrapper__option": { + display: "flex", + flexDirection: "column", + gap: "8px", + width: "100%", + }, + ".dropdown-wrapper__help-text": { + fontSize: "12px", + whiteSpace: "normal", + color: COLORS["ui-fleet-black-50"], + fontStyle: "italic", + }, + }), + menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections + noOptionsMessage: (provided) => ({ + ...provided, + textAlign: "left", + fontSize: "14px", + padding: "10px 8px", + }), + }; + + const renderLabel = () => { + const labelWrapperClasses = classnames( + `${baseClass}__label`, + labelClassname, + { + [`${baseClass}__label--error`]: !!error, + [`${baseClass}__label--disabled`]: isDisabled, + } + ); + + if (!label) { + return ""; + } + + return ( + + ); + }; + + return ( + + + classNamePrefix="react-select" + isSearchable={isSearchable} + styles={customStyles} + options={options} + components={{ + Option: CustomOption, + DropdownIndicator: CustomDropdownIndicator, + IndicatorSeparator: () => null, + }} + value={getCurrentValue()} + onChange={handleChange} + isDisabled={isDisabled} + menuPortalTarget={ + menuPortalTarget === undefined ? document.body : menuPortalTarget + } + noOptionsMessage={() => "No results found"} + tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility + placeholder={placeholder} + /> + + ); +}; + +export default DropdownWrapper; diff --git a/frontend/components/forms/fields/DropdownWrapper/_styles.scss b/frontend/components/forms/fields/DropdownWrapper/_styles.scss new file mode 100644 index 000000000000..2a92f5ced65a --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/_styles.scss @@ -0,0 +1,14 @@ +// react-select's