diff --git a/.custom-gcl.yml b/.custom-gcl.yml index 3b85dff0eb4..d6676782c2c 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -1,7 +1,7 @@ # This configures how golangci-lint builds a custom build, wich is necessary to use nilaway as a plugin per https://github.com/uber-go/nilaway?tab=readme-ov-file#golangci-lint--v1570 # This has to be >= v1.57.0 for module plugin system support. -version: v2.7.1 +version: v2.11.3 plugins: - module: "go.uber.org/nilaway" import: "go.uber.org/nilaway/cmd/gclplugin" diff --git a/.github/workflows/build-kencove.yml b/.github/workflows/build-kencove.yml new file mode 100644 index 00000000000..52ecac13106 --- /dev/null +++ b/.github/workflows/build-kencove.yml @@ -0,0 +1,120 @@ +name: Build & Push Kencove Fleet Image + +on: + push: + branches: [kencove] + tags: + - 'fleet-v*' + paths: + - 'cmd/**' + - 'ee/**' + - 'server/**' + - 'frontend/**' + - 'orbit/**' + - 'pkg/**' + - 'go.mod' + - 'go.sum' + - 'package.json' + - 'yarn.lock' + - 'webpack.config.js' + - 'Dockerfile' + - '.github/workflows/build-kencove.yml' + pull_request: + branches: [kencove] + paths: + - 'cmd/**' + - 'ee/**' + - 'server/**' + - 'frontend/**' + - 'orbit/**' + - 'pkg/**' + - 'go.mod' + - 'go.sum' + - 'package.json' + - 'yarn.lock' + - 'webpack.config.js' + - 'Dockerfile' + - '.github/workflows/build-kencove.yml' + # Tag pushes ignore paths filter per GitHub docs, so fleet-v* tags always build. + # For branch and PR pushes, only source code changes trigger a rebuild. + workflow_dispatch: + +env: + REGION: us-central1 + PROJECT_ID: kencove-prod + REPOSITORY: kencove-docker-repo + IMAGE_NAME: fleet + +jobs: + build-and-push: + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Derive Fleet version and image tag + id: version + run: | + if [[ "$GITHUB_REF" == refs/tags/fleet-v* ]]; then + BASE_TAG="${GITHUB_REF#refs/tags/}" + else + BASE_TAG=$(git describe --tags --match 'fleet-v*' --abbrev=0 2>/dev/null || echo "fleet-vdev") + fi + FLEET_VERSION="${BASE_TAG#fleet-}" + echo "fleet_version=${FLEET_VERSION}" >> "$GITHUB_OUTPUT" + + # PR builds get a pr-N tag; branch/tag builds get the release tag + latest + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + IMAGE_TAG="${FLEET_VERSION}-kencove-pr${{ github.event.number }}" + echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo "extra_tags=" >> "$GITHUB_OUTPUT" + else + IMAGE_TAG="${FLEET_VERSION}-kencove" + echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + EXTRA="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:latest" + echo "extra_tags=${EXTRA}" >> "$GITHUB_OUTPUT" + fi + echo "Fleet version: ${FLEET_VERSION}, image tag: ${IMAGE_TAG}" + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: 'projects/103143301688/locations/global/workloadIdentityPools/github-wlif/providers/github-oidc' + service_account: 'github-actions-seer@kencove-prod.iam.gserviceaccount.com' + + - uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet + + - uses: docker/setup-buildx-action@v3 + + - name: Build image tags + id: tags + run: | + TAGS="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.image_tag }}" + if [[ -n "${{ steps.version.outputs.extra_tags }}" ]]; then + TAGS="${TAGS},${{ steps.version.outputs.extra_tags }}" + fi + echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" + + - uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.tags.outputs.tags }} + build-args: | + FLEET_VERSION=${{ steps.version.outputs.fleet_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Output image info + run: | + echo "## Build Complete" >> $GITHUB_STEP_SUMMARY + echo "Image: \`${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-ledo.yml b/.github/workflows/build-ledo.yml new file mode 100644 index 00000000000..4fdac75fe4f --- /dev/null +++ b/.github/workflows/build-ledo.yml @@ -0,0 +1,80 @@ +name: Build & Push Ledo Fleet Image + +on: + push: + branches: [ledoent] + tags: + - 'fleet-v*' + paths: + - 'cmd/**' + - 'ee/**' + - 'server/**' + - 'frontend/**' + - 'orbit/**' + - 'pkg/**' + - 'go.mod' + - 'go.sum' + - 'package.json' + - 'yarn.lock' + - 'webpack.config.js' + - 'Dockerfile' + - '.github/workflows/build-ledo.yml' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ledoent/fleet + +jobs: + build-and-push: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Derive Fleet version and image tag + id: version + run: | + if [[ "$GITHUB_REF" == refs/tags/fleet-v* ]]; then + BASE_TAG="${GITHUB_REF#refs/tags/}" + else + BASE_TAG=$(git describe --tags --match 'fleet-v*' --abbrev=0 2>/dev/null || echo "fleet-vdev") + fi + FLEET_VERSION="${BASE_TAG#fleet-}" + IMAGE_TAG="${FLEET_VERSION}-ledo" + echo "fleet_version=${FLEET_VERSION}" >> "$GITHUB_OUTPUT" + echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo "Fleet version: ${FLEET_VERSION}, image tag: ${IMAGE_TAG}" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.image_tag }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + build-args: | + FLEET_VERSION=${{ steps.version.outputs.fleet_version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Output image info + run: | + echo "## Build Complete" >> $GITHUB_STEP_SUMMARY + echo "Image: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml index fd70c3a2cf6..291002f0014 100644 --- a/.github/workflows/build-orbit.yaml +++ b/.github/workflows/build-orbit.yaml @@ -31,6 +31,7 @@ permissions: jobs: build: + if: github.repository == 'fleetdm/fleet' runs-on: macos-latest steps: - name: Harden Runner diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml index c8d10c7212d..2eed39e5323 100644 --- a/.github/workflows/dogfood-gitops.yml +++ b/.github/workflows/dogfood-gitops.yml @@ -30,6 +30,7 @@ permissions: jobs: fleet-gitops: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 10 runs-on: ubuntu-latest steps: @@ -82,6 +83,8 @@ jobs: DOGFOOD_END_USER_SSO_METADATA: ${{ secrets.DOGFOOD_END_USER_SSO_METADATA }} DOGFOOD_TESTING_AND_QA_ENROLL_SECRET: ${{ secrets.DOGFOOD_TESTING_AND_QA_ENROLL_SECRET }} DOGFOOD_OKTA_CA_CERTIFICATE: ${{ secrets.DOGFOOD_OKTA_CA_CERTIFICATE }} + DOGFOOD_OKTA_ANDROID_MANAGEMENT_HINT: ${{ secrets.DOGFOOD_OKTA_ANDROID_MANAGEMENT_HINT }} + DOGFOOD_OKTA_IOS_MANAGEMENT_HINT: ${{ secrets.DOGFOOD_OKTA_IOS_MANAGEMENT_HINT }} DOGFOOD_OKTA_VERIFY_WINDOWS_URL: ${{ secrets.DOGFOOD_OKTA_VERIFY_WINDOWS_URL }} DOGFOOD_ENTRA_TENANT_ID: ${{ secrets.DOGFOOD_ENTRA_TENANT_ID }} DOGFOOD_OKTA_METADATA_URL_ADMINS: ${{ secrets.DOGFOOD_OKTA_METADATA_URL_ADMINS }} diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml index 0e884516d0b..4f8f06d0d3e 100644 --- a/.github/workflows/fleet-and-orbit.yml +++ b/.github/workflows/fleet-and-orbit.yml @@ -39,6 +39,7 @@ permissions: jobs: gen: + if: github.repository == 'fleetdm/fleet' runs-on: ubuntu-latest outputs: subdomain: ${{ steps.gen.outputs.subdomain }} @@ -61,6 +62,7 @@ jobs: echo "enroll_secret=$ENROLL" >> $GITHUB_OUTPUT run-server: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 strategy: matrix: @@ -178,6 +180,7 @@ jobs: # # This job also makes sure the Fleet server is up and running. set-enroll-secret: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 runs-on: ubuntu-latest needs: gen @@ -219,6 +222,7 @@ jobs: # Here we generate the Fleet Desktop and osqueryd targets for # macOS which can only be generated from a macOS host. build-macos-targets: + if: github.repository == 'fleetdm/fleet' # Set macOS version to '14' for building the binary as Fleet's minimum supported macOS version. runs-on: macos-14 steps: @@ -261,6 +265,7 @@ jobs: # We run this job in ubuntu because Github macOS runner doesn't have Docker # installed, and installing it is time consuming and unreliable. run-tuf-and-gen-pkgs: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 runs-on: ubuntu-latest needs: [gen, build-macos-targets] @@ -328,6 +333,7 @@ jobs: fleet-osquery.msi orbit-macos: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 runs-on: macos-latest needs: [gen, run-tuf-and-gen-pkgs] @@ -390,6 +396,7 @@ jobs: sudo ./it-and-security/lib/macos/scripts/uninstall-fleetd-macos.sh orbit-ubuntu: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 runs-on: ubuntu-latest needs: [gen, run-tuf-and-gen-pkgs] @@ -449,6 +456,7 @@ jobs: sudo apt remove fleet-osquery -y orbit-windows: + if: github.repository == 'fleetdm/fleet' timeout-minutes: 60 needs: [gen, run-tuf-and-gen-pkgs] runs-on: windows-latest diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index f3e6b2d3b17..0ccc64abef1 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -73,7 +73,7 @@ jobs: run: | # Don't forget to update # docs/Contributing/Testing-and-local-development.md when this version changes - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@a4b55ebc3471c9fbb763fd56eefede8050f99887 # v2.7.1 + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@6008b81b81c690c046ffc3fd5bce896da715d5fd # v2.11.3 SKIP_INCREMENTAL=1 make lint-go - name: Run cloner-check tool @@ -136,7 +136,7 @@ jobs: run: | # Don't forget to update # docs/Contributing/Testing-and-local-development.md when this version changes - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@a4b55ebc3471c9fbb763fd56eefede8050f99887 # v2.7.1 + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@6008b81b81c690c046ffc3fd5bce896da715d5fd # v2.11.3 # custom build of golangci-lint that incorporates nilaway - see .custom-gcl.yml golangci-lint custom ./custom-gcl run -c .golangci-incremental.yml --new-from-rev=origin/${{ github.base_ref }} --timeout 15m ./... diff --git a/.github/workflows/test-fma-windows-pr-only.yml b/.github/workflows/test-fma-windows-pr-only.yml index 155b40dae37..26e620289b7 100644 --- a/.github/workflows/test-fma-windows-pr-only.yml +++ b/.github/workflows/test-fma-windows-pr-only.yml @@ -96,6 +96,7 @@ jobs: "has_windows_apps=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "has_google_chrome=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append exit 0 } @@ -107,6 +108,7 @@ jobs: "has_windows_apps=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "has_google_chrome=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "No windows apps changed, skipping Windows workflow" } else { "has_windows_apps=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append @@ -129,6 +131,14 @@ jobs: } else { "has_7zip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append } + + # Check if firefox/windows or firefox@esr/windows is in the changed apps + if (("firefox/windows" -in $windowsSlugs) -or ("firefox@esr/windows" -in $windowsSlugs)) { + "has_firefox=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "Firefox detected in changed apps" + } else { + "has_firefox=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + } } shell: pwsh @@ -232,6 +242,90 @@ jobs: } shell: pwsh + - name: Remove pre-installed Firefox + if: steps.check-windows-apps.outputs.has_windows_apps == 'true' && steps.check-windows-apps.outputs.has_firefox == 'true' + run: | + Write-Host "Listing all installed packages containing 'Firefox':" + Get-Package | Where-Object { $_.Name -like "*Firefox*" } | ForEach-Object { + Write-Host " - $($_.Name) (Version: $($_.Version))" + } + + $uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + $found = $false + foreach ($path in $uninstallPaths) { + $entries = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*Mozilla Firefox*" } + foreach ($entry in $entries) { + if (-not $entry) { continue } + $found = $true + Write-Host "Found Firefox: $($entry.DisplayName)" + + $uninstallString = if ($entry.QuietUninstallString) { + $entry.QuietUninstallString + } elseif ($entry.UninstallString) { + $entry.UninstallString + } else { + $null + } + + if ($uninstallString) { + Write-Host "Uninstall string: $uninstallString" + try { + $splitArgs = $uninstallString.Split('"') + if ($splitArgs.Length -ge 3) { + $exePath = $splitArgs[1] + Write-Host "Uninstalling Firefox via: $exePath /S" + Start-Process -FilePath $exePath -ArgumentList "/S" -Wait -NoNewWindow + Write-Host "Successfully removed $($entry.DisplayName)" + } else { + Write-Host "Uninstalling Firefox via: $uninstallString /S" + Start-Process -FilePath $uninstallString -ArgumentList "/S" -Wait -NoNewWindow + Write-Host "Successfully removed $($entry.DisplayName)" + } + } catch { + Write-Host "Failed to remove Firefox: $($_.Exception.Message)" + } + } else { + Write-Host "Firefox uninstall string not found in registry entry" + } + } + } + + if (-not $found) { + Write-Host "Firefox not found in registry" + } + + # Kill any lingering Firefox/Mozilla processes + Write-Host "Stopping any lingering Firefox processes..." + Get-Process -Name "firefox","plugin-container","updater","maintenanceservice*","helper" -ErrorAction SilentlyContinue | ForEach-Object { + Write-Host " Killing process: $($_.Name) (PID: $($_.Id))" + Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue + } + + Start-Sleep -Seconds 10 + + # Force-remove leftover Firefox directories from Program Files + $firefoxDirs = @( + "C:\Program Files\Mozilla Firefox", + "C:\Program Files (x86)\Mozilla Firefox", + "C:\Program Files\Mozilla Maintenance Service" + ) + foreach ($dir in $firefoxDirs) { + if (Test-Path $dir) { + Write-Host "Removing leftover directory: $dir" + Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue + if (Test-Path $dir) { + Write-Host "WARNING: Failed to fully remove $dir" + } else { + Write-Host "Removed $dir" + } + } + } + shell: pwsh + - name: Filter apps.json and verify changed apps if: steps.check-windows-apps.outputs.has_windows_apps == 'true' run: | diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 88364273b44..e960ebd17ec 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -31,6 +31,7 @@ permissions: jobs: trivy: + if: github.repository == 'fleetdm/fleet' permissions: id-token: write # for aws-actions/configure-aws-credentials contents: read # for actions/checkout to fetch code diff --git a/.golangci-incremental.yml b/.golangci-incremental.yml index be027cc707b..d7a9ed92676 100644 --- a/.golangci-incremental.yml +++ b/.golangci-incremental.yml @@ -10,9 +10,25 @@ issues: linters: default: none enable: + - gosec - modernize - nilaway settings: + gosec: + # Only enable rules that are too noisy on existing code but valuable for new code. + # Existing violations were audited during the v2.7.1 -> v2.11.3 upgrade and found + # to be false positives or safe patterns, but we want to catch real issues going forward. + includes: + - G101 # Potential hardcoded credentials. + - G115 # Integer overflow conversion. + - G117 # Marshaled struct field matches secret pattern. + - G118 # Goroutine uses context.Background/TODO while request-scoped context is available. + - G122 # Filesystem race in filepath.Walk/WalkDir callback. + - G202 # SQL string concatenation. + - G602 # Slice index out of range. + - G704 # SSRF via taint analysis. + - G705 # XSS via taint analysis. + - G706 # Log injection via taint analysis. custom: nilaway: type: module diff --git a/.golangci.yml b/.golangci.yml index 7023fba2a0c..3681ff5bd00 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -177,7 +177,22 @@ linters: - G104 # Errors unhandled. We are using errcheck linter instead of this rule. - G204 # Subprocess launched with variable. Some consider this rule to be too noisy. - G301 # Directory permissions 0750 as opposed to standard 0755. Consider enabling stricter permission in the future. - - G304 # File path provided as taint input + - G304 # File path provided as taint input. + - G702 # Command injection via taint analysis (taint version of excluded G204). + - G703 # Path traversal via taint analysis (taint version of excluded G304). + # The following rules are excluded from the full lint but enabled in the incremental + # linter (.golangci-incremental.yml) so they only apply to new/changed code. + # Existing violations were audited during the v2.7.1 -> v2.11.3 upgrade. + - G101 # Potential hardcoded credentials. + - G115 # Integer overflow conversion. + - G117 # Marshaled struct field matches secret pattern. + - G118 # Goroutine uses context.Background/TODO while request-scoped context is available. + - G122 # Filesystem race in filepath.Walk/WalkDir callback. + - G202 # SQL string concatenation. + - G602 # Slice index out of range. + - G704 # SSRF via taint analysis. + - G705 # XSS via taint analysis. + - G706 # Log injection via taint analysis. config: G306: "0644" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0726a2f7f0..b67760c527b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: hooks: - id: gitleaks - repo: https://github.com/golangci/golangci-lint - rev: v2.7.1 + rev: v2.11.3 hooks: - id: golangci-lint - repo: https://github.com/jumanjihouse/pre-commit-hooks diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..edaa8f8fdc9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Multi-stage Dockerfile for Fleet with PostgreSQL dialect support +# Built from ledoent/fleet fork with DialectHelper abstraction + +# Stage 1: Build frontend assets +FROM node:24-bookworm AS frontend +WORKDIR /build +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile --network-timeout 600000 +COPY . . +RUN NODE_ENV=production yarn run webpack --progress + +# Stage 2: Build Go binary +FROM golang:1.26.1-bookworm AS backend +RUN apt-get update && apt-get install -y --no-install-recommends gcc +WORKDIR /build +COPY --from=frontend /build . +RUN go run github.com/kevinburke/go-bindata/go-bindata -pkg=bindata -tags full \ + -o=server/bindata/generated.go \ + frontend/templates/ assets/... server/mail/templates +ARG FLEET_VERSION=dev +RUN CGO_ENABLED=1 go build -tags full,fts5,netgo -trimpath \ + -ldflags "-extldflags '-static' \ + -X github.com/fleetdm/fleet/v4/server/version.version=${FLEET_VERSION}-ledoent \ + -X github.com/fleetdm/fleet/v4/server/version.branch=ledoent" \ + -o fleet ./cmd/fleet + +# Stage 3: Runtime image +FROM alpine:3.21 +RUN apk --no-cache add ca-certificates tini +RUN addgroup -S fleet && adduser -S fleet -G fleet +USER fleet +COPY --from=backend /build/fleet /usr/bin/fleet +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["fleet", "serve"] diff --git a/Dockerfile-desktop-linux b/Dockerfile-desktop-linux index db24f9380a0..4c07f5b5da2 100644 --- a/Dockerfile-desktop-linux +++ b/Dockerfile-desktop-linux @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 golang:1.25.7-trixie@sha256:dfdd969010ba978942302cee078235da13aef030d22841e873545001d68a61a7 +FROM --platform=linux/amd64 golang:1.26.1-trixie@sha256:96b28783b99bcd265fbfe0b36a3ac6462416ce6bf1feac85d4c4ff533cbaa473 LABEL maintainer="Fleet Developers" RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/* diff --git a/Makefile b/Makefile index eb057710718..f95dda14aa7 100644 --- a/Makefile +++ b/Makefile @@ -582,6 +582,15 @@ e2e-serve-premium: e2e-reset-db e2e-set-desktop-token: docker compose exec -T mysql_test bash -c 'echo "INSERT INTO e2e.host_device_auth (host_id, token) VALUES ($(host_id), \"$(token)\") ON DUPLICATE KEY UPDATE token=VALUES(token)" | MYSQL_PWD=toor mysql -uroot' +# PostgreSQL e2e targets (experimental — requires postgres_test docker container) +e2e-reset-db-pg: + docker compose exec -T postgres_test psql -U fleet -c 'DROP DATABASE IF EXISTS e2e;' && \ + docker compose exec -T postgres_test psql -U fleet -c 'CREATE DATABASE e2e;' + ./build/fleet prepare db --mysql_driver=postgres --mysql_address=localhost:$${FLEET_POSTGRES_TEST_PORT:-5434} --mysql_username=fleet --mysql_password=insecure --mysql_database=e2e + +e2e-serve-pg: e2e-reset-db-pg + ./build/fleet serve --mysql_driver=postgres --mysql_address=localhost:$${FLEET_POSTGRES_TEST_PORT:-5434} --mysql_username=fleet --mysql_password=insecure --mysql_database=e2e --server_address=0.0.0.0:8642 + changelog: find changes -type f ! -name .keep -exec awk 'NF' {} + > new-CHANGELOG.md sh -c "cat new-CHANGELOG.md CHANGELOG.md > tmp-CHANGELOG.md && rm new-CHANGELOG.md && mv tmp-CHANGELOG.md CHANGELOG.md" diff --git a/articles/agritech-producer.md b/articles/agritech-producer.md index be0d908d1e5..997bce7e1b8 100644 --- a/articles/agritech-producer.md +++ b/articles/agritech-producer.md @@ -34,3 +34,4 @@ Fleet is the open-source endpoint management platform that gives you total contr + \ No newline at end of file diff --git a/articles/ai-security-company.md b/articles/ai-security-company.md index 8eb2d75f86a..a4b65ba8787 100644 --- a/articles/ai-security-company.md +++ b/articles/ai-security-company.md @@ -34,4 +34,4 @@ Fleet is the open-source endpoint management platform that gives you total contr - + diff --git a/articles/banking-platform.md b/articles/banking-platform.md index 0e25c5fadc9..d2903f59aff 100644 --- a/articles/banking-platform.md +++ b/articles/banking-platform.md @@ -36,3 +36,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/cannabis-technology-company.md b/articles/cannabis-technology-company.md index 278b915511b..80b688e3628 100644 --- a/articles/cannabis-technology-company.md +++ b/articles/cannabis-technology-company.md @@ -36,3 +36,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + diff --git a/articles/cloud-data-platform.md b/articles/cloud-data-platform.md index c0bffe971f4..7a4474debbe 100644 --- a/articles/cloud-data-platform.md +++ b/articles/cloud-data-platform.md @@ -77,3 +77,4 @@ The cloud data platform's adoption of Fleet Device Management exemplifies how mo + diff --git a/articles/collaboration-platform.md b/articles/collaboration-platform.md index 801c30792e6..e9fbefb6b23 100644 --- a/articles/collaboration-platform.md +++ b/articles/collaboration-platform.md @@ -79,3 +79,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/communications-platform.md b/articles/communications-platform.md index 0f46b1bcab9..294e25029d8 100644 --- a/articles/communications-platform.md +++ b/articles/communications-platform.md @@ -38,3 +38,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/computational-research-company.md b/articles/computational-research-company.md new file mode 100644 index 00000000000..4d046d9e563 --- /dev/null +++ b/articles/computational-research-company.md @@ -0,0 +1,75 @@ +# Computational research company unifies endpoint management with Fleet + +A computational research company develops software that accelerates drug discovery and materials science. Its teams rely on macOS, Linux, and Windows devices across a highly technical environment. + +Before Fleet, Linux desktops and servers were less visible and harder to manage. Fleet helps the company bring those systems into a single platform with better automation and stronger policy enforcement. + +## At a glance + +* **Industry:** Healthcare technology and computational research + +* **Devices managed:** ~1,553 devices across macOS, Linux, and Windows + +* **Primary requirements:** Unified visibility, security enforcement, GitOps, and osquery support + +* **Previous challenge:** Linux systems lacked consistent management and visibility + +## The challenge + +Before Fleet, the company relied on multiple tools across operating systems. + +Workspace ONE did not meet expectations for support and communication. Intune lacked some Windows capabilities the team needed, and Jamf did not provide sufficient depth for a mixed environment with a large Linux footprint. + +Linux desktops and servers were the biggest gap. The team needed a way to bring those systems into a more consistent security and compliance program. + +## The evaluation criteria + +The team focused on three priorities: + +1. **Unified visibility** + Manage macOS, Windows, and Linux from one place. + +2. **Security and compliance enforcement** + Automatically apply policies and reduce manual work. + +3. **GitOps and osquery support** + Manage configuration through code and use SQL-based telemetry for deeper visibility. + +## The solution + +Fleet gave the team a single system for policy management, device visibility, and automation. + +The company uses Fleet to run scripts on Linux, map device users via SCIM, and schedule updates that reduce disruption for scientists and technical staff. Fleet also replaced parts of its previous automation stack, which reduced complexity. + +The open-source model was a strong fit because it gave the team direct visibility into how the platform works and how features evolve over time. + +## The results + +Fleet helped the company reduce silos and improve device oversight across operating systems. + +* **Improved Linux visibility:** Linux devices that were previously less managed are now part of a unified workflow. + +* **Stronger vulnerability response:** Real-time telemetry and policy enforcement help the team respond faster. + +* **Less tool sprawl:** Fleet replaced separate workflows and helped centralize management. + +## Why they recommend Fleet + +For this company, the biggest benefit is unified management. Fleet gives the team one place to manage macOS, Linux, and Windows with greater transparency, automation, and reduced operational overhead. + +## About Fleet + +Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs. + +By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives. + +Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction. + + + + + + + + + diff --git a/articles/consumer-electronics.md b/articles/consumer-electronics.md new file mode 100644 index 00000000000..0caef470cd2 --- /dev/null +++ b/articles/consumer-electronics.md @@ -0,0 +1,75 @@ +# Consumer electronics company simplifies cross-platform management with Fleet + +A consumer electronics company supports employees and contractors across a global business. Its environment includes macOS, Windows, and Linux devices used by both business teams and engineers. + +Existing tools created friction and left Linux unmanaged. Fleet helps the company manage all devices in one place. + +## At a glance + +* **Industry:** Consumer electronics and audio technology + +* **Devices managed:** ~3,200-3,400 devices + +* **Primary requirements:** GitOps workflows, visibility into all devices, unified management + +* **Previous challenge:** Jamf and Intune created bottlenecks with weak Linux coverage + +## The challenge + +Before Fleet, the company relied on Jamf and Intune. + +Those tools created friction in different ways. Jamf involved certificate and profile complexity, and Intune was slow to return data and lacked some remote management features the team needed. Linux devices remained a major blind spot. + +The team wanted one platform that could support all major operating systems and reduce the need to switch between consoles. + +## The evaluation criteria + +The team focused on three capabilities: + +1. **GitOps workflows** + Manage devices through code and version control. + +2. **osquery visibility** + Collect deep, real-time endpoint data. + +3. **Unified management** + Support macOS, Windows, and Linux in one system. + +## The solution + +Fleet gave the team one platform to track devices, enforce compliance, and query data in real time. + +The company integrated Fleet with internal inventory systems and GitHub to automate compliance tracking and keep asset records up to date. Linux enrollment is being rolled out in phases, with a self-service model designed to support adoption without disrupting engineering workflows. + +The open development model also helped build trust with technical users who want visibility into how the product evolves. + +## The results + +Fleet helps the team reduce complexity and improve response time. + +* **Improved Linux coverage:** Linux devices are now managed for the first time. + +* **Faster access to device data:** Teams can act on compliance and security issues faster. + +* **Tool consolidation:** Fewer separate management systems are needed across operating systems. + +## Why they recommend Fleet + +For this company, the biggest benefit is operational simplicity. Fleet helps their team manage more devices with fewer tools and faster access to the data they need. + +## About Fleet + +Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs. + +By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives. + +Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction. + + + + + + + + + diff --git a/articles/cybersecurity-company-1.md b/articles/cybersecurity-company-1.md index e29c4587332..36202036cde 100644 --- a/articles/cybersecurity-company-1.md +++ b/articles/cybersecurity-company-1.md @@ -74,3 +74,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/cybersecurity-company.md b/articles/cybersecurity-company.md index d77e43058f1..1957cc7c85e 100644 --- a/articles/cybersecurity-company.md +++ b/articles/cybersecurity-company.md @@ -84,3 +84,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/data-platform.md b/articles/data-platform.md new file mode 100644 index 00000000000..9a930c6cb53 --- /dev/null +++ b/articles/data-platform.md @@ -0,0 +1,76 @@ +# Data platform company cuts $5–6M in hardware costs with API-driven device management + +A global data platform company supports a large workforce across macOS, Windows, Linux, iOS, and Android. With device count matching its workforce, the team needed a scalable way to manage hardware, security, and compliance. + +Fleet helps the company manage devices through APIs and stream endpoint telemetry directly into its own data platform. + +## At a glance + +* **Industry:** Data cloud technology + +* **Devices managed:** ~10,000-11,000 devices + +* **Primary requirements:** API and GitOps support, multi-OS management, real-time data streaming + +* **Previous challenge:** Legacy tools were expensive and did not support Linux and BYOD well + +## The challenge + +Before Fleet, the company relied on tools that worked well for macOS but did not support Linux, Android, and BYOD with the same depth. + +The team also wanted to move away from expensive licensing models and manual UI-driven workflows. Linux devices and BYOD systems were especially hard to manage, which limited visibility across the environment. + +## The evaluation criteria + +The team focused on three priorities: + +1. **API and GitOps support** + Remove manual operations and manage workflows through code. + +2. **Multi-OS management** + Support macOS, iOS, Android, Windows, and Linux from one platform. + +3. **Real-time data streaming** + Send endpoint telemetry directly into the company’s internal data platform. + +## The solution + +Fleet provided the team with a platform that aligns with its API-first engineering model. + +The company uses Fleet for asynchronous file verification and streams endpoint telemetry directly into its internal data environment. This allows security teams to query, model, and act on endpoint data within minutes. + +Fleet inventory data also helps the company make better hardware decisions, including identifying overprovisioned devices and improving refresh planning. + +## The results + +Fleet improved both operational efficiency and cost control. + +* **Major hardware savings:** Fleet data helped identify opportunities that saved an estimated $5-6 million in hardware costs. + +* **Better multi-OS visibility:** Linux and BYOD systems now fit into the broader management strategy. + +* **Faster security analysis:** Streaming telemetry shortens the gap between data collection and response. + +## Why they recommend Fleet + +For this company, the biggest benefit is API-driven efficiency. + +Fleet gives the team one platform for automation, cross-platform management, and real-time endpoint data. + +## About Fleet + +Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs. + +By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives. + +Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction. + + + + + + + + + + \ No newline at end of file diff --git a/articles/deploy-santa-with-fleet-gitops-and-skip-the-sync-server.md b/articles/deploy-santa-with-fleet-gitops-and-skip-the-sync-server.md index aa91be98a75..cb18e64810b 100644 --- a/articles/deploy-santa-with-fleet-gitops-and-skip-the-sync-server.md +++ b/articles/deploy-santa-with-fleet-gitops-and-skip-the-sync-server.md @@ -71,4 +71,3 @@ The [next article](https://fleetdm.com/guides/how-we-deployed-santa-at-fleet) in - diff --git a/articles/digital-bank.md b/articles/digital-bank.md index bfedf7ccf08..b75d20d77c5 100644 --- a/articles/digital-bank.md +++ b/articles/digital-bank.md @@ -83,3 +83,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/electric-vehicle-manufacturer.md b/articles/electric-vehicle-manufacturer.md index 8ad7aea1501..501829feda7 100644 --- a/articles/electric-vehicle-manufacturer.md +++ b/articles/electric-vehicle-manufacturer.md @@ -83,3 +83,4 @@ The decision to purchase Fleet was driven by the need for a more reliable, compr + \ No newline at end of file diff --git a/articles/ev-manufacturer.md b/articles/ev-manufacturer.md index d566f1f7922..89b10dbbb64 100644 --- a/articles/ev-manufacturer.md +++ b/articles/ev-manufacturer.md @@ -83,3 +83,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/financial-data-company.md b/articles/financial-data-company.md index 382dea372f8..9c00244a80a 100644 --- a/articles/financial-data-company.md +++ b/articles/financial-data-company.md @@ -91,4 +91,5 @@ Fleet is the single endpoint management platform for macOS, iOS, Android, Window - \ No newline at end of file + + \ No newline at end of file diff --git a/articles/financial-services-company-1.md b/articles/financial-services-company-1.md index d8341b7e088..8e713f89c91 100644 --- a/articles/financial-services-company-1.md +++ b/articles/financial-services-company-1.md @@ -75,3 +75,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/financial-services-company.md b/articles/financial-services-company.md index 7557c8d75c8..391f25b5cb2 100644 --- a/articles/financial-services-company.md +++ b/articles/financial-services-company.md @@ -70,4 +70,6 @@ The migration to [Fleet Device Management](https://fleetdm.com/device-management - \ No newline at end of file + + + \ No newline at end of file diff --git a/articles/financial-services-platform.md b/articles/financial-services-platform.md index 286c0c3bcc1..b2e336c61b2 100644 --- a/articles/financial-services-platform.md +++ b/articles/financial-services-platform.md @@ -36,3 +36,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/financial-technology-company.md b/articles/financial-technology-company.md index 434150afeff..92db49776c3 100644 --- a/articles/financial-technology-company.md +++ b/articles/financial-technology-company.md @@ -36,3 +36,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/fintech-company-strengthens-infrastructure-visibility.md b/articles/fintech-company-strengthens-infrastructure-visibility.md index 1ff6e11cad8..f3e141b2bc9 100644 --- a/articles/fintech-company-strengthens-infrastructure-visibility.md +++ b/articles/fintech-company-strengthens-infrastructure-visibility.md @@ -72,3 +72,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/fintech-company.md b/articles/fintech-company.md index 7b87dcc834d..f16d33102f4 100644 --- a/articles/fintech-company.md +++ b/articles/fintech-company.md @@ -80,3 +80,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/fleet-4.82.0.md b/articles/fleet-4.82.0.md index 05f2576a779..ca6619c8cee 100644 --- a/articles/fleet-4.82.0.md +++ b/articles/fleet-4.82.0.md @@ -1,7 +1,7 @@ # Fleet 4.82.0 | Fleets and reports, new technician role, and more...
- +
Fleet 4.82.0 is now available. See the complete [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.82.0) or read on for highlights. For upgrade instructions, visit the [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. diff --git a/articles/gaming-platform.md b/articles/gaming-platform.md index 07faa573185..5778878f7a2 100644 --- a/articles/gaming-platform.md +++ b/articles/gaming-platform.md @@ -102,3 +102,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/gaming-technology-company.md b/articles/gaming-technology-company.md index 89317124b49..89c166329d0 100644 --- a/articles/gaming-technology-company.md +++ b/articles/gaming-technology-company.md @@ -36,3 +36,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/global-entertainment-company.md b/articles/global-entertainment-company.md index a52d54c24a8..8d9655a7c4e 100644 --- a/articles/global-entertainment-company.md +++ b/articles/global-entertainment-company.md @@ -76,3 +76,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + diff --git a/articles/global-saas-company.md b/articles/global-saas-company.md index 6c4010241c3..ff11f5dc926 100644 --- a/articles/global-saas-company.md +++ b/articles/global-saas-company.md @@ -85,3 +85,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/global-social-media-platform.md b/articles/global-social-media-platform.md index 00e8de68091..f8669d0dd92 100644 --- a/articles/global-social-media-platform.md +++ b/articles/global-social-media-platform.md @@ -78,4 +78,4 @@ Transitioning to Fleet provided the platform with a strategic solution that addr - + diff --git a/articles/global-technology-platform.md b/articles/global-technology-platform.md index faa821469bb..53b9731f964 100644 --- a/articles/global-technology-platform.md +++ b/articles/global-technology-platform.md @@ -113,3 +113,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/global-workforce-management-company.md b/articles/global-workforce-management-company.md index 94ee93c74fd..19551457ec6 100644 --- a/articles/global-workforce-management-company.md +++ b/articles/global-workforce-management-company.md @@ -66,3 +66,6 @@ By switching to Fleet, this global workforce management company gained a powerfu + + + diff --git a/articles/healthcare-technology-organization.md b/articles/healthcare-technology-organization.md index 6ecd2e3cee5..619bf2a0068 100644 --- a/articles/healthcare-technology-organization.md +++ b/articles/healthcare-technology-organization.md @@ -34,3 +34,4 @@ Fleet is the open-source endpoint management platform that gives you total contr + \ No newline at end of file diff --git a/articles/identity-platform.md b/articles/identity-platform.md index 29a35061b60..d30f766c3f0 100644 --- a/articles/identity-platform.md +++ b/articles/identity-platform.md @@ -76,3 +76,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + diff --git a/articles/identity-security-company.md b/articles/identity-security-company.md index ae6ad820192..6cf070515f8 100644 --- a/articles/identity-security-company.md +++ b/articles/identity-security-company.md @@ -74,3 +74,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/interactive-entertainment-company.md b/articles/interactive-entertainment-company.md index 1d6e8b6fa83..67e5f01375d 100644 --- a/articles/interactive-entertainment-company.md +++ b/articles/interactive-entertainment-company.md @@ -74,3 +74,4 @@ By switching to Fleet, it solved their critical challenges in device management, + \ No newline at end of file diff --git a/articles/it-platform-provider.md b/articles/it-platform-provider.md index c494b759f8b..2d580b54ec7 100644 --- a/articles/it-platform-provider.md +++ b/articles/it-platform-provider.md @@ -33,4 +33,5 @@ Fleet is the open-source endpoint management platform that gives you total contr - \ No newline at end of file + + \ No newline at end of file diff --git a/articles/it-service-company.md b/articles/it-service-company.md new file mode 100644 index 00000000000..74d02a90579 --- /dev/null +++ b/articles/it-service-company.md @@ -0,0 +1,73 @@ +# IT services company builds zero-touch workflows with Fleet + +This IT services company provides endpoint management and security solutions for enterprise customers. As it expands its own environment, the team wants device management that is secure by default and easy to automate. + +Fleet helps the company build zero-touch deployment workflows and manage device state through code. + +## At a glance + +* **Industry:** IT services and endpoint management + +* **Devices managed:** 100+ Windows devices, expanding to macOS and Linux + +* **Primary requirements:** Zero-touch deployment, GitOps workflows, osquery visibility + +* **Previous challenge:** Legacy tools could not support secure, automated deployments at scale + +## The challenge + +Before Fleet, the company relied on legacy tools that were not built for zero-touch, secure laptop deployment. + +The team needed better visibility into device state and a more automated way to manage credentials, policies, and user transitions. Existing tools relied on manual workflows, created friction, and could not support a secure-by-default model at scale. + +## The evaluation criteria + +The team focused on three capabilities: + +1. **Zero-touch deployment** + Automate device setup from the moment a machine is unboxed. + +2. **GitOps workflows** + Manage device state programmatically rather than through manual UI actions. + +3. **osquery integration** + Use deep system-level data to drive security workflows. + +## The solution + +Fleet gave the team a platform for automated deployment, identity-aware workflows, and real-time visibility. + +The company used the Fleet API to rotate Recovery Lock passwords, manage hidden admin accounts, and respond to identity changes without manual intervention. Integrations with Okta also helped automatically remove deactivated users, reducing the risk of leftover access on managed devices. + +The open-source model was important because it allowed the team to inspect the code and verify behavior directly. + +## The results + +Fleet gave the team a stronger foundation for secure deployment and lifecycle management. + +* **Better zero-touch workflows:** New devices can be set up with more consistency and less manual work. + +* **Faster security response:** Identity and device changes can trigger action quickly. + +* **Simpler management:** The team can centralize security policies across operating systems. + +## Why they recommend Fleet + +For this company, the biggest benefit is zero-touch visibility. Fleet helps the team build automated workflows that are more secure, more scalable, and easier to verify. + +## About Fleet + +Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs. + +By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives. + +Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction. + + + + + + + + + \ No newline at end of file diff --git a/articles/it-service-provider.md b/articles/it-service-provider.md index 88f48a92573..7b73b246b27 100644 --- a/articles/it-service-provider.md +++ b/articles/it-service-provider.md @@ -40,3 +40,4 @@ Fleet is the open-source endpoint management platform that gives you total contr + diff --git a/articles/journalism-nonprofit.md b/articles/journalism-nonprofit.md index d0ee7e5927d..4ccaebeedba 100644 --- a/articles/journalism-nonprofit.md +++ b/articles/journalism-nonprofit.md @@ -33,4 +33,5 @@ Fleet is the open-source endpoint management platform that gives you total contr - \ No newline at end of file + + \ No newline at end of file diff --git a/articles/lock-wipe-hosts.md b/articles/lock-wipe-hosts.md index 9a9a15c1e3c..3870a2f9a9d 100644 --- a/articles/lock-wipe-hosts.md +++ b/articles/lock-wipe-hosts.md @@ -55,7 +55,7 @@ When wiping and re-installing the operating system (OS) on a host, delete the ho If you're gifting a company-owned macOS host or you want to prevent the host from automatically re-enrolling to Fleet for some other reason, first release the host from Apple Business Manager (ABM) and then delete the host in Fleet. -For Windows hosts, Fleet uses the [doWipeProtected](https://learn.microsoft.com/en-us/windows/client-management/mdm/remotewipe-csp#dowipeprotected) command. According to Microsoft, this leaves the host [unable to boot](https://learn.microsoft.com/en-us/windows/client-management/mdm/remotewipe-csp#:~:text=In%20some%20device%20configurations%2C%20this%20command%20may%20leave%20the%20device%20unable%20to%20boot.). +For Windows hosts, Fleet uses the [doWipeProtected](https://learn.microsoft.com/en-us/windows/client-management/mdm/remotewipe-csp#dowipeprotected) command by default. According to Microsoft, this leaves the host [unable to boot](https://learn.microsoft.com/en-us/windows/client-management/mdm/remotewipe-csp#:~:text=In%20some%20device%20configurations%2C%20this%20command%20may%20leave%20the%20device%20unable%20to%20boot.). However, it is possible to use the [doWipe command via the API](https://fleetdm.com/docs/rest-api/rest-api#parameters57). ## Unlock a host diff --git a/articles/medical-research-institution.md b/articles/medical-research-institution.md index 6935e6d72a9..d316b8506ad 100644 --- a/articles/medical-research-institution.md +++ b/articles/medical-research-institution.md @@ -74,3 +74,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/national-research-lab.md b/articles/national-research-lab.md index 67e86672b78..5ae6379ceec 100644 --- a/articles/national-research-lab.md +++ b/articles/national-research-lab.md @@ -71,5 +71,6 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na - + + diff --git a/articles/national-research-organization.md b/articles/national-research-organization.md index 84e2d40ac94..dfcfb1e0fdd 100644 --- a/articles/national-research-organization.md +++ b/articles/national-research-organization.md @@ -76,3 +76,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/old-it-is-dead.md b/articles/old-it-is-dead.md new file mode 100644 index 00000000000..475d3ea3a1c --- /dev/null +++ b/articles/old-it-is-dead.md @@ -0,0 +1,144 @@ +# Old IT is dead - GitOps & AI are burying it + +![old-IT-is-dead](../website/assets/images/articles/old-IT-is-dead-736x414@2x.png) + +I was a skeptic. + +For years, "AI in IT" meant chatbots that confidently gave you wrong answers, autocomplete that finished your sentences badly, and a lot of vendor hype that amounted to a fancy search bar. If you tried AI tooling before late 2025 and walked away unimpressed, I completely understand. So did I. + +But something changed around November 2025. These models got *good*. Not incrementally better but genuinely, meaningfully good. And for those of us working with structured, well-documented, open data, the leap forward is almost unfair in the best possible way. + +This is my journey of how I started using AI to manage devices with Fleet and why I think it represents the biggest shift in how IT work gets done in a generation. + +## First, a word about "Old IT" + +You know the drill. A change needs to happen. You schedule a meeting. Then another meeting to review the output from the first meeting. A change advisory board convenes. Someone asks if we've documented the rollback plan, and the honest answer is "not really." The change goes in, something breaks, and you spend three days figuring out what happened because there's no true audit trail, no version history, and no transparency into exactly what changed and when. + +Old IT is slow, opaque, and manual. And for a long time, we accepted it because there wasn't a better way. + +There is now. + +## Why Fleet? Why now? + +Fleet is an open-source device management platform, and it is uniquely positioned for this moment in a way that no other vendor comes close to matching. + +Here's why: + +The source code is open. AI models can read it, reason about it, and work with it. There's no black box and no proprietary API that the model has to guess at. + +There is a single, well-documented API. When you give an AI a coherent, documented surface to work against, the results are dramatically better. Fleet's API isn't a patchwork of legacy endpoints. It's structured, consistent, and readable. + +GitOps is a first-class citizen. Fleet is designed from the ground up to be managed through code: through pull requests, version history, and Infrastructure as Code workflows. That's not a bolt-on feature. It's the architecture. + +No other device management company is as well-positioned to take advantage of this new era of IT. + +## The Intune breach we should all be thinking about + +The [recent Microsoft Intune breach](https://krebsonsecurity.com/2026/03/iran-backed-hackers-claim-wiper-attack-on-medtech-firm-stryker/) is a case study in what happens when your management plane is a GUI that any compromised account can click through. + +Think about what Infrastructure-as-Code, and the products built natively around it, can actually give you: a read-only UI. Changes can only come through code, through pull requests, through a reviewable, auditable process. There is no "log in and click deploy" path for a bad actor, or an honest mistake, to exploit. + +If the Intune environment that was breached had been managed through Infrastructure as Code, the blast radius of that breach would have been fundamentally different. The attack surface shrinks dramatically when the UI can show you the state but cannot change it. + +This isn't hypothetical security theater. It's a structural property of the GitOps model. And it matters, in this moment. + +## How the journey started: small, specific, concrete + +I didn't start by asking AI to solve everything. + +I started small. + +*"Make sure all my workstations are running the latest version of Mozilla Firefox."* + +That's it. A simple, finite, verifiable task. The AI looked at [Fleet's GitOps documentation](https://fleetdm.com/docs/configuration/yaml-files), understood the desired state model, and produced a policy that I could review, commit, and ship. It worked exactly as expected. + +From there, the requests got a little more abstract. *"Make Slack and Zoom available during the setup experience and self-service."* *"Make sure screen lock activates after fifteen minutes of inactivity."* Each time, correct output, correctly structured, ready to review. It even created configuration profiles without me having to use iMazing or Apple Configurator. + +But here's where it started to surprise me. When I asked it to deploy Mozilla Firefox, I was thinking about the *deployment*. The AI was thinking further ahead than I was originally. It detected that the installer was x86 and automatically created a label so the software would be scoped only to compatible devices. I hadn't asked for that. I hadn't even thought about it. And in the old world, I probably wouldn't have until an IT ticket landed in my team's queue from a confused user on an incompatible machine. The AI caught the edge case before it became someone's problem. That shift from "AI does what I ask" to "AI thinks about what I actually need" is when I realized this was something different. + +## Ready for production + +We started asking bigger questions. Not "configure this specific setting" but: + +*"Apply industry best practice security controls to my workstations."* + +*"Make my devices ready for ISO 27001 compliance."* ([Link to pull request](https://github.com/fleetdm/fleet/pull/40958)) + +And it delivered. Not just a list of things to configure, but a complete picture. The pull request included what controls we already had in place, called out what was missing, and explained why each addition mattered. Structured, coherent, policy-ready output mapped to real compliance frameworks, grounded in Fleet's actual configuration model, and ready for human review. + +You can see every experiment we've been running, including how we are using AI to keep documentation up to date in our public repo: [fleetdm/fleet on GitHub](https://github.com/fleetdm/fleet/issues?q=is%3Apr%20author%3Aapp%2Fkilo-code-bot%20state%3Aclosed) + +## A pleasant surprise: naming conventions + +Anyone who's worked in IT for more than a few years has opinions about naming conventions. Strong opinions. Perhaps, at times, unreasonably strong opinions. + +(I include myself in this. Fully. No apologies.) + +What caught me off guard was that when the AI generated policies, labels, and configuration files, the output matched my naming conventions. My commenting style. My structural preferences. It wasn't generic boilerplate. It read like something I had written. + +I don't know whether to attribute this to the AI being genuinely good at picking up on patterns or to the fact that good conventions in a well-documented open-source project tend to converge on something sensible. Either way, it was this small detail that made me think, "Okay, this is actually going to work." + +## Human in the loop: this is not "AI just deploys things" + +I want to be direct about something, because I think it's the most important part of this post for anyone who manages real devices for real people. + +Nothing ships without a human reviewing it. + +**This is GitOps:** Every change is a pull request. Every pull request is a diff you can read, a history you can audit, and a commit you can revert. Every change is documented. The AI proposes. A human reviews. The human approves or rejects. Only then does anything change in your environment. + +This is also why [GitOps training](https://fleetdm.com/gitops-workshop) matters. The workflow is the safety layer. If you haven't invested in understanding pull requests, branching strategies, and rollback procedures, now is the time because that understanding is what makes AI-assisted management safe, not just fast. + +The human is not a rubber stamp. The human is the point. + +## The tool that made this click for me: Kilo Code + +I've confirmed this workflow with two AI setups - Kilo Code and Claude - directly. It should work with any AI you're already invested in — OpenAI, Gemini, local models, whatever your organization has standardized on. + +Fleet's open architecture means the AI just needs to be able to read and reason about well-structured text, which all the capable modern models can do. + +But I want to specifically call out why Kilo Code resonated with me: I never opened a text editor. I never opened an IDE. I never typed a `git` command. + +I chatted with Slack. That's it. I described what I wanted in plain English, got back a diff to review, approved it, and it was done. For IT practitioners who are interested in GitOps but intimidated by the toolchain, this is the on-ramp. The barrier to entry is collapsing. + +## This is a superpower, not a pink slip + +I know what some of you are thinking. I've thought of it too. + +*"If AI can do my job, what do I do?"* + +Here's my honest answer: AI can handle repetitive, structured, and lookup-based work. It is genuinely, impressively good at that work now. And that should free you, not threaten you. + +The engineering work that requires human creativity, judgment, relationship, and context is not going anywhere: + +- Understanding your organization's actual risk tolerance +- Navigating a complex stakeholder conversation about a security tradeoff +- Knowing that the policy that's technically correct will cause a rebellion in the engineering org if you ship it on a Friday +- Building the trust that makes your IT and security programs actually work + +AI cannot do any of that. You can. + +What AI can do is handle the eighty percent of your backlog that is well-defined, documented, and implementable, so you can spend your time and energy on the twenty percent that actually requires you. + +That's not a threat. That's a gift, and you should exploit it. + +## The future is open source + +The through-line in all of this is openness. Open source platform. Open API. Open configuration manifests. Open workflows that any auditor, any colleague, any future team member can read and understand. + +Old IT was closed, opaque, and tribal — knowledge locked in the heads of whoever happened to have been there longest. New IT is transparent, reviewable, and collaborative. + +Fleet is built for new IT. AI just made new IT available to everyone. + +If you dismissed AI tooling before late 2025, I'd genuinely encourage you to try again. The models are different now. And when you point a good AI model at a well-documented, open-source solution, what comes out looks a lot like the future of device management. + +--- + +*Tested and confirmed working with Kilo Code and Claude. Architecturally compatible with any AI investment your organization has already made. The open source future is already here — you can see it and experience it yourself, right now.* + + + + + + + + diff --git a/articles/online-gaming-platform.md b/articles/online-gaming-platform.md index c553753a5f6..4719b6f92ca 100644 --- a/articles/online-gaming-platform.md +++ b/articles/online-gaming-platform.md @@ -79,3 +79,4 @@ By adopting Fleet for server observability, they've successfully addressed scala + \ No newline at end of file diff --git a/articles/online-marketplace.md b/articles/online-marketplace.md index faa66afd818..5419b519c19 100644 --- a/articles/online-marketplace.md +++ b/articles/online-marketplace.md @@ -77,3 +77,5 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + + \ No newline at end of file diff --git a/articles/open-source-organization.md b/articles/open-source-organization.md index 2af438d97b0..f3baa494e49 100644 --- a/articles/open-source-organization.md +++ b/articles/open-source-organization.md @@ -33,4 +33,5 @@ Fleet is the open-source endpoint management platform that gives you total contr - \ No newline at end of file + + \ No newline at end of file diff --git a/articles/open-source-software-company.md b/articles/open-source-software-company.md new file mode 100644 index 00000000000..d81ab087f44 --- /dev/null +++ b/articles/open-source-software-company.md @@ -0,0 +1,75 @@ +# Open-source software company closes the Linux gap in device management with Fleet + +A global open-source software company manages Windows, macOS, and Linux at scale. Linux is central to its business, but existing tools didn’t support it with the same depth as macOS and Windows. + +Fleet helps the company adopt Linux as part of a unified endpoint strategy while supporting strict security and audit requirements. + +## At a glance + +* **Industry:** Open-source software + +* **Devices managed:** ~5,100 devices across Windows, macOS, and Linux + +* **Primary requirements:** Self-hosting, GitOps, visibility into their devices + +* **Previous challenge:** Weak Linux support in existing tools + +## The challenge + +Before Fleet, the company used tools such as Jamf and Intune for macOS and Windows. + +That left Linux as a major gap. For an organization built around Linux and open-source software, that disconnect created both technical and operational friction. + +The team needed a platform that could handle Linux seriously, support self-hosting, and align with compliance requirements such as Common Criteria certification and security audits. + +## The evaluation criteria + +The team focused on three requirements: + +1. **Self-hosted deployment** + Maintain control for security and compliance. + +2. **GitOps workflows** + Manage policy and software through code. + +3. **osquery integration** + Collect detailed data for compliance and security monitoring. + +## The solution + +Fleet gave the team a way to manage Windows, macOS, and Linux through one platform. + +The company uses GitOps workflows to automate software management and keep endpoint configuration aligned with security policy. Migration moved quickly, starting with user acceptance testing and progressing to broad rollout within weeks. + +Fleet’s open-source model was a strong fit for the company’s culture and technical standards. + +## The results + +Fleet helped the team close Linux gaps and simplify management across operating systems. + +* **Fast rollout:** From testing to broad rollout in just a matter of weeks + +* **Better Linux coverage:** Linux devices now align with macOS and Windows management. + +* **Simpler operations:** One platform reduces administrative complexity for a global workforce. + +## Why they recommend Fleet + +Fleet lets the team manage Linux, macOS, and Windows in one place, while staying aligned with its open-source and compliance requirements. + +## About Fleet + +Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs. + +By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives. + +Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction. + + + + + + + + + \ No newline at end of file diff --git a/articles/open-source-technology-company.md b/articles/open-source-technology-company.md index bd5fd937be5..423b7131a43 100644 --- a/articles/open-source-technology-company.md +++ b/articles/open-source-technology-company.md @@ -72,3 +72,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/robotics-company.md b/articles/robotics-company.md index 969b5f0aab9..2babd32977b 100644 --- a/articles/robotics-company.md +++ b/articles/robotics-company.md @@ -38,3 +38,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/setup-experience.md b/articles/setup-experience.md index 5de2b3a2081..9447fac83ae 100644 --- a/articles/setup-experience.md +++ b/articles/setup-experience.md @@ -197,6 +197,8 @@ The Fleet setup experience for macOS will exit if any of the following occurs: * All setup steps complete, including failed installs or script runs, with the "Cancel setup if software install fails" option _not_ enabled (see ["Blocking setup on failed software installs"](https://fleetdm.com/guides/macos-setup-experience#install-software)). * The user presses Command (⌘) + Shift + X at any time during the setup process. +> If the end user is stuck, you can send the [DeviceConfigured](https://developer.apple.com/documentation/devicemanagement/device-configured-command) using Fleet's [Run MDM command](https://fleetdm.com/docs/rest-api/rest-api#run-mdm-command) API to let the user through. + ## Setup Assistant When an end user unboxes their new Apple device, or starts up a freshly wiped device, they're presented with the Setup Assistant. Here they see panes that allow them to configure accessibility, appearance, and more. diff --git a/articles/technology-platform.md b/articles/technology-platform.md index f63020d506d..f7b8b5c3afc 100644 --- a/articles/technology-platform.md +++ b/articles/technology-platform.md @@ -88,3 +88,4 @@ Fleet offers total deployment flexibility: on-premises, air-gapped, container-na + \ No newline at end of file diff --git a/articles/workspace-software-company.md b/articles/workspace-software-company.md index ef9fdeb3dae..892b5c9d7df 100644 --- a/articles/workspace-software-company.md +++ b/articles/workspace-software-company.md @@ -35,4 +35,5 @@ Fleet is the open-source endpoint management platform that gives you total contr - \ No newline at end of file + + diff --git a/articles/worldwide-security-and-authentication-platform.md b/articles/worldwide-security-and-authentication-platform.md index 46bfa3ddd46..bd157e8edbd 100644 --- a/articles/worldwide-security-and-authentication-platform.md +++ b/articles/worldwide-security-and-authentication-platform.md @@ -91,3 +91,4 @@ To learn more about how Fleet can support your organization, visit [fleetdm.com/ + diff --git a/changes/34433-speedup-macos-profile-delivery b/changes/34433-speedup-macos-profile-delivery new file mode 100644 index 00000000000..b322b8ed9d6 --- /dev/null +++ b/changes/34433-speedup-macos-profile-delivery @@ -0,0 +1 @@ +- Moved Apple MDM worker to a faster cron, and started sending profiles on Post DEP enrollment job, to speed up initial macOS setup. \ No newline at end of file diff --git a/changes/37323-jetbrains-cve b/changes/37323-jetbrains-cve new file mode 100644 index 00000000000..d2449e10b9e --- /dev/null +++ b/changes/37323-jetbrains-cve @@ -0,0 +1 @@ +* Updated ingestion/CVE logic to support JetBrains software with 2 version numbers, like WebStorm 2025.1 diff --git a/changes/40910-correct-request-certificate-pem b/changes/40910-correct-request-certificate-pem new file mode 100644 index 00000000000..73c3e8965da --- /dev/null +++ b/changes/40910-correct-request-certificate-pem @@ -0,0 +1 @@ +* Updated the Request Certificate API to return the proper PEM header for PKCS #7 certificates returned by EST CAs diff --git a/changes/41324-support-labels-include-all-for-installers b/changes/41324-support-labels-include-all-for-installers new file mode 100644 index 00000000000..9d8b7b50d5d --- /dev/null +++ b/changes/41324-support-labels-include-all-for-installers @@ -0,0 +1 @@ +- Added support for `labels_include_all` conditional scoping for software installers and apps. diff --git a/changes/update-go-1.26.1 b/changes/update-go-1.26.1 new file mode 100644 index 00000000000..b87f97540c8 --- /dev/null +++ b/changes/update-go-1.26.1 @@ -0,0 +1 @@ +* Updated go to 1.26.1 diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index 223d4cc1fdd..6e929540f7d 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -14,6 +14,10 @@ dependencies: condition: mysql.enabled version: 9.12.5 repository: https://charts.bitnami.com/bitnami + - name: postgresql + condition: postgresql.enabled + version: 15.5.38 + repository: https://charts.bitnami.com/bitnami - name: redis condition: redis.enabled version: 18.1.6 diff --git a/charts/fleet/templates/deployment.yaml b/charts/fleet/templates/deployment.yaml index edf681fcb71..23e28dfbf80 100644 --- a/charts/fleet/templates/deployment.yaml +++ b/charts/fleet/templates/deployment.yaml @@ -188,7 +188,11 @@ spec: name: {{ .Values.fleet.license.secretName }} {{- end }} ## END FLEET SECTION - ## BEGIN MYSQL SECTION + ## BEGIN DATABASE SECTION + {{- if .Values.database.driver }} + - name: FLEET_MYSQL_DRIVER + value: "{{ .Values.database.driver }}" + {{- end }} - name: FLEET_MYSQL_ADDRESS value: "{{ .Values.database.address }}" - name: FLEET_MYSQL_DATABASE @@ -224,7 +228,7 @@ spec: - name: FLEET_MYSQL_TLS_SERVER_NAME value: "{{ .Values.database.tls.serverName }}" {{- end }} - ## END MYSQL SECTION + ## END DATABASE SECTION ## BEGIN MYSQL READ REPLICA SECTION {{- if .Values.database_read_replica }} {{- if .Values.database_read_replica.address }} diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 4a4b98c9bbb..ace4e676c71 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -221,9 +221,13 @@ osquery: resultTopic: "" ## Section: database -# All of the connection settings for MySQL +# All of the connection settings for the primary database. +# Supports MySQL (default) and PostgreSQL (experimental). database: - # Name of the Secret resource containing MySQL password and TLS secrets + # Driver selects the database backend: "mysql" (default) or "postgres". + # PostgreSQL support is experimental for self-hosted deployments. + driver: mysql + # Name of the Secret resource containing database password and TLS secrets secretName: mysql address: 127.0.0.1:3306 database: fleet @@ -323,5 +327,11 @@ environments: mysql: enabled: false +# PostgreSQL subchart (experimental). Enable when database.driver is "postgres" +# and you want Helm to provision a PG instance. For production, use a managed +# PostgreSQL service (e.g., AWS RDS, GCP Cloud SQL) and set database.address. +postgresql: + enabled: false + redis: enabled: false diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index edc4c7a0dcd..9c94ed87912 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -732,8 +732,6 @@ func newWorkerIntegrationsSchedule( logger *slog.Logger, depStorage *mysql.NanoDEPStorage, commander *apple_mdm.MDMAppleCommander, - bootstrapPackageStore fleet.MDMBootstrapPackageStore, - vppInstaller fleet.AppleMDMVPPInstaller, androidModule android.Service, ) (*schedule.Schedule, error) { const ( @@ -782,13 +780,7 @@ func newWorkerIntegrationsSchedule( DEPService: depSvc, DEPClient: depCli, } - appleMDM := &worker.AppleMDM{ - Datastore: ds, - Log: logger, - Commander: commander, - BootstrapPackageStore: bootstrapPackageStore, - VPPInstaller: vppInstaller, - } + vppVerify := &worker.AppleSoftware{ Datastore: ds, Log: logger, @@ -803,7 +795,7 @@ func newWorkerIntegrationsSchedule( Log: logger, AndroidModule: androidModule, } - w.Register(jira, zendesk, macosSetupAsst, appleMDM, dbMigrate, vppVerify, softwareWorker) + w.Register(jira, zendesk, macosSetupAsst, dbMigrate, vppVerify, softwareWorker) // Read app config a first time before starting, to clear up any failer client // configuration if we're not on a fleet-owned server. Technically, the ServerURL @@ -904,6 +896,53 @@ func newFailerClient(forcedFailures string) *worker.TestAutomationFailer { return failerClient } +func newAppleMDMWorkerSchedule( + ctx context.Context, + instanceID string, + ds fleet.Datastore, + logger *slog.Logger, + commander *apple_mdm.MDMAppleCommander, + bootstrapPackageStore fleet.MDMBootstrapPackageStore, + vppInstaller fleet.AppleMDMVPPInstaller, +) (*schedule.Schedule, error) { + const ( + name = string(fleet.CronAppleMDMWorker) + scheduleInterval = 10 * time.Second // schedule a worker to run every 10 seconds if none is running + maxRunTime = 10 * time.Minute // allow the worker to run for 10 minutes + ) + + logger = logger.With("cron", name) + + w := worker.NewWorker(ds, logger) + + appleMDM := &worker.AppleMDM{ + Datastore: ds, + Log: logger, + Commander: commander, + BootstrapPackageStore: bootstrapPackageStore, + VPPInstaller: vppInstaller, + } + + w.Register(appleMDM) + + s := schedule.New( + ctx, name, instanceID, scheduleInterval, ds, ds, + schedule.WithAltLockID("apple_mdm"), + schedule.WithLogger(logger), + schedule.WithJob("apple_mdm_worker", func(ctx context.Context) error { + workCtx, cancel := context.WithTimeout(ctx, maxRunTime) + defer cancel() + + if err := w.ProcessJobs(workCtx); err != nil { + return fmt.Errorf("processing apple mdm jobs: %w", err) + } + return nil + }), + ) + + return s, nil +} + func newCleanupsAndAggregationSchedule( ctx context.Context, instanceID string, diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 07f3c7cbc27..1ac08456e3a 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -30,6 +30,7 @@ import ( "github.com/fleetdm/fleet/v4/ee/server/service/est" "github.com/fleetdm/fleet/v4/ee/server/service/hostidentity" "github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig" + "github.com/fleetdm/fleet/v4/ee/server/service/scep" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/pkg/scripts" "github.com/fleetdm/fleet/v4/pkg/str" @@ -872,7 +873,7 @@ the way that the Fleet server works. } eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod) - scepConfigMgr := eeservice.NewSCEPConfigService(logger, nil) + scepConfigMgr := scep.NewSCEPConfigService(logger, nil) digiCertService := digicert.NewService(digicert.WithLogger(logger)) ctx = ctxerr.NewContext(ctx, eh) @@ -1176,12 +1177,19 @@ the way that the Fleet server works. if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) - vppInstaller := svc.(fleet.AppleMDMVPPInstaller) - return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, bootstrapPackageStore, vppInstaller, androidSvc) + return newWorkerIntegrationsSchedule(ctx, instanceID, ds, logger, depStorage, commander, androidSvc) }); err != nil { initFatal(err, "failed to register worker integrations schedule") } + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + vppInstaller := svc.(fleet.AppleMDMVPPInstaller) + return newAppleMDMWorkerSchedule(ctx, instanceID, ds, logger, commander, bootstrapPackageStore, vppInstaller) + }); err != nil { + initFatal(err, "failed to register apple_mdm_worker schedule") + } + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { return newAppleMDMDEPProfileAssigner(ctx, instanceID, config.MDM.AppleDEPSyncPeriodicity, ds, depStorage, logger) }); err != nil { diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 179b20737cb..18865a4da3f 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -141,7 +141,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,"numHostsABMPending":888,"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,"mdmRecoveryLockPasswordEnabled":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,"fleetMaintainedAppsMacOS":["1password/darwin"],"fleetMaintainedAppsWindows":["google-chrome/windows"],"oktaConditionalAccessConfigured":false,"conditionalAccessBypassDisabled":false}`, requestBody) + assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numHostsABMPending":888,"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,"mdmRecoveryLockPasswordEnabled":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,"fleetMaintainedAppsMacOS":["1password/darwin"],"fleetMaintainedAppsWindows":["google-chrome/windows"],"conditionalAccessEnabled":false,"oktaConditionalAccessConfigured":false,"conditionalAccessBypassDisabled":false,"entraConditionalAccessConfigured":false}`, requestBody) } func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) { diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go index 6c66dbdcba9..4ffd2a79845 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops.go +++ b/cmd/fleetctl/fleetctl/generate_gitops.go @@ -1954,6 +1954,10 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint, labels = softwareTitle.SoftwarePackage.LabelsExcludeAny labelKey = "labels_exclude_any" } + if len(softwareTitle.SoftwarePackage.LabelsIncludeAll) > 0 { + labels = softwareTitle.SoftwarePackage.LabelsIncludeAll + labelKey = "labels_include_all" + } if _, exists := setupSoftwareBySoftwareTitle[softwareTitle.ID]; exists { softwareSpec["setup_experience"] = true } @@ -1968,6 +1972,10 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint, labels = softwareTitle.AppStoreApp.LabelsExcludeAny labelKey = "labels_exclude_any" } + if len(softwareTitle.AppStoreApp.LabelsIncludeAll) > 0 { + labels = softwareTitle.AppStoreApp.LabelsIncludeAll + labelKey = "labels_include_all" + } if _, exists := setupSoftwareByPlatformAndAppID[platformAndAppID]; exists { softwareSpec["setup_experience"] = true } diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go index ddc7d98b5fb..312c20699fd 100644 --- a/cmd/fleetctl/fleetctl/generate_gitops_test.go +++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go @@ -473,9 +473,15 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi return &fleet.SoftwareTitle{ ID: 6, AppStoreApp: &fleet.VPPAppStoreApp{ - VPPAppID: fleet.VPPAppID{AdamID: "com.example.setup-experience-software", Platform: fleet.AndroidPlatform}, - LabelsExcludeAny: []fleet.SoftwareScopeLabel{}, - SelfService: true, + VPPAppID: fleet.VPPAppID{AdamID: "com.example.setup-experience-software", Platform: fleet.AndroidPlatform}, + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label C", + }, { + LabelName: "Label D", + }, + }, + SelfService: true, }, IconUrl: ptr.String("/api/icon3.png"), }, nil @@ -488,6 +494,13 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi AppStoreApp: &fleet.VPPAppStoreApp{ VPPAppID: fleet.VPPAppID{AdamID: "com.example.ios-auto-update", Platform: fleet.IOSPlatform}, SelfService: false, + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label C", + }, { + LabelName: "Label D", + }, + }, }, IconUrl: ptr.String("/api/icon4.png"), SoftwareAutoUpdateConfig: fleet.SoftwareAutoUpdateConfig{ @@ -524,6 +537,14 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi ID: 9, Name: "My Windows FMA", SoftwarePackage: &fleet.SoftwareInstaller{ + LabelsIncludeAll: []fleet.SoftwareScopeLabel{ + { + LabelName: "Label A", + }, + { + LabelName: "Label B", + }, + }, InstallScript: "install", UninstallScript: "uninstall", SelfService: true, diff --git a/cmd/fleetctl/fleetctl/gitops.go b/cmd/fleetctl/fleetctl/gitops.go index 7308344e492..548cd9df6b7 100644 --- a/cmd/fleetctl/fleetctl/gitops.go +++ b/cmd/fleetctl/fleetctl/gitops.go @@ -801,10 +801,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(softwarePackage.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) + return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) } labels = softwarePackage.LabelsExcludeAny } + if len(softwarePackage.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("Software package '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", softwarePackage.URL) + } + labels = softwarePackage.LabelsIncludeAll + } updateLabelUsage(labels, softwarePackage.URL, "Software Package", result) } @@ -816,10 +822,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(vppApp.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) + return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) } labels = vppApp.LabelsExcludeAny } + if len(vppApp.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("App Store App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", vppApp.AppStoreID) + } + labels = vppApp.LabelsIncludeAll + } updateLabelUsage(labels, vppApp.AppStoreID, "App Store App", result) } @@ -830,10 +842,16 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) { } if len(maintainedApp.LabelsExcludeAny) > 0 { if len(labels) > 0 { - return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) + return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) } labels = maintainedApp.LabelsExcludeAny } + if len(maintainedApp.LabelsIncludeAll) > 0 { + if len(labels) > 0 { + return nil, fmt.Errorf("Fleet Maintained App '%s' has multiple label keys; please choose one of `labels_include_all`, `labels_include_any`, `labels_exclude_any`.", maintainedApp.Slug) + } + labels = maintainedApp.LabelsIncludeAll + } updateLabelUsage(labels, maintainedApp.Slug, "Fleet Maintained App", result) } diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml index 72376119d0c..6a2c1a1904e 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSoftware.yaml @@ -30,6 +30,9 @@ fleet_maintained_apps: path: ../lib/some-team/queries/my-fma-darwin-preinstallquery.yml self_service: true - slug: fma2/windows + labels_include_all: + - Label A + - Label B self_service: true app_store_apps: - app_store_id: com.example.team-software @@ -42,10 +45,16 @@ app_store_apps: self_service: true platform: darwin - app_store_id: com.example.setup-experience-software + labels_include_all: + - Label C + - Label D platform: android self_service: true setup_experience: true - app_store_id: com.example.ios-auto-update + labels_include_all: + - Label C + - Label D auto_update_enabled: true auto_update_window_start: "01:00" auto_update_window_end: "03:00" diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml index f501b334698..56c82b028ae 100644 --- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml +++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/fleets/team-a-thumbsup.yml @@ -121,6 +121,9 @@ software: - app_store_id: com.example.setup-experience-software icon: path: "../lib/team-a-👍/icons/my-setup-experience-app-android-icon.png" + labels_include_all: + - Label C + - Label D platform: android self_service: true setup_experience: true @@ -130,6 +133,9 @@ software: auto_update_window_start: "01:00" icon: path: "../lib/team-a-👍/icons/my-ios-auto-update-app-ios-icon.png" + labels_include_all: + - Label C + - Label D platform: ios fleet_maintained_apps: - categories: @@ -148,6 +154,9 @@ software: slug: fma1/darwin - icon: path: "../lib/team-a-👍/icons/my-windows-fma-windows-icon.png" + labels_include_all: + - Label A + - Label B self_service: true slug: fma2/windows packages: diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml new file mode 100644 index 00000000000..95a39e8ae26 --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/no_team_software_installer_valid_include_all.yml @@ -0,0 +1,19 @@ +name: No team +controls: +policies: +software: + packages: + - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb + install_script: + path: lib/install_ruby.sh + pre_install_query: + path: lib/query_ruby.yml + post_install_script: + path: lib/post_install_ruby.sh + uninstall_script: + path: lib/uninstall_ruby.sh + labels_include_all: + - a + - b + - url: ${SOFTWARE_INSTALLER_URL}/other.deb + self_service: true diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml new file mode 100644 index 00000000000..5ffe5af2346 --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/team_software_installer_valid_include_all.yml @@ -0,0 +1,28 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: +policies: +queries: +software: + packages: + - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb + install_script: + path: lib/install_ruby.sh + pre_install_query: + path: lib/query_ruby_apply.yml + post_install_script: + path: lib/post_install_ruby.sh + labels_include_all: + - a + - b + - url: ${SOFTWARE_INSTALLER_URL}/other.deb + self_service: true diff --git a/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml b/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml new file mode 100644 index 00000000000..5486e9db6f5 --- /dev/null +++ b/cmd/fleetctl/fleetctl/testdata/gitops/team_vpp_valid_app_labels_include_all.yml @@ -0,0 +1,20 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: +policies: +queries: +software: + app_store_apps: + - app_store_id: "1" + labels_include_all: + - "label 1" + - "label 2" diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go index db515c4b1da..deb40eb803e 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go @@ -329,6 +329,10 @@ team_settings: withLabelsExcludeAny = ` labels_exclude_any: - Label1 +` + withLabelsIncludeAll = ` + labels_include_all: + - Label1 ` ) @@ -392,6 +396,34 @@ team_settings: require.Len(t, meta.LabelsExcludeAny, 1) require.Equal(t, "Label1", meta.LabelsExcludeAny[0].LabelName) + // switch both to labels_include_all + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, withLabelsIncludeAll), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, withLabelsIncludeAll, teamName), 0o644) + require.NoError(t, err) + + // Apply configs + s.assertDryRunOutputWithDeprecation(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), "--dry-run"}), true) + s.assertRealRunOutputWithDeprecation(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name()}), true) + + // the installer is now scoped by labels_include_all for no team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + + // the installer is now scoped by labels_include_all for team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + // remove the label conditions err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, emptyLabelsIncludeAny), 0o644) require.NoError(t, err) @@ -411,6 +443,7 @@ team_settings: require.Equal(t, noTeamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) require.NoError(t, err) @@ -418,6 +451,7 @@ team_settings: require.Equal(t, teamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) } func (s *enterpriseIntegrationGitopsTestSuite) TestNoTeamWebhookSettingsDeprecated() { @@ -944,6 +978,7 @@ team_settings: require.True(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -1000,6 +1035,7 @@ team_settings: require.NoError(t, err) require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAll) require.Len(t, meta.LabelsIncludeAny, 1) require.Equal(t, lbl.ID, meta.LabelsIncludeAny[0].LabelID) require.Empty(t, meta.InstallScript) // install script should be ignored for ipa apps @@ -1039,6 +1075,7 @@ team_settings: require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go index 5471c3c5dbe..a63e6494fd9 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go @@ -22,8 +22,8 @@ import ( "github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils" "github.com/fleetdm/fleet/v4/cmd/fleetctl/integrationtest" ma "github.com/fleetdm/fleet/v4/ee/maintained-apps" - eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/ee/server/service/digicert" + "github.com/fleetdm/fleet/v4/ee/server/service/scep" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/filesystem" "github.com/fleetdm/fleet/v4/server/datastore/mysql" @@ -111,7 +111,7 @@ func (s *enterpriseIntegrationGitopsTestSuite) SetupSuite() { SCEPStorage: scepStorage, Pool: redisPool, APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589", - SCEPConfigService: eeservice.NewSCEPConfigService(slog.New(slog.NewTextHandler(os.Stdout, nil)), nil), + SCEPConfigService: scep.NewSCEPConfigService(slog.New(slog.NewTextHandler(os.Stdout, nil)), nil), DigiCertService: digicert.NewService(), SoftwareTitleIconStore: softwareTitleIconStore, } @@ -1072,6 +1072,10 @@ software: ` emptyLabelsIncludeAny = ` labels_include_any: +` + withLabelsIncludeAll = ` + labels_include_all: + - Label1 ` teamTemplate = ` controls: @@ -1152,6 +1156,34 @@ settings: require.Len(t, meta.LabelsExcludeAny, 1) require.Equal(t, "Label1", meta.LabelsExcludeAny[0].LabelName) + // switch both to labels_include_all + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, withLabelsIncludeAll), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, withLabelsIncludeAll, teamName), 0o644) + require.NoError(t, err) + + // Apply configs + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), "--dry-run"})) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, + []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name()})) + + // the installer is now scoped by labels_include_all for no team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + + // the installer is now scoped by labels_include_all for team + meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsExcludeAny) + require.Len(t, meta.LabelsIncludeAll, 1) + require.Equal(t, "Label1", meta.LabelsIncludeAll[0].LabelName) + // remove the label conditions err = os.WriteFile(noTeamFilePath, []byte(fmt.Sprintf(noTeamTemplate, emptyLabelsIncludeAny)), 0o644) require.NoError(t, err) @@ -1171,6 +1203,7 @@ settings: require.Equal(t, noTeamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) meta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) require.NoError(t, err) @@ -1178,6 +1211,7 @@ settings: require.Equal(t, teamTitleID, *meta.TitleID) require.Len(t, meta.LabelsExcludeAny, 0) require.Len(t, meta.LabelsIncludeAny, 0) + require.Len(t, meta.LabelsIncludeAll, 0) } func (s *enterpriseIntegrationGitopsTestSuite) TestDeletingNoTeamYAML() { @@ -2347,6 +2381,7 @@ settings: require.True(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -2403,6 +2438,7 @@ settings: require.NoError(t, err) require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) + require.Empty(t, meta.LabelsIncludeAll) require.Len(t, meta.LabelsIncludeAny, 1) require.Equal(t, lbl.ID, meta.LabelsIncludeAny[0].LabelID) require.Empty(t, meta.InstallScript) // install script should be ignored for ipa apps @@ -2442,6 +2478,7 @@ settings: require.False(t, meta.SelfService) require.Empty(t, meta.LabelsExcludeAny) require.Empty(t, meta.LabelsIncludeAny) + require.Empty(t, meta.LabelsIncludeAll) } require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources) require.ElementsMatch(t, []string{"ios", "ipados"}, platforms) @@ -4156,3 +4193,204 @@ name: %s require.NoError(t, err) require.Len(t, titles, 0) } + +// TestFMALabelsIncludeAll tests that labels_include_all is correctly applied and +// cleared for Fleet Maintained Apps via gitops, for both no-team and a specific team. +func (s *enterpriseIntegrationGitopsTestSuite) TestFMALabelsIncludeAll() { + t := s.T() + ctx := context.Background() + + user := s.createGitOpsUser(t) + fleetctlConfig := s.createFleetctlConfig(t, user) + + lbl, err := s.DS.NewLabel(ctx, &fleet.Label{Name: "Label1" + t.Name(), Query: "SELECT 1"}) + require.NoError(t, err) + require.NotZero(t, lbl.ID) + + slug := fmt.Sprintf("foo%s/darwin", t.Name()) + + const ( + globalTemplate = ` +agent_options: +controls: +org_settings: + server_settings: + server_url: $FLEET_URL + org_info: + org_name: Fleet + secrets: +policies: +reports: +` + noTeamTemplate = `name: No team +controls: +policies: +software: + fleet_maintained_apps: + - slug: %s +%s +` + teamTemplate = ` +controls: +software: + fleet_maintained_apps: + - slug: %s +%s +reports: +policies: +agent_options: +name: %s +settings: + secrets: [{"secret":"enroll_secret"}] +` + ) + const noLabels = "" + + withLabelsIncludeAll := fmt.Sprintf(` + labels_include_all: + - %s +`, lbl.Name) + + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFile.WriteString(globalTemplate) + require.NoError(t, err) + err = globalFile.Close() + require.NoError(t, err) + + noTeamFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = fmt.Fprintf(noTeamFile, noTeamTemplate, slug, withLabelsIncludeAll) + require.NoError(t, err) + err = noTeamFile.Close() + require.NoError(t, err) + noTeamFilePath := filepath.Join(filepath.Dir(noTeamFile.Name()), "no-team.yml") + err = os.Rename(noTeamFile.Name(), noTeamFilePath) + require.NoError(t, err) + + teamName := uuid.NewString() + teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = fmt.Fprintf(teamFile, teamTemplate, slug, withLabelsIncludeAll, teamName) + require.NoError(t, err) + err = teamFile.Close() + require.NoError(t, err) + + // Set the required environment variables + t.Setenv("FLEET_URL", s.Server.URL) + testing_utils.StartSoftwareInstallerServer(t) + + // Mock server to serve FMA installer bytes + installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("foo")) + })) + defer installerServer.Close() + + // Mock server to serve the FMA manifest + manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + versions := []*ma.FMAManifestApp{ + { + Version: "1.0", + Queries: ma.FMAQueries{ + Exists: "SELECT 1 FROM osquery_info;", + }, + InstallerURL: installerServer.URL + "/foo.pkg", + InstallScriptRef: "fooscript", + UninstallScriptRef: "fooscript", + SHA256: "no_check", + }, + } + manifest := ma.FMAManifestFile{ + Versions: versions, + Refs: map[string]string{"fooscript": "echo hello"}, + } + err := json.NewEncoder(w).Encode(manifest) + require.NoError(t, err) + })) + defer manifestServer.Close() + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t) + + // Insert the FMA record so gitops can resolve the slug + mysql.ExecAdhocSQL(t, s.DS, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, + `INSERT INTO fleet_maintained_apps (name, slug, platform, unique_identifier) + VALUES (?, ?, 'darwin', ?)`, "foo"+t.Name(), slug, `com.example.foo`+t.Name()) + return err + }) + + // Apply configs — dry-run first, then real run + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + "--dry-run", + })) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + })) + + // Retrieve the team so we have its ID + team, err := s.DS.TeamByName(ctx, teamName) + require.NoError(t, err) + + // Locate the FMA installer for no-team and assert labels_include_all is set + noTeamTitles, _, _, err := s.DS.ListSoftwareTitles(ctx, + fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: ptr.Uint(0)}, + fleet.TeamFilter{User: test.UserAdmin}) + require.NoError(t, err) + require.Len(t, noTeamTitles, 1) + noTeamTitleID := noTeamTitles[0].ID + + noTeamMeta, err := s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, noTeamMeta.LabelsIncludeAny) + require.Empty(t, noTeamMeta.LabelsExcludeAny) + require.Len(t, noTeamMeta.LabelsIncludeAll, 1) + require.Equal(t, lbl.Name, noTeamMeta.LabelsIncludeAll[0].LabelName) + + // Locate the FMA installer for the team and assert labels_include_all is set + teamTitles, _, _, err := s.DS.ListSoftwareTitles(ctx, + fleet.SoftwareTitleListOptions{TeamID: &team.ID}, + fleet.TeamFilter{User: test.UserAdmin}) + require.NoError(t, err) + require.Len(t, teamTitles, 1) + teamTitleID := teamTitles[0].ID + + teamMeta, err := s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, teamMeta.LabelsIncludeAny) + require.Empty(t, teamMeta.LabelsExcludeAny) + require.Len(t, teamMeta.LabelsIncludeAll, 1) + require.Equal(t, lbl.Name, teamMeta.LabelsIncludeAll[0].LabelName) + + // Now re-apply without labels_include_all and confirm they are cleared + err = os.WriteFile(noTeamFilePath, fmt.Appendf(nil, noTeamTemplate, slug, noLabels), 0o644) + require.NoError(t, err) + err = os.WriteFile(teamFile.Name(), fmt.Appendf(nil, teamTemplate, slug, noLabels, teamName), 0o644) + require.NoError(t, err) + + s.assertDryRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + "--dry-run", + })) + s.assertRealRunOutput(t, fleetctl.RunAppForTest(t, []string{ + "gitops", "--config", fleetctlConfig.Name(), + "-f", globalFile.Name(), "-f", noTeamFilePath, "-f", teamFile.Name(), + })) + + // Labels should now be empty for no-team + noTeamMeta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, noTeamTitleID, false) + require.NoError(t, err) + require.Empty(t, noTeamMeta.LabelsIncludeAny) + require.Empty(t, noTeamMeta.LabelsExcludeAny) + require.Empty(t, noTeamMeta.LabelsIncludeAll) + + // Labels should now be empty for the team + teamMeta, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, teamTitleID, false) + require.NoError(t, err) + require.Empty(t, teamMeta.LabelsIncludeAny) + require.Empty(t, teamMeta.LabelsExcludeAny) + require.Empty(t, teamMeta.LabelsIncludeAll) +} diff --git a/cmd/fleetctl/integrationtest/gitops/software_test.go b/cmd/fleetctl/integrationtest/gitops/software_test.go index 00fd71c0ca8..87f53ac5681 100644 --- a/cmd/fleetctl/integrationtest/gitops/software_test.go +++ b/cmd/fleetctl/integrationtest/gitops/software_test.go @@ -54,10 +54,11 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) { }, { "testdata/gitops/team_software_installer_invalid_both_include_exclude.yml", - `only one of "labels_exclude_any" or "labels_include_any" can be specified`, + `only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified`, }, {"testdata/gitops/team_software_installer_valid_include.yml", ""}, {"testdata/gitops/team_software_installer_valid_exclude.yml", ""}, + {"testdata/gitops/team_software_installer_valid_include_all.yml", ""}, { "testdata/gitops/team_software_installer_invalid_unknown_label.yml", "Please create the missing labels, or update your settings to not refer to these labels.", @@ -360,10 +361,11 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { }, { "testdata/gitops/no_team_software_installer_invalid_both_include_exclude.yml", - `only one of "labels_exclude_any" or "labels_include_any" can be specified`, + `only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified`, }, {"testdata/gitops/no_team_software_installer_valid_include.yml", ""}, {"testdata/gitops/no_team_software_installer_valid_exclude.yml", ""}, + {"testdata/gitops/no_team_software_installer_valid_include_all.yml", ""}, { "testdata/gitops/no_team_software_installer_invalid_unknown_label.yml", "Please create the missing labels, or update your settings to not refer to these labels.", @@ -505,6 +507,10 @@ func TestGitOpsTeamVPPApps(t *testing.T) { "testdata/gitops/team_vpp_valid_app_labels_include_any.yml", "", time.Now().Add(24 * time.Hour), map[string]uint{"label 1": 1, "label 2": 2}, }, + { + "testdata/gitops/team_vpp_valid_app_labels_include_all.yml", "", time.Now().Add(24 * time.Hour), + map[string]uint{"label 1": 1, "label 2": 2}, + }, { "testdata/gitops/team_vpp_invalid_app_labels_exclude_any.yml", "Please create the missing labels, or update your settings to not refer to these labels.", time.Now().Add(24 * time.Hour), @@ -517,7 +523,7 @@ func TestGitOpsTeamVPPApps(t *testing.T) { }, { "testdata/gitops/team_vpp_invalid_app_labels_both.yml", - `only one of "labels_exclude_any" or "labels_include_any" can be specified for app store app`, time.Now().Add(24 * time.Hour), + `only one of "labels_include_all", "labels_exclude_any" or "labels_include_any" can be specified for app store app`, time.Now().Add(24 * time.Hour), map[string]uint{}, }, } diff --git a/deploy/ledo/values.yaml b/deploy/ledo/values.yaml new file mode 100644 index 00000000000..6ed5f4fb865 --- /dev/null +++ b/deploy/ledo/values.yaml @@ -0,0 +1,110 @@ +# Fleet Helm values for fleet.hx.ledoweb.com (Ledo personal cluster) +# Deploy: helm upgrade --install fleet charts/fleet -f deploy/ledo/values.yaml -n fleet --create-namespace + +hostName: fleet.hx.ledoweb.com +replicas: 1 +imageRepository: ghcr.io/ledoent/fleet +imageTag: latest # override with specific tag e.g. v4.82.0-ledo + +resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: "0.1" + memory: 128Mi + +# Disable anti-affinity on single-node clusters +affinity: {} + +ingress: + enabled: true + className: nginx # adjust if using traefik or another ingress controller + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-body-size: "100m" # needed for software installers + hosts: + - host: fleet.hx.ledoweb.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: fleet-hx-tls + hosts: + - fleet.hx.ledoweb.com + +fleet: + listenPort: 8080 + secretName: fleet + autoApplySQLMigrations: true + + # Let cert-manager/ingress handle TLS termination + tls: + enabled: false + + auth: + bcryptCost: 12 + saltKeySize: 24 + app: + tokenKeySize: 24 + inviteTokenValidityPeriod: 120h + session: + keySize: 64 + duration: 2160h # 90 days + + logging: + debug: false + json: true + disableBanner: false + + # MDM: Apple APNs cert loaded via secret (see below) + # After deploy, configure APNs via Fleet UI: Settings > Integrations > MDM + mdm: + windows: + wstepIdentityCertKey: "" + wstepIdentityKeyKey: "" + + # S3 for software installers — point at your MinIO or S3-compatible store + # Leave blank to disable software installers + softwareInstallers: + s3: + bucketName: "" + prefix: "" + accessKeyID: "" + secretKey: software-installers + region: "" + endpointURL: "" + forceS3PathStyle: false + + carving: + s3: + bucketName: "" + prefix: "" + accessKeyID: "" + secretKey: s3-bucket + region: "" + endpointURL: "" + forceS3PathStyle: false + +## Section: Database +database: + # Use an external MySQL — set connection details in the fleet Secret + # kubectl create secret generic fleet -n fleet \ + # --from-literal=FLEET_MYSQL_ADDRESS=mysql:3306 \ + # --from-literal=FLEET_MYSQL_DATABASE=fleet \ + # --from-literal=FLEET_MYSQL_USERNAME=fleet \ + # --from-literal=FLEET_MYSQL_PASSWORD= \ + # --from-literal=FLEET_REDIS_ADDRESS=redis:6379 \ + # --from-literal=FLEET_SERVER_PRIVATE_KEY=<32-byte-random-hex> + address: mysql:3306 + database: fleet + username: fleet + passwordPath: "" # loaded via secret env FLEET_MYSQL_PASSWORD + +## Section: Cache +cache: + address: redis:6379 + +## Section: Prometheus +prometheus: + enabled: false diff --git a/docker-compose.yml b/docker-compose.yml index 17e42eeaad3..86f28d7120b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,6 +81,38 @@ services: - /var/lib/mysql:rw,noexec,nosuid - /tmpfs + # PostgreSQL test instance for POSTGRES_TEST=1 integration tests. + # Usage: docker compose up -d postgres_test + # Then: POSTGRES_TEST=1 go test ./server/datastore/mysql/... + postgres_test: + image: postgres:16-alpine + environment: + POSTGRES_USER: fleet + POSTGRES_PASSWORD: insecure + POSTGRES_DB: fleet + ports: + - "${FLEET_POSTGRES_TEST_PORT:-5434}:5432" + tmpfs: + - /var/lib/postgresql/data:rw,noexec,nosuid + command: [ + "postgres", + "-c", "fsync=off", + "-c", "full_page_writes=off", + "-c", "synchronous_commit=off", + ] + + # PostgreSQL development instance. + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: fleet + POSTGRES_PASSWORD: insecure + POSTGRES_DB: fleet + ports: + - "5433:5432" + volumes: + - postgres-persistent-volume:/var/lib/postgresql/data + # Unauthenticated SMTP server. mailhog: image: mailhog/mailhog:latest @@ -170,4 +202,5 @@ services: volumes: mysql-persistent-volume: + postgres-persistent-volume: data-s3: diff --git a/docs/Contributing/getting-started/testing-and-local-development.md b/docs/Contributing/getting-started/testing-and-local-development.md index 512831e948c..debe254aed9 100644 --- a/docs/Contributing/getting-started/testing-and-local-development.md +++ b/docs/Contributing/getting-started/testing-and-local-development.md @@ -73,7 +73,7 @@ Check out [`/tools/osquery` directory instructions](https://github.com/fleetdm/f You must install the [`golangci-lint`](https://golangci-lint.run/) command to run `make test[-go]` or `make lint[-go]`, using: ```sh -go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@a4b55ebc3471c9fbb763fd56eefede8050f99887 +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@6008b81b81c690c046ffc3fd5bce896da715d5fd ``` This installs the version of `golangci-lint` used in our CI environment (currently 2.7.1). Make sure it is available in your `PATH`. To execute the basic unit and integration tests, run the following from the root of the repository: diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index b84d5bbaefe..728d1272c68 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -11,13 +11,13 @@ - [Commands](#commands) - [Integrations](#integrations-1) - [Policies](#policies) -- [Queries](#queries) +- [Reports](#reports) - [Schedule (deprecated)](#schedule) - [Scripts](#scripts) - [Sessions](#sessions) - [Software](#software) - [Targets](#targets) -- [Teams](#teams) +- [Fleets](#fleets) - [Translator](#translator) - [Users](#users) - [Custom variables](#custom-variables) @@ -102,7 +102,8 @@ Authenticates the user with the specified credentials. Use the token returned fr "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [] + "teams": [], + "fleets": [] }, "token": "{your token}" } @@ -326,7 +327,8 @@ Retrieves the user data for the authenticated user. "force_password_reset": false, "gravatar_url": "", "sso_enabled": false, - "teams": [] + "fleets": [], + "fleets": [] }, "available_teams" : [ { @@ -335,6 +337,13 @@ Retrieves the user data for the authenticated user. "description": "Employee workstations" } ], + "available_fleets" : [ + { + "id": 1, + "name": "Workstations", + "description": "Employee workstations" + } + ] } ``` @@ -375,7 +384,8 @@ Resets the password of the authenticated user. Requires that `force_password_res "gravatar_url": "", "sso_enabled": false, "global_role": "admin", - "teams": [] + "teams": [], + "fleets": [] } } ``` @@ -556,10 +566,13 @@ Returns a list of the activities that have been performed in Fleet. For a compre "actor_gravatar": "", "actor_email": "name@example.com", "type": "created_team", + "type": "created_fleet", "fleet_initiated": false, "details": { "team_id": 2, - "team_name": "Apples" + "fleet_id": 2, + "team_name": "Apples", + "fleet_name": "Apples" } }, { @@ -728,7 +741,7 @@ Object with the following structure: ### Add certificate template -Add a certificate template to deploy a certificate to all hosts on the team. Fleet currently supports adding certificates for Android that are issued from a custom [SCEP](https://en.wikipedia.org/wiki/Simple_Certificate_Enrollment_Protocol) certificate authority. +Add a certificate template to deploy a certificate to all hosts on the fleet. Fleet currently supports adding certificates for Android that are issued from a custom [SCEP](https://en.wikipedia.org/wiki/Simple_Certificate_Enrollment_Protocol) certificate authority. `POST /api/v1/fleet/certificates` @@ -737,7 +750,7 @@ Add a certificate template to deploy a certificate to all hosts on the team. Fle | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------- | | name | string | body | **Required.** The name of the certificate. Name can be used as certificate alias to reference in configuration profiles. | -| team_id | string | body | _Available in Fleet Premium_. The ID of the team to add profiles to. | +| fleet_id | string | body | _Available in Fleet Premium_. The ID of the fleet to add profiles to. | | certificate_authority_id | integer | body | **Required.** The certificate authority (CA) ID to issue certificate from. Currently, only custom SCEP CA is supported. To get ID use [List certificate authorities](#list-certificate-authorities-cas). | | subject_name | string | body |**Required** The certificate's subject name (SN). Separate subject fields by a "/". For example: "/CN=john@example.com/O=Acme Inc.". | @@ -751,6 +764,7 @@ Add a certificate template to deploy a certificate to all hosts on the team. Fle { "name": "wifi-certificate", "team_id": 1, + "fleet_id": 1, "certificate_authority_id": 1, "subject_name": "/CN=$FLEET_VAR_HOST_END_USER_IDP_USERNAME/OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL" } @@ -913,7 +927,7 @@ List certificate added to Fleet. Currently, they can only be added via GitOps. | Name | Type | In | Description | | ----------| ------- | ---- | -------------------------------------------------------------- | -| team | string | query | _Available in Fleet Premium_. The team ID to filter profiles. | +| fleet | string | query | _Available in Fleet Premium_. The fleet ID to filter profiles. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | @@ -1377,8 +1391,8 @@ Retrieves the specified carve block. This endpoint retrieves the data that was c - [Update configuration](#update-configuration) - [Get global enroll secrets](#get-global-enroll-secrets) - [Update global enroll secrets](#update-global-enroll-secrets) -- [Get team enroll secrets](#get-team-enroll-secrets) -- [Update team enroll secrets](#update-team-enroll-secrets) +- [Get fleet enroll secrets](#get-fleet-enroll-secrets) +- [Update fleet enroll secrets](#update-fleet-enroll-secrets) - [Get version](#get-version) The Fleet server exposes API endpoints that handle the configuration of Fleet as well as endpoints that manage enroll secret operations. These endpoints require prior authentication, you so you'll need to log in before calling any of the endpoints documented below. @@ -1716,7 +1730,7 @@ Modifies the Fleet's configuration with the supplied information. | sso_settings | object | body | See [sso_settings](#sso-settings). | | host_expiry_settings | object | body | See [host_expiry_settings](#host-expiry-settings). | | activity_expiry_settings | object | body | See [activity_expiry_settings](#activity-expiry-settings). | -| agent_options | objects | body | The agent_options spec that is applied to all hosts. In Fleet 4.0.0 the `api/v1/fleet/spec/osquery_options` endpoints were removed. | +| agent_options | objects | body | The agent_options spec that is applied to all hosts. | | fleet_desktop | object | body | See [fleet_desktop](#fleet-desktop). | | webhook_settings | object | body | See [webhook_settings](#webhook-settings). | | integrations | object | body | See [integrations](#integrations). | @@ -2410,8 +2424,8 @@ When updating conditional access config, all `conditional_access` fields must ei | windows_enabled_and_configured | boolean | Enables Windows MDM support. | | windows_entra_tenant_ids | array | _Available in Fleet Premium._ IDs of Microsoft Entra tenants to connect to Fleet, to enable automatic (Autopilot) and manual enrollment by end users (**Settings** > **Accounts** > **Access work or school** on Windows). Find your **Tenant ID**, on [**Microsoft Entra ID** > **Home**](https://entra.microsoft.com/#home). | | enable_turn_on_windows_mdm_manually | boolean | _Available in Fleet Premium._ Specifies whether or not to require end users to manually turn on MDM in **Settings > Access work or school**. If `false`, MDM is automatically turned on for all Windows hosts that aren't connected to any MDM solution. | -| enable_disk_encryption | boolean | _Available in Fleet Premium._ Hosts that belong to no team will have disk encryption enabled if set to true. | -| windows_require_bitlocker_pin | boolean | _Available in Fleet Premium._ End users on Windows hosts that belong to no team will be required to set a BitLocker PIN if set to true. `enable_disk_encryption` must be set to true. When the PIN is set, it's required to unlock Windows host during startup. | +| enable_disk_encryption | boolean | _Available in Fleet Premium._ Hosts that are "Unassigned" will have disk encryption enabled if set to true. | +| windows_require_bitlocker_pin | boolean | _Available in Fleet Premium._ End users on Windows hosts that are "Unassigned" will be required to set a BitLocker PIN if set to true. `enable_disk_encryption` must be set to true. When the PIN is set, it's required to unlock Windows host during startup. | | macos_updates | object | See [`mdm.macos_updates`](#mdm-macos-updates). | | ios_updates | object | See [`mdm.ios_updates`](#mdm-ios-updates). | | ipados_updates | object | See [`mdm.ipados_updates`](#mdm-ipados-updates). | @@ -2434,8 +2448,8 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to no team and are enrolled into Fleet's MDM will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to no team and are enrolled into Fleet's MDM will be forced to update their OS after this deadline (noon local time for hosts already on macOS 14 or above, 20:00 UTC for hosts on earlier macOS versions). | +| minimum_version | string | Hosts that are "Unassigned" and have MDM turned on will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that are "Unassigned" and have MDM turned on will be forced to update their OS after this deadline (7PM local time for hosts already on macOS 14 or above, 20:00 UTC for hosts on earlier macOS versions). | | update_new_hosts | string | macOS hosts that automatically enroll (ADE) are updated to [Apple's latest version](https://fleetdm.com/guides/enforce-os-updates) during macOS Setup Assistant. For backwards compatibility, if not specified, and `deadline` and `minimum_version` are set, `update_new_hosts` is set to `true`. Otherwise, `update_new_hosts` defaults to `false`. |
@@ -2448,8 +2462,8 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to no team will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to no team will be forced to update their OS after this deadline (noon local time). | +| minimum_version | string | Hosts that are "Unassigned" will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that are "Unassigned" will be forced to update their OS after this deadline (7PM local time). |
@@ -2461,8 +2475,8 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to no team will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to no team will be forced to update their OS after this deadline (noon local time). | +| minimum_version | string | Hosts that are "Unassigned" will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that are "Unassigned" will be forced to update their OS after this deadline (7PM local time). |
@@ -2474,8 +2488,8 @@ _Available in Fleet Premium._ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| deadline_days | integer | Hosts that belong to no team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. | -| grace_period_days | integer | Hosts that belong to no team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. | +| deadline_days | integer | Hosts that are "Unassigned" and have MDM turned on will have this number of days before updates are installed on Windows. | +| grace_period_days | integer | Hosts that are "Unassigned" and have MDM turned on will have this number of days before Windows restarts to install updates. |
@@ -2722,11 +2736,11 @@ Delete all global enroll secrets. {} ``` -### Get team enroll secrets +### Get fleet enroll secrets -Returns the valid team enroll secrets. +Returns the fleet's enroll secrets. -`GET /api/v1/fleet/teams/:id/secrets` +`GET /api/v1/fleet/fleets/:id/secrets` #### Parameters @@ -2734,7 +2748,7 @@ None. #### Example -`GET /api/v1/fleet/teams/1/secrets` +`GET /api/v1/fleet/fleets/1/secrets` ##### Default response @@ -2746,31 +2760,32 @@ None. { "created_at": "2021-06-16T22:05:49Z", "secret": "aFtH2Nq09hrvi73ErlWNQfa7M53D3rPR", - "team_id": 1 + "team_id": 1, + "fleet_id": 1, } ] } ``` -### Update team enroll secrets +### Update fleet enroll secrets -Replaces all existing team enroll secrets. +Replaces all existing enroll secrets for a fleet. -`PATCH /api/v1/fleet/teams/:id/secrets` +`PATCH /api/v1/fleet/fleets/:id/secrets` #### Parameters | Name | Type | In | Description | | --------- | ------- | ---- | -------------------------------------- | -| id | integer | path | **Required**. The team's id. | +| id | integer | path | **Required**. The fleet's id. | | secrets | array | body | **Required**. A list of enroll secrets | #### Example -Replace all of a team's existing enroll secrets with a new enroll secret +Replace all of a fleet's existing enroll secrets with a new enroll secret -`PATCH /api/v1/fleet/teams/2/secrets` +`PATCH /api/v1/fleet/fleets/2/secrets` ##### Request body @@ -2801,9 +2816,9 @@ Replace all of a team's existing enroll secrets with a new enroll secret #### Example -Delete all of a team's existing enroll secrets +Delete all of a fleet's existing enroll secrets -`PATCH /api/v1/fleet/teams/2/secrets` +`PATCH /api/v1/fleet/fleets/2/secrets` ##### Request body @@ -2865,8 +2880,8 @@ None. - [Delete host](#delete-host) - [Refetch host](#refetch-host) - [Refetch host by Fleet Desktop token](#refetch-host-by-fleet-desktop-token) -- [Update hosts' team](#update-hosts-team) -- [Update hosts' team by filter](#update-hosts-team-by-filter) +- [Update hosts' fleet](#update-hosts-fleet) +- [Update hosts' fleet by filter](#update-hosts-fleet-by-filter) - [Turn off host's MDM](#turn-off-hosts-mdm) - [Batch-delete hosts](#batch-delete-hosts) - [Update human-device mapping](#update-human-device-mapping) @@ -2908,10 +2923,10 @@ the `software` table. - `created_at`: the time the row in the database was created, which usually corresponds to the first enrollment of the host. - `updated_at`: the last time the row in the database for the `hosts` table was updated. -- `detail_updated_at`: the last time Fleet updated host data, based on the results from the detail queries (this includes updates to host associated tables, e.g. `host_users`). -- `label_updated_at`: the last time Fleet updated the label membership for the host based on the results from the queries ran. +- `detail_updated_at`: the last time Fleet updated host data (this includes updates to host associated tables, e.g. `host_users`). +- `label_updated_at`: the last time Fleet updated the label membership for the host - `last_enrolled_at`: the last time the host enrolled to Fleet. -- `policy_updated_at`: the last time we updated the policy results for the host based on the queries ran. +- `policy_updated_at`: the last time we updated the policy results for the host - `seen_time`: the last time the host contacted the fleet server, regardless of what operation it was for. - `software_updated_at`: the last time software changed for the host in any way. - `last_restarted_at`: the last time that the host was restarted. @@ -2932,7 +2947,7 @@ the `software` table. | status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). | | additional_info_filters | string | query | A comma-delimited list of fields to include in each host's `additional` object. This query is populated by the `additional_queries` in the `features` section of the configuration YAML. | -| team_id | integer | query | _Available in Fleet Premium_. Filters to only include hosts in the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters to only include hosts in the specified fleet. Use `0` to filter by "Unassigned" hosts. | | policy_id | integer | query | The ID of the policy to filter hosts by. | | policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. | | software_version_id | integer | query | The ID of the software version to filter hosts by. | @@ -2947,14 +2962,14 @@ the `software` table. | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. 'pending' only includes Apple (macOS, iOS, iPadOS) hosts in Apple Business Manager (ABM) that are not yet enrolled to Fleet. | | connected_to_fleet | boolean | query | Filter hosts that are talking to this Fleet server for MDM features. In rare cases, hosts can be enrolled to one Fleet server but talk to a different Fleet server for MDM features. In this case, the value would be `false`. Always `false` for Linux hosts. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to 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.** | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | disable_failing_policies| boolean | query | If `true`, hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | | macos_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | | 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 disk encryption status. 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.** | +| 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 fleet ID filter, the results include only "Unassigned" hosts.** | +| os_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | | 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, including Fleet-maintained policies. | | populate_users | boolean | query | If `true`, the response will include user data for each host. | @@ -3048,7 +3063,9 @@ To filter hosts by platform (macOS, Windows, Linux), use the ["List label's host "status": "offline", "display_text": "Annas-MacBook-Pro.local", "team_id": null, + "fleet_id": null, "team_name": null, + "fleet_name": null, "gigs_disk_space_available": 174.98, "percent_disk_space_available": 71, "gigs_total_disk_space": 246, @@ -3236,7 +3253,7 @@ Response payload with the `munki_issue_id` filter provided: | after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. | | status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). | -| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified fleet. | | policy_id | integer | query | The ID of the policy to filter hosts by. | | policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. | | software_version_id | integer | query | The ID of the software version to filter hosts by. | @@ -3245,17 +3262,17 @@ Response payload with the `munki_issue_id` filter provided: | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | | os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` | | vulnerability | string | query | The cve to filter hosts by (including "cve-" prefix, case-insensitive). | -| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `after`, `status`, `query` and `team_id`. | +| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `after`, `status`, `query` and `fleet_id`. | | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. 'pending' only includes Apple (macOS, iOS, iPadOS) hosts in Apple Business Manager (ABM) that are not yet enrolled to Fleet. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to 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.** | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | macos_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | -| 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'. **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 | 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 disk encryption status. 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.** | +| 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'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | +| 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 fleet ID filter, the results include only "Unassigned" hosts.** | +| os_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | If `additional_info_filters` is not specified, no `additional` information will be returned. @@ -3295,13 +3312,13 @@ Returns the count of all hosts organized by status. `online_count` includes all | Name | Type | In | Description | | --------------- | ------- | ---- | ------------------------------------------------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team whose host counts should be included. Defaults to all teams. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet whose host counts should be included. Defaults to all fleets. | | platform | string | query | Platform to filter by when counting. Defaults to all platforms. | | low_disk_space | integer | query | _Available in Fleet Premium_. Returns the count of hosts with less GB of disk space available than this value. Must be a number between 1-100. | #### Example -`GET /api/v1/fleet/host_summary?team_id=1&low_disk_space=32` +`GET /api/v1/fleet/host_summary?fleet_id=1&low_disk_space=32` ##### Default response @@ -3310,6 +3327,7 @@ Returns the count of all hosts organized by status. `online_count` includes all ```json { "team_id": 1, + "fleet_id": 1, "totals_hosts_count": 2408, "online_count": 2267, "offline_count": 141, @@ -3493,8 +3511,10 @@ Returns the information of the specified host. "config_tls_refresh": 10, "logger_tls_period": 10, "team_id": null, + "fleet_id": null, "pack_stats": null, "team_name": null, + "fleet_name": null, "gigs_disk_space_available": 174.98, "percent_disk_space_available": 71, "gigs_total_disk_space": 246, @@ -3783,7 +3803,9 @@ If `hostname` is specified when there is more than one host with the same hostna "config_tls_refresh": 60, "logger_tls_period": 10, "team_id": 2, + "fleet_id": 2, "team_name": null, + "fleet_name": null, "gigs_disk_space_available": 19.29, "percent_disk_space_available": 74, "gigs_total_disk_space": 192, @@ -3858,7 +3880,9 @@ If `hostname` is specified when there is more than one host with the same hostna "hosts": null, "host_ids": null, "teams": null, - "team_ids": null + "fleet": null, + "team_ids": null, + "fleet_ids": null } ], "policies": [ @@ -3871,6 +3895,7 @@ If `hostname` is specified when there is more than one host with the same hostna "author_name": "", "author_email": "", "team_id": null, + "fleet_id": null, "resolution": "To enable full disk encryption, on the failing device, select System Preferences > Security & Privacy > FileVault > Turn On FileVault.", "platform": "darwin,linux", "created_at": "2022-09-02T18:52:19Z", @@ -4011,8 +4036,10 @@ X-Client-Cert-Serial: "config_tls_refresh": 10, "logger_tls_period": 10, "team_id": null, + "fleet_id": null, "pack_stats": null, "team_name": null, + "fleet_name": null, "additional": {}, "gigs_disk_space_available": 174.98, "percent_disk_space_available": 71, @@ -4166,7 +4193,7 @@ Deletes the specified host from Fleet. Note that a deleted host will fail authen ### Refetch host -Flags the host details, labels and policies to be refetched the next time the host checks in for distributed queries. Note that we cannot be certain when the host will actually check in and update the query results. Further requests to the host APIs will indicate that the refetch has been requested through the `refetch_requested` field on the host object. +Flags the host details, labels and policies to be refetched the next time the host checks in. Note that we cannot be certain when the host will actually check in. Further requests to the host APIs will indicate that the refetch has been requested through the `refetch_requested` field on the host object. `POST /api/v1/fleet/hosts/:id/refetch` @@ -4204,7 +4231,7 @@ Same as [Refetch host](#refetch-host) except with the Fleet Desktop token instea `Status: 200` -### Update hosts' team +### Update hosts' fleet _Available in Fleet Premium_ @@ -4214,7 +4241,7 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------- | ------- | ---- | ----------------------------------------------------------------------- | -| team_id | integer | body | **Required**. The ID of the team you'd like to transfer the host(s) to. | +| fleet_id | integer | body | **Required**. The ID of the fleet you'd like to assign the host(s) to. | | hosts | array | body | **Required**. A list of host IDs. | #### Example @@ -4226,6 +4253,7 @@ _Available in Fleet Premium_ ```json { "team_id": 1, + "fleet_id": 1, "hosts": [3, 2, 4, 6, 1, 5, 7] } ``` @@ -4235,7 +4263,7 @@ _Available in Fleet Premium_ `Status: 200` -### Update hosts' team by filter +### Update hosts' fleet by filter _Available in Fleet Premium_ @@ -4245,7 +4273,7 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| team_id | integer | body | **Required**. The ID of the team you'd like to transfer the host(s) to. | +| fleet_id | integer | body | **Required**. The ID of the fleet you'd like to assign the host(s) to. | | filters | object | body | **Required**. See [filters](#filters) | @@ -4256,7 +4284,7 @@ _Available in Fleet Premium_ | query | string | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. | | status | string | Host status. Can either be `new`, `online`, `offline`, `mia` or `missing`. | | label_id | number | ID of a label to filter by. | -| team_id | number | ID of the team to filter by. | +| fleet_id | number | ID of the fleet to filter by. | > Note: `label_id` and `status` filters cannot be used at the same time. @@ -4270,9 +4298,11 @@ _Available in Fleet Premium_ ```json { "team_id": 1, + "fleet_id": 1, "filters": { "status": "online", "team_id": 2, + "fleet_id": 2, } } ``` @@ -4324,7 +4354,7 @@ Delete hosts selected by filter or ids. | query | string | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. | | status | string | Host status. Can either be `new`, `online`, `offline`, `mia` or `missing`. | | label_id | number | ID of a label to filter by. | -| team_id | number | ID of the team to filter by. | +| fleet_id | number | ID of the fleet to filter by. | > Notes: `label_id` and `status` filters cannot be used at the same time. @@ -4344,6 +4374,7 @@ Request (using `filters`): "status": "online", "label_id": 1, "team_id": 1, + "fleet_id": 1, "query": "abc" } } @@ -4366,7 +4397,8 @@ Request (`filters` is specified and empty, to delete all hosts): { "filters": { "status": "online", - "team_id": 1 + "team_id": 1, + "fleet_id": 1 } } ``` @@ -4528,14 +4560,14 @@ Retrieves MDM enrollment summary. Windows servers are excluded from the aggregat | Name | Type | In | Description | | -------- | ------- | ----- | -------------------------------------------------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. Filter by team | +| fleet_id | integer | query | _Available in Fleet Premium_. Filter by fleet. | | platform | string | query | Filter by platform ("windows" or "darwin") | -A `team_id` of `0` returns the statistics for hosts that are not part of any team. A `null` or missing `team_id` returns statistics for all hosts regardless of the team. +A `fleet_id` of `0` returns the statistics for hosts that are "Unassigned". A `null` or missing `fleet_id` returns statistics for all hosts on all fleets. #### Example -`GET /api/v1/fleet/hosts/summary/mdm?team_id=1&platform=windows` +`GET /api/v1/fleet/hosts/summary/mdm?fleet_id=1&platform=windows` ##### Default response @@ -4634,9 +4666,9 @@ Retrieves MDM enrollment status and Munki versions, aggregated across all hosts. | Name | Type | In | Description | | ------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. Filters the aggregate host information to only include hosts in the specified team. | | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the aggregate host information to only include hosts in the specified fleet. | | -A `team_id` of `0` returns the statistics for hosts that are not part of any team. A `null` or missing `team_id` returns statistics for all hosts regardless of the team. +A `fleet_id` of `0` returns the statistics for hosts that are "Unassigned". A `null` or missing `fleet_id` returns statistics for all hosts on all fleets. #### Example @@ -4808,7 +4840,7 @@ Currently, `hash_sha256`, `executable_sha256`, and `executable_path` are only su { "id": 147, "name": "Logic Pro", - "icon_url": "/api/latest/fleet/software/titles/147/icon?team_id=2", + "icon_url": "/api/latest/fleet/software/titles/147/icon?fleet_id=2", "software_package": null, "app_store_app": { "app_store_id": "1091189122", @@ -4905,7 +4937,7 @@ requested by a web browser. | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | | status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | -| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified fleet. | | policy_id | integer | query | The ID of the policy to filter hosts by. | | policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** | | software_version_id | integer | query | The ID of the software version to filter hosts by. | @@ -4917,11 +4949,11 @@ requested by a web browser. | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. 'pending' only includes Apple (macOS, iOS, iPadOS) hosts in Apple Business Manager (ABM) that are not yet enrolled to Fleet. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to 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.** | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only hosts that are "Unassigned".** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | -| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `team_id`. | -| 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'. **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.** | +| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `fleet_id`. | +| 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'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only hosts that are "Unassigned".** | | disable_failing_policies | boolean | query | If `true`, hosts will return failing policies as 0 (returned as the `issues` column) regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. @@ -4935,7 +4967,7 @@ If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Se `Status: 200` ```csv -created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,last_enrolled_at,seen_time,refetch_requested,hostname,uuid,platform,osquery_version,os_version,build,platform_like,code_name,uptime,memory,cpu_type,cpu_subtype,cpu_brand,cpu_physical_cores,cpu_logical_cores,hardware_vendor,hardware_model,hardware_version,hardware_serial,computer_name,primary_ip_id,primary_ip,primary_mac,distributed_interval,config_tls_refresh,logger_tls_period,team_id,team_name,gigs_disk_space_available,percent_disk_space_available,gigs_total_disk_space,issues,device_mapping,status,display_text +created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,last_enrolled_at,seen_time,refetch_requested,hostname,uuid,platform,osquery_version,os_version,build,platform_like,code_name,uptime,memory,cpu_type,cpu_subtype,cpu_brand,cpu_physical_cores,cpu_logical_cores,hardware_vendor,hardware_model,hardware_version,hardware_serial,computer_name,primary_ip_id,primary_ip,primary_mac,distributed_interval,config_tls_refresh,logger_tls_period,team_name,fleet_name,gigs_disk_space_available,percent_disk_space_available,gigs_total_disk_space,issues,device_mapping,status,display_text 2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,1,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,false,foo.local0,a4fc55a1-b5de-409c-a2f4-441f564680d3,debian,,,,,,0s,0,,,,0,0,,,,,,,,,0,0,0,,,0,0,0,0,,,, 2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:22:56Z,false,foo.local1,689539e5-72f0-4bf7-9cc5-1530d3814660,rhel,,,,,,0s,0,,,,0,0,,,,,,,,,0,0,0,,,0,0,0,0,,,, 2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,3,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:21:56Z,false,foo.local2,48ebe4b0-39c3-4a74-a67f-308f7b5dd171,linux,,,,,,0s,0,,,,0,0,,,,,,,,,0,0,0,,,0,0,0,0,,,, @@ -5067,6 +5099,7 @@ Retrieves a list of the configuration profiles assigned to a host. { "profile_uuid": "bc84dae7-396c-4e10-9d45-5768bce8b8bd", "team_id": 0, + "fleet_id": 0, "name": "Example profile", "identifier": "com.example.profile", "created_at": "2023-03-31T00:00:00Z", @@ -5620,14 +5653,15 @@ The `hostname` host identifier is deprecated. Please use `host_ids`, `hardware_s "count": 0, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null } } ``` ### Update label -Updates the specified label. Note: Label queries, platforms, and teams are immutable. To change these, you must delete the label and create a new label. +Updates the specified label. Note: Label queries, platforms, and fleets are immutable. To change these, you must delete the label and create a new label. `PATCH /api/v1/fleet/labels/:id` @@ -5676,7 +5710,9 @@ The `hostname` host identifier is deprecated. Please use `host_ids`, `hardware_s "host_ids": [42, 43], "author_id": 1, "team_id": null, - "team_name": null + "fleet_id": null, + "team_name": null, + "fleet_name": null } } ``` @@ -5717,7 +5753,9 @@ Returns the specified label. "host_ids": null, "author_id": 1, "team_id": null, - "team_name": null + "fleet_id": null, + "team_name": null, + "fleet_name": null, } } ``` @@ -5732,7 +5770,7 @@ Returns a list of labels in Fleet, including basic information on each label. | Name | Type | In | Description | | --------------- | ------- | ----- |------------------------------------- | -| team_id | string | query | _Available in Fleet Premium._ Filters to labels belonging to the specified team, plus global labels. Specify `"global"` to show only globally-available labels. If omitted, Fleet returns all global labels, plus all labels for teams to which the requestor has access. | +| fleet_id | string | query | _Available in Fleet Premium._ Filters to labels belonging to the specified fleet, plus global labels. Specify `"global"` to show only globally-available labels. If omitted, Fleet returns all global labels, plus all labels for fleets to which the requestor has access. | #### Example @@ -5750,42 +5788,48 @@ Returns a list of labels in Fleet, including basic information on each label. "name": "All Hosts", "description": "All hosts which have enrolled in Fleet", "label_type": "builtin", - "team_id": null + "team_id": null, + "fleet_id": null }, { "id": 7, "name": "macOS", "description": "All macOS hosts", "label_type": "builtin", - "team_id": null + "team_id": null, + "fleet_id": null }, { "id": 8, "name": "Ubuntu Linux", "description": "All Ubuntu hosts", "label_type": "builtin", - "team_id": null + "team_id": null, + "fleet_id": null }, { "id": 9, "name": "CentOS Linux", "description": "All CentOS hosts", "label_type": "builtin", - "team_id": null + "team_id": null, + "fleet_id": null }, { "id": 10, "name": "MS Windows", "description": "All Windows hosts", "label_type": "builtin", - "team_id": null + "team_id": null, + "fleet_id": null }, { "id": 11, - "name": "My team-specific label", - "description": "This one goes to eleven, but only on one team", + "name": "My fleet-specific label", + "description": "This one goes to eleven, but only on one fleet", "label_type": "regular", - "team_id": 1 + "team_id": 1, + "fleet_id": 1 } ] } @@ -5804,7 +5848,7 @@ Returns a list of labels. | include_host_counts | boolean | query | Whether or not to calculate host counts for each label. Default is `true`. See "additional notes" for more information. | | order_key | string | query | What to order results by. Can be any column in the labels table. | | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | -| team_id | string | query | _Available in Fleet Premium._ Filters to labels belonging to the specified team, plus global labels. Specify `"global"` to show only globally-available labels. If omitted, Fleet returns all global labels, plus all labels for teams to which the requestor has access. | +| fleet_id | string | query | _Available in Fleet Premium._ Filters to labels belonging to the specified fleet, plus global labels. Specify `"global"` to show only globally-available labels. If omitted, Fleet returns all global labels, plus all labels for fleets to which the requestor has access. | When `include_host_counts` is `true` (or omitted), `host_count` will only be included for `labels` that are in use by one or more hosts, but `count` will always be included, even if it is `0`. When `include_host_counts` is `false`, `host_count` will always be omitted, and `count` will be returned as `0` for each label. Setting `include_host_counts=false` will improve API performance, especially on deployments with large numbers of hosts and labels. @@ -5833,7 +5877,8 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 7, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null }, { "created_at": "2021-02-02T23:55:25Z", @@ -5850,7 +5895,8 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 1, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null }, { "created_at": "2021-02-02T23:55:25Z", @@ -5867,7 +5913,8 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 3, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null }, { "created_at": "2021-02-02T23:55:25Z", @@ -5883,7 +5930,8 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 3, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null }, { "created_at": "2021-02-02T23:55:25Z", @@ -5899,14 +5947,15 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 0, "host_ids": null, "author_id": 1, - "team_id": null + "team_id": null, + "fleet_id": null }, { "created_at": "2025-11-13T06:14:20Z", "updated_at": "2025-11-13T06:14:20Z", "id": 4663, "name": "Team: g-software", - "description": "Workstations used by team g-software", + "description": "Workstations used by g-software", "query": "", "platform": "", "label_type": "regular", @@ -5915,7 +5964,8 @@ When `include_host_counts` is `true` (or omitted), `host_count` will only be inc "count": 0, "host_ids": null, "author_id": 1, - "team_id": 2 + "team_id": 2, + "fleet_id": null } ] } @@ -5939,17 +5989,17 @@ Returns a list of the hosts that belong to the specified label. | after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. | | status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. | -| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified fleet. | | disable_failing_policies | boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | | mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). | | mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). | | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. 'pending' only includes Apple (macOS, iOS, iPadOS) hosts in Apple Business Manager (ABM) that are not yet enrolled to Fleet. | -| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to 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.** | +| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | | low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | macos_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | -| 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'. **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 | 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 disk encryption status. 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.** | +| 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'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | +| 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 fleet ID filter, the results include only "Unassigned" hosts.** | +| os_settings_disk_encryption | string | query | Filters the hosts by disk encryption status. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a fleet ID filter, the results include only "Unassigned" hosts.** | If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings_disk_encryption` is specified, then Windows Servers are excluded from the results. @@ -6003,8 +6053,10 @@ If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings "config_tls_refresh": 10, "logger_tls_period": 10, "team_id": null, + "fleet_id": null, "pack_stats": null, "team_name": null, + "fleet_name": null, "status": "offline", "display_text": "e2e7f8d8983d", "mdm": { @@ -6096,7 +6148,7 @@ Add a configuration profile to enforce custom settings on macOS and Windows host | Name | Type | In | Description | | ------------------------- | -------- | ---- | ------------------------------------------------------------------------------------------------------------- | | profile | file | body | **Required.** The .mobileconfig and JSON for macOS or XML for Windows file containing the profile. | -| team_id | string | body | _Available in Fleet Premium_. The team ID for the profile. If specified, the profile is applied to only hosts that are assigned to the specified team. If not specified, the profile is applied to only to hosts that are not assigned to any team. | +| fleet_id | string | body | _Available in Fleet Premium_. The fleet ID for the profile. If specified, the profile is applied to only hosts that are assigned to the specified fleet. If not specified, the profile is applied to only hosts that are "Unassigned". | | labels_include_all | array | body | _Available in Fleet Premium_. Target hosts that have all labels, specified by label name, in the array. | | labels_include_any | array | body | _Available in Fleet Premium_. Target hosts that have any label, specified by label name, in the array. | | labels_exclude_any | array | body | _Available in Fleet Premium_. Target hosts that that don’t have any label, specified by label name, in the array. | @@ -6109,7 +6161,7 @@ of duplicate payload display name or duplicate payload identifier (macOS profile #### Example Add a new configuration profile to be applied to macOS hosts -assigned to a team. Note that in this example the form data specifies`team_id` in addition to +assigned to a fleet. Note that in this example the form data specifies `fleet_id` in addition to `profile`. `POST /api/v1/fleet/configuration_profiles` @@ -6118,7 +6170,7 @@ assigned to a team. Note that in this example the form data specifies`team_id` i ```http profile="Foo.mobileconfig" -team_id="1" +fleet_id="1" labels_include_all="Label name 1" ``` @@ -6139,8 +6191,8 @@ labels_include_all="Label name 1" Get a list of the configuration profiles in Fleet. For Fleet Premium, the list can -optionally be filtered by team ID. If no team ID is specified, team profiles are excluded from the -results (i.e., only profiles that are associated with "No team" are listed). +optionally be filtered by fleet ID. If no fleet ID is specified, fleet profiles are excluded from the +results (i.e., only profiles that are associated with "Unassigned" are listed). `GET /api/v1/fleet/configuration_profiles` @@ -6148,13 +6200,13 @@ results (i.e., only profiles that are associated with "No team" are listed). | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | string | query | _Available in Fleet Premium_. The team id to filter profiles. | +| fleet_id | string | query | _Available in Fleet Premium_. The fleet id to filter profiles. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | #### Example -List all configuration profiles for macOS and Windows hosts enrolled to Fleet's MDM that are not assigned to any team. +List all configuration profiles for macOS and Windows hosts enrolled to Fleet's MDM that are "Unassigned". `GET /api/v1/fleet/configuration_profiles` @@ -6341,7 +6393,7 @@ Resends a configuration profile for the specified host. Currently, macOS, iOS, i ### Batch-update custom OS settings (configuration profiles) -Modify configuration profiles for a team. The provided list of profiles will be the active profiles for the specified team. If no team (`team_id` or `team_name`) is provided, the profiles are applied for all hosts (Fleet Free) or for hosts that are assigned to "No team" (Fleet Premium). +Modify configuration profiles for a fleet. The provided list of profiles will be the active profiles for the specified fleet. If no fleet (`fleet_id` or `fleet_name`) is provided, the profiles are applied for all hosts (Fleet Free) or for hosts that are "Unassigned" (Fleet Premium). For Apple (macOS, iOS, iPadOS) profiles, Fleet will send only an `InstallProfile` command (edit) for all existing profiles with the same `PayloadIdentifier` (specified in the .mobileconfig file). Fleet will send a `RemoveProfile` command to hosts for all existing profiles that are not part of the list. @@ -6359,8 +6411,8 @@ For requests with 100+ profiles, requests will take 5+ seconds. | Name | Type | In | Description | | --------- | ------ | ----- | --------------------------------------------------------------------------------------------------------------------------------- | -| team_id | number | query | _Available in Fleet Premium_ The team ID to apply the configuration profiles to. Only one of `team_name` or `team_id` may be included in the request. | -| team_name | string | query | _Available in Fleet Premium_ The name of the team to apply the custom settings to. Only one of `team_name` or `team_id` may be included in the request. | +| fleet_id | number | query | _Available in Fleet Premium_ The fleet ID to apply the configuration profiles to. Only one of `fleet_name` or `fleet_id` may be included in the request. | +| fleet_name | string | query | _Available in Fleet Premium_ The name of the fleet to apply the custom settings to. Only one of `fleet_name` or `fleet_id` may be included in the request. | | dry_run | bool | query | Validate the provided profiles and return any validation errors, but do not apply the changes. | | configuration_profiles | object | body | **Required**. See [configuration_profiles](#configuration-profiles) | @@ -6378,7 +6430,7 @@ For each `profile`, only one of `labels_include_all`, `labels_include_any`, or ` #### Example -`POST /api/v1/fleet/configuration_profiles/batch?team_id=1` +`POST /api/v1/fleet/configuration_profiles/batch?fleet_id=1` ##### Request body @@ -6471,8 +6523,8 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------------- | ------ | ---- | -------------------------------------------------------------------------------------- | -| team_id | integer | body | The team ID to apply the settings to. Settings applied to hosts in no team if absent. | -| enable_disk_encryption | boolean | body | Whether disk encryption should be enforced on devices that belong to the team (or no team). | +| fleet_id | integer | body | The fleet ID to apply the settings to. Settings are applied to "Unassigned" hosts if absent. | +| enable_disk_encryption | boolean | body | Whether disk encryption should be enforced on devices that belong to the fleet (or "Unassigned"). | | windows_require_bitlocker_pin | boolean | body | End users on Windows hosts will be required to set a BitLocker PIN if set to true. `enable_disk_encryption` must be set to true. When the PIN is set, it's required to unlock Windows host during startup. | #### Example @@ -6490,7 +6542,7 @@ _Available in Fleet Premium_ Get aggregate status counts of disk encryption enforced on macOS and Windows hosts. -The summary can optionally be filtered by team ID. +The summary can optionally be filtered by fleet ID. `GET /api/v1/fleet/disk_encryption` @@ -6498,7 +6550,7 @@ The summary can optionally be filtered by team ID. | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | string | query | _Available in Fleet Premium_. The team ID to filter the summary. | +| fleet_id | string | query | _Available in Fleet Premium_. The fleet ID to filter the summary. | #### Example @@ -6527,7 +6579,7 @@ The summary can optionally be filtered by team ID. Get aggregate status counts of all OS settings (configuration profiles and disk encryption) enforced on hosts. For Fleet Premium users, the counts can -optionally be filtered by `team_id`. If no `team_id` is specified, team profiles are excluded from the results (i.e., only profiles that are associated with "No team" are listed). +optionally be filtered by `fleet_id`. If no `fleet_id` is specified, fleet profiles are excluded from the results (i.e., only profiles that are associated with "Unassigned" are listed). `GET /api/v1/fleet/configuration_profiles/summary` @@ -6535,11 +6587,11 @@ optionally be filtered by `team_id`. If no `team_id` is specified, team profiles | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | string | query | _Available in Fleet Premium_. The team ID to filter profiles. | +| fleet_id | string | query | _Available in Fleet Premium_. The fleet ID to filter profiles. | #### Example -Get aggregate status counts of profiles for to macOS and Windows hosts that are assigned to "No team". +Get aggregate status counts of profiles for macOS and Windows hosts that are "Unassigned". `GET /api/v1/fleet/configuration_profiles/summary` @@ -6616,7 +6668,7 @@ Get status counts of a single OS settings (configuration profile) enforced on ho _Available in Fleet Premium_ -Sets the custom MDM setup enrollment profile for a team or no team. +Sets the custom MDM setup enrollment profile for a fleet or "Unassigned". `POST /api/v1/fleet/enrollment_profiles/automatic` @@ -6624,7 +6676,7 @@ Sets the custom MDM setup enrollment profile for a team or no team. | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | integer | json | The team ID this custom enrollment profile applies to, or no team if omitted. | +| fleet_id | integer | json | The fleet ID this custom enrollment profile applies to, or "Unassigned" if omitted. | | name | string | json | The filename of the uploaded custom enrollment profile. | | enrollment_profile | object | json | The custom enrollment profile's json, as documented in https://developer.apple.com/documentation/devicemanagement/profile. | @@ -6639,6 +6691,7 @@ Sets the custom MDM setup enrollment profile for a team or no team. ```json { "team_id": 123, + "fleet_id": 123, "name": "dep_profile.json", "uploaded_at": "2023-04-04:00:00Z", "enrollment_profile": { @@ -6654,7 +6707,7 @@ Sets the custom MDM setup enrollment profile for a team or no team. _Available in Fleet Premium_ -Gets the custom MDM setup enrollment profile for a team or no team. +Gets the custom MDM setup enrollment profile for a fleet or "Unassigned". `GET /api/v1/fleet/enrollment_profiles/automatic` @@ -6662,11 +6715,11 @@ Gets the custom MDM setup enrollment profile for a team or no team. | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | integer | query | The team ID for which to return the custom enrollment profile, or no team if omitted. | +| fleet_id | integer | query | The fleet ID for which to return the custom enrollment profile, or "Unassigned" if omitted. | #### Example -`GET /api/v1/fleet/enrollment_profiles/automatic?team_id=123` +`GET /api/v1/fleet/enrollment_profiles/automatic?fleet_id=123` ##### Default response @@ -6675,6 +6728,7 @@ Gets the custom MDM setup enrollment profile for a team or no team. ```json { "team_id": 123, + "fleet_id": 123, "name": "dep_profile.json", "uploaded_at": "2023-04-04:00:00Z", "enrollment_profile": { @@ -6688,7 +6742,7 @@ Gets the custom MDM setup enrollment profile for a team or no team. _Available in Fleet Premium_ -Deletes the custom MDM setup enrollment profile assigned to a team or no team. +Deletes the custom MDM setup enrollment profile assigned to a fleet or "Unassigned". `DELETE /api/v1/fleet/enrollment_profiles/automatic` @@ -6696,11 +6750,11 @@ Deletes the custom MDM setup enrollment profile assigned to a team or no team. | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | integer | query | The team ID for which to delete the custom enrollment profile, or no team if omitted. | +| fleet_id | integer | query | The fleet ID for which to delete the custom enrollment profile, or "Unassigned" if omitted. | #### Example -`DELETE /api/v1/fleet/enrollment_profiles/automatic?team_id=123` +`DELETE /api/v1/fleet/enrollment_profiles/automatic?fleet_id=123` ##### Default response @@ -6711,9 +6765,9 @@ Deletes the custom MDM setup enrollment profile assigned to a team or no team. `GET /api/v1/fleet/enrollment_profiles/ota` -The returned value is a signed `.mobileconfig` OTA enrollment profile (see [Apple enrollment profile docs](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/OTASecurity/OTASecurity.html)). Install this profile on macOS, iOS, or iPadOS hosts to enroll them to a specific team in Fleet and turn on MDM features. +The returned value is a signed `.mobileconfig` OTA enrollment profile (see [Apple enrollment profile docs](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/OTASecurity/OTASecurity.html)). Install this profile on macOS, iOS, or iPadOS hosts to enroll them to a specific fleet in Fleet and turn on MDM features. -If the team in Fleet has [end user authentication](https://fleetdm.com/guides/setup-experience#end-user-authentication) enabled, the OTA enrollment profile won't work. Use the [manual enrollment profile](#get-manual-enrollment-profile) instead. +If the fleet has [end user authentication](https://fleetdm.com/guides/setup-experience#end-user-authentication) enabled, the OTA enrollment profile won't work. Use the [manual enrollment profile](#get-manual-enrollment-profile) instead. To enroll macOS hosts, turn on MDM features, and add [human-device mapping](https://fleetdm.com/guides/foreign-vitals-map-idp-users-to-hosts), use the [manual enrollment profile](#get-manual-enrollment-profile) instead. @@ -6721,7 +6775,7 @@ To enroll macOS hosts, turn on MDM features, and add [human-device mapping](http | Name | Type | In | Description | |-------------------|---------|-------|----------------------------------------------------------------------------------| -| enroll_secret | string | query | **Required**. The enroll secret of the team this host will be assigned to. | +| enroll_secret | string | query | **Required**. The enroll secret of the fleet this host will be assigned to. | #### Example @@ -6819,13 +6873,13 @@ Upload a bootstrap package that will be automatically installed during DEP setup | Name | Type | In | Description | | ------- | ------ | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | package | file | body | **Required**. The bootstrap package installer. It must be a signed `pkg` file. | -| team_id | string | body | The team ID for the package. If specified, the package will be installed to hosts that are assigned to the specified team. If not specified, the package will be installed to hosts that are not assigned to any team. | +| fleet_id | string | body | The fleet ID for the package. If specified, the package will be installed to hosts that are assigned to the specified fleet. If not specified, the package will be installed on "Unassigned" hosts. | | manual_agent_install | boolean | body | If set to `true` Fleet's agent (fleetd) won't be installed as part of automatic enrollment (ADE) on macOS hosts. (Default: `false`) | #### Example Upload a bootstrap package that will be installed to macOS hosts enrolled to MDM that are -assigned to a team. Note that in this example the form data specifies `team_id` in addition to +assigned to a fleet. Note that in this example the form data specifies `fleet_id` in addition to `package`. `POST /api/v1/fleet/bootstrap` @@ -6833,7 +6887,7 @@ assigned to a team. Note that in this example the form data specifies `team_id` ##### Request body ```http -team_id="1" +fleet_id="1" package="bootstrap-package.pkg" ``` @@ -6847,13 +6901,13 @@ _Available in Fleet Premium_ Get information about a bootstrap package that was uploaded to Fleet. -`GET /api/v1/fleet/bootstrap/:team_id/metadata` +`GET /api/v1/fleet/bootstrap/:fleet_id/metadata` #### Parameters | Name | Type | In | Description | | ------- | ------ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| team_id | string | url | **Required** The team ID for the package. Zero (0) can be specified to get information about the bootstrap package for hosts that don't belong to a team. | +| fleet_id | string | url | **Required** The fleet ID for the package. Zero (0) can be specified to get information about the bootstrap package for "Unassigned" hosts. | | for_update | boolean | query | If set to `true`, the authorization will be for a `write` action instead of a `read`. Useful for the write-only `gitops` role when requesting the bootstrap metadata to check if the package needs to be replaced. | #### Example @@ -6868,6 +6922,7 @@ Get information about a bootstrap package that was uploaded to Fleet. { "name": "bootstrap-package.pkg", "team_id": 0, + "fleet_id": 0, "sha256": "6bebb4433322fd52837de9e4787de534b4089ac645b0692dfb74d000438da4a3", "token": "AA598E2A-7952-46E3-B89D-526D45F7E233", "created_at": "2023-04-20T13:02:05Z" @@ -6884,15 +6939,15 @@ In the response above: _Available in Fleet Premium_ -Delete a team's bootstrap package. +Delete a fleet's bootstrap package. -`DELETE /api/v1/fleet/bootstrap/:team_id` +`DELETE /api/v1/fleet/bootstrap/:fleet_id` #### Parameters | Name | Type | In | Description | | ------- | ------ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| team_id | string | url | **Required** The team ID for the package. Zero (0) can be specified to get information about the bootstrap package for hosts that don't belong to a team. | +| fleet_id | string | url | **Required** The fleet ID for the package. Zero (0) can be specified to get information about the bootstrap package for "Unassigned" hosts. | #### Example @@ -6940,7 +6995,7 @@ _Available in Fleet Premium_ Get aggregate status counts of bootstrap packages delivered to DEP enrolled hosts. -The summary can optionally be filtered by team ID. +The summary can optionally be filtered by fleet ID. `GET /api/v1/fleet/bootstrap/summary` @@ -6948,7 +7003,7 @@ The summary can optionally be filtered by team ID. | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | string | query | The team ID to filter the summary. | +| fleet_id | string | query | The fleet ID to filter the summary. | #### Example @@ -6978,7 +7033,7 @@ _Available in Fleet Premium_ | Name | Type | In | Description | | ------------- | ------ | ---- | -------------------------------------------------------------------------------------- | -| team_id | integer | body | The team ID to apply the settings to. Settings applied to hosts in no team if absent. | +| fleet_id | integer | body | The fleet ID to apply the settings to. Settings are applied to "Unassigned" hosts if absent. | | enable_end_user_authentication | boolean | body | When enabled, require end users to authenticate with your identity provider (IdP) when they set up their new macOS hosts. | | require_all_software_macos | boolean | body | If set to `true`, setup will be canceled on macOS hosts if any software installs fail. | | enable_release_device_manually | boolean | body | When enabled, you're responsible for sending the [`DeviceConfigured` command](https://developer.apple.com/documentation/devicemanagement/device-configured-command). End users will be stuck in Setup Assistant until this command is sent. | @@ -6993,6 +7048,7 @@ _Available in Fleet Premium_ ```json { "team_id": 1, + "fleet_id": 1, "enable_end_user_authentication": true, "enable_release_device_manually": true } @@ -7131,14 +7187,14 @@ List software that can be automatically installed during setup. If `install_duri | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | | platform | string | query | Filters software titles available for install by platforms. Options are `"macos"`, `"windows"`, `"linux"`, `"ios"`, `"ipados"`, and `"android"`. Defaults to `"macos"`. To show titles from multiple platforms, separate the platforms with commas (e.g. `?platform=macos,ios,android`). | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to filter software by. If not specified, it will filter only software that's available to hosts with no team. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet to filter software by. If not specified, it will filter only software that's available for "Unassigned" hosts. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | #### Example -`GET /api/v1/fleet/setup_experience/software?team_id=3` +`GET /api/v1/fleet/setup_experience/software?fleet_id=3` ##### Default response @@ -7150,7 +7206,7 @@ List software that can be automatically installed during setup. If `install_duri { "id": 12, "name": "Firefox.app", - "icon_url": "/api/latest/fleet/software/titles/12/icon?team_id=3", + "icon_url": "/api/latest/fleet/software/titles/12/icon?fleet_id=3", "software_package": { "name": "FirefoxInstall.pkg", "platform": "darwin", @@ -7204,7 +7260,7 @@ Set software that will be automatically installed during setup. Software that is | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | | platform | string | query | Platform to install software for. Either `"macos"`, `"windows"`, `"linux"`, `"ios"`, `"ipados"`, or `"android"`. Defaults to `"macos"`. | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to set the software for. If not specified, it will set the software for hosts with no team. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet to set the software for. If not specified, it will set the software for "Unassigned" hosts. | | software_title_ids | array | body | The ID of software titles to install during setup. | #### Example @@ -7217,6 +7273,7 @@ Set software that will be automatically installed during setup. Software that is { "platform": "linux", "team_id": 1, + "fleet_id": 1, "software_title_ids": [3000, 3001] } ``` @@ -7241,7 +7298,7 @@ Add a script that will automatically run during macOS setup. | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | -| team_id | integer | body | _Available in Fleet Premium_. The ID of the team to add the script to. If not specified, a script will be added for hosts with no team. | +| fleet_id | integer | body | _Available in Fleet Premium_. The ID of the fleet to add the script to. If not specified, a script will be added for "Unassigned" hosts. | | script | file | body | The contents of the script to run during setup. | #### Example @@ -7251,7 +7308,7 @@ Add a script that will automatically run during macOS setup. ##### Request body ```http -team_id="1" +fleet_id="1" script="myscript.sh" ``` @@ -7263,7 +7320,7 @@ script="myscript.sh" _Available in Fleet Premium_ -Changes the script that will automatically run during macOS setup. Updates the existing script for the team, or for hosts with no team, if one already exists. +Changes the script that will automatically run during macOS setup. Updates the existing script for the fleet, or for "Unassigned" hosts, if one already exists. > You need to send a request of type `multipart/form-data`. @@ -7271,7 +7328,7 @@ Changes the script that will automatically run during macOS setup. Updates the e | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | -| team_id | integer | body | _Available in Fleet Premium_. The ID of the team to add the script to. If not specified, a script will be added for hosts with no team. | +| fleet_id | integer | body | _Available in Fleet Premium_. The ID of the fleet to add the script to. If not specified, a script will be added for "Unassigned" hosts. | | script | file | body | The contents of the script to run during setup. | #### Example @@ -7281,7 +7338,7 @@ Changes the script that will automatically run during macOS setup. Updates the e ##### Request body ```http -team_id="1" +fleet_id="1" script="myscript.sh" ``` @@ -7299,13 +7356,13 @@ Get a script that will automatically run during macOS setup. | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to get the script for. If not specified, script will be returned for hosts with no team. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet to get the script for. If not specified, script will be returned for "Unassigned" hosts. | | alt | string | query | If specified and set to "media", downloads the script's contents. | #### Example (get script) -`GET /api/v1/fleet/setup_experience/script?team_id=3` +`GET /api/v1/fleet/setup_experience/script?fleet_id=3` ##### Default response @@ -7315,6 +7372,7 @@ Get a script that will automatically run during macOS setup. { "id": 1, "team_id": 3, + "fleet_id": 3, "name": "setup-experience-script.sh", "created_at": "2023-07-30T13:41:07Z", "updated_at": "2023-07-30T13:41:07Z" @@ -7323,7 +7381,7 @@ Get a script that will automatically run during macOS setup. #### Example (download script) -`GET /api/v1/fleet/setup_experience/script?team_id=3?alt=media` +`GET /api/v1/fleet/setup_experience/script?fleet_id=3?alt=media` ##### Example response headers @@ -7351,11 +7409,11 @@ Delete a script that will automatically run during macOS setup. | Name | Type | In | Description | | ----- | ------ | ----- | ---------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to get the script for. If not specified, script will be returned for hosts with no team. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet to get the script for. If not specified, script will be returned for "Unassigned" hosts. | #### Example -`DELETE /api/v1/fleet/setup_experience/script?team_id=3` +`DELETE /api/v1/fleet/setup_experience/script?fleet_id=3` ##### Default response @@ -7581,13 +7639,25 @@ None. "name": "💻 Workstations", "id": 1 }, + "macos_fleet": { + "name": "💻 Workstations", + "id": 1 + }, "ios_team": { "name": "📱🏢 Company-owned iPhones", "id": 2 }, + "ios_fleet": { + "name": "📱🏢 Company-owned iPhones", + "id": 2 + }, "ipados_team": { "name": "🔳🏢 Company-owned iPads", "id": 3 + }, + "ipados_fleet": { + "name": "🔳🏢 Company-owned iPads", + "id": 3 } } ] @@ -7618,7 +7688,7 @@ None. "org_name": "Fleet Device Management Inc.", "location": "https://example.com/mdm/apple/mdm", "renew_date": "2023-11-29T00:00:00Z", - "teams": [ + "fleets": [ { "name": "💻 Workstations", "id": 1 @@ -7706,17 +7776,17 @@ None. ## Policies - [List policies](#list-policies) -- [List team policies](#list-team-policies) +- [List fleet policies](#list-fleet-policies) - [Get policies count](#get-policies-count) -- [Get team policies count](#get-team-policies-count) +- [Get fleet policies count](#get-fleet-policies-count) - [Get policy](#get-policy) -- [Get team policy](#get-team-policy) +- [Get fleet policy](#get-fleet-policy) - [Create policy](#create-policy) -- [Create team policy](#create-team-policy) +- [Create fleet policy](#create-fleet-policy) - [Delete policies](#delete-policies) -- [Delete team policies](#delete-team-policies) +- [Delete fleet policies](#delete-fleet-policies) - [Update policy](#update-policy) -- [Update team policy](#update-team-policy) +- [Update fleet policy](#update-fleet-policy) - [Reset policy automations](#reset-policy-automations) Policies are yes or no questions you can ask about your hosts. @@ -7795,18 +7865,18 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th --- -### List team policies +### List fleet policies _Available in Fleet Premium_ -`GET /api/v1/fleet/teams/:id/policies` +`GET /api/v1/fleet/fleets/:id/policies` #### Parameters | Name | Type | In | Description | | ------------------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | -| id | integer | path | **Required.** Defines what team ID to operate on | -| merge_inherited | boolean | query | If `true`, will return both team policies **and** inherited ("All teams") policies the `policies` list, and will not return a separate `inherited_policies` list. | +| id | integer | path | **Required.** Defines what fleet ID to operate on | +| merge_inherited | boolean | query | If `true`, will return both fleet policies **and** inherited ("All fleets") policies in the `policies` list, and will not return a separate `inherited_policies` list. | | query | string | query | Search query keywords. Searchable fields include `name`. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | @@ -7814,7 +7884,7 @@ _Available in Fleet Premium_ #### Example (default usage) -`GET /api/v1/fleet/teams/1/policies` +`GET /api/v1/fleet/fleets/1/policies` ##### Default response @@ -7897,7 +7967,7 @@ _Available in Fleet Premium_ "inherited_policies": [ { "id": 136, - "name": "Arbitrary Test Policy (all platforms) (all teams)", + "name": "Arbitrary Test Policy (all platforms) (all fleets)", "query": "SELECT 1 FROM osquery_info WHERE 1=1;", "description": "If you're seeing this, mostly likely this is because someone is testing out failing policies in dogfood. You can ignore this.", "critical": true, @@ -7919,7 +7989,7 @@ _Available in Fleet Premium_ #### Example (returns single list) -`GET /api/v1/fleet/teams/1/policies?merge_inherited=true` +`GET /api/v1/fleet/fleets/1/policies?merge_inherited=true` ##### Default response @@ -7973,7 +8043,7 @@ _Available in Fleet Premium_ }, { "id": 136, - "name": "Arbitrary Test Policy (all platforms) (all teams)", + "name": "Arbitrary Test Policy (all platforms) (all fleets)", "query": "SELECT 1 FROM osquery_info WHERE 1=1;", "description": "If you're seeing this, mostly likely this is because someone is testing out failing policies in dogfood. You can ignore this.", "critical": true, @@ -8022,22 +8092,22 @@ _Available in Fleet Premium_ --- -### Get team policies count +### Get fleet policies count _Available in Fleet Premium_ -`GET /api/v1/fleet/team/:team_id/policies/count` +`GET /api/v1/fleet/fleets/:fleet_id/policies/count` #### Parameters | Name | Type | In | Description | | ------------------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | -| team_id | integer | path | **Required.** Defines what team ID to operate on +| fleet_id | integer | path | **Required.** Defines what fleet ID to operate on | query | string | query | Search query keywords. Searchable fields include `name`. | -| merge_inherited | boolean | query | If `true`, will include inherited ("All teams") policies in the count. | +| merge_inherited | boolean | query | If `true`, will include inherited ("All fleets") policies in the count. | #### Example -`GET /api/v1/fleet/team/1/policies/count` +`GET /api/v1/fleet/fleets/1/policies/count` ##### Default response @@ -8094,22 +8164,22 @@ _Available in Fleet Premium_ --- -### Get team policy +### Get fleet policy _Available in Fleet Premium_ -`GET /api/v1/fleet/teams/:team_id/policies/:policy_id` +`GET /api/v1/fleet/fleets/:fleet_id/policies/:policy_id` #### Parameters | Name | Type | In | Description | | ------------------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------- | -| team_id | integer | path | **Required.** Defines what team ID to operate on | +| fleet_id | integer | path | **Required.** Defines what fleet ID to operate on | | policy_id | integer | path | **Required.** The policy's ID. | #### Example -`GET /api/v1/fleet/teams/1/policies/43` +`GET /api/v1/fleet/fleets/1/policies/43` ##### Default response @@ -8220,21 +8290,21 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne --- -### Create team policy +### Create fleet policy _Available in Fleet Premium_ > **Experimental feature**. Software related features (like install software policy automation) are undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. -The semantics for creating a team policy are the same as for global policies, see [Create policy](#create-policy). +The semantics for creating a fleet policy are the same as for global policies, see [Create policy](#create-policy). -`POST /api/v1/fleet/teams/:id/policies` +`POST /api/v1/fleet/fleets/:id/policies` #### Parameters | Name | Type | In | Description | |-------------------| ------- | ---- |--------------------------------------------------------------------------------------------------------------------------------------------------------| -| id | integer | path | Defines what team ID to operate on. | +| id | integer | path | Defines what fleet ID to operate on. | | name | string | body | The policy's name. | | query | string | body | The policy's query in SQL. | | description | string | body | The policy's description. | @@ -8252,7 +8322,7 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne #### Example -`POST /api/v1/fleet/teams/1/policies` +`POST /api/v1/fleet/fleets/1/policies` ##### Request body @@ -8283,6 +8353,7 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne "author_name": "John", "author_email": "john@example.com", "team_id": 1, + "fleet_id": 1, "resolution": "Resolution steps", "platform": "darwin", "created_at": "2021-12-16T14:37:37Z", @@ -8340,22 +8411,22 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne --- -### Delete team policies +### Delete fleet policies _Available in Fleet Premium_ -`POST /api/v1/fleet/teams/:team_id/policies/delete` +`POST /api/v1/fleet/fleets/:fleet_id/policies/delete` #### Parameters | Name | Type | In | Description | | -------- | ------- | ---- | ------------------------------------------------- | -| team_id | integer | path | **Required.** Defines what team ID to operate on | +| fleet_id | integer | path | **Required.** Defines what fleet ID to operate on | | ids | array | body | **Required.** The IDs of the policies to delete. | #### Example -`POST /api/v1/fleet/teams/1/policies/delete` +`POST /api/v1/fleet/fleets/1/policies/delete` ##### Request body @@ -8443,7 +8514,7 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne --- -### Update team policy +### Update fleet policy _Available in Fleet Premium_ @@ -8451,13 +8522,13 @@ _Available in Fleet Premium_ > + The `conditional_access_bypass_enabled` setting is experimental, and will be replaced with a reference to the policy's `critical` setting in Fleet 4.83.0. To ensure a seamless upgrade, please avoid enabling bypass for policies marked `critical`. > + Software related features (like install software policy automation) are undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. -`PATCH /api/v1/fleet/teams/:team_id/policies/:policy_id` +`PATCH /api/v1/fleet/fleets/:fleet_id/policies/:policy_id` #### Parameters | Name | Type | In | Description | |-------------------------| ------- | ---- |---------------------------------------------------------------------------------------------------------------------------------------------------------| -| team_id | integer | path | The team's ID. | +| fleet_id | integer | path | The fleet's ID. | | policy_id | integer | path | The policy's ID. | | name | string | body | The query's name. | | query | string | body | The query in SQL. | @@ -8477,7 +8548,7 @@ Only one of `labels_include_any` or `labels_exclude_any` can be specified. If ne #### Example -`PATCH /api/v1/fleet/teams/2/policies/42` +`PATCH /api/v1/fleet/fleets/2/policies/42` ##### Request body @@ -8543,7 +8614,7 @@ Resets [webhook and ticket policy automations](https://fleetdm.com/docs/using-fl | Name | Type | In | Description | | ---------- | -------- | ---- | -------------------------------------------------------- | | policy_ids | array | body | Filters to only run policy automations for the specified policies. | -| team_ids | array | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. | +| fleet_ids | array | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified fleets. | #### Example @@ -8555,6 +8626,7 @@ Resets [webhook and ticket policy automations](https://fleetdm.com/docs/using-fl ```json { "team_ids": [1], + "fleet_ids": [1], "policy_ids": [1, 2, 3] } ``` @@ -8569,41 +8641,41 @@ Resets [webhook and ticket policy automations](https://fleetdm.com/docs/using-fl --- -## Queries +## Reports -- [List queries](#list-queries) -- [Get query](#get-query) -- [Get query report](#get-query-report) -- [Get host's query report](#get-hosts-query-report) -- [Create query](#create-query) -- [Update query](#update-query) -- [Delete query by name](#delete-query-by-name) -- [Delete query by ID](#delete-query-by-id) -- [Delete queries](#delete-queries) -- [Run live query](#run-live-query) +- [List reports](#list-reports) +- [Get report](#get-report) +- [Get report data](#get-report-data) +- [Get host's report data](#get-hosts-report-data) +- [Create report](#create-report) +- [Update report](#update-report) +- [Delete report by name](#delete-report-by-name) +- [Delete report by ID](#delete-report-by-id) +- [Delete reports](#delete-reports) +- [Run live report](#run-live-report) -### List queries +### List reports -Returns a list of global queries or team queries. +Returns a list of reports. To see each report's data, use the [get report data](#get-report-data) endpoint. -`GET /api/v1/fleet/queries` +`GET /api/v1/fleet/reports` #### Parameters | Name | Type | In | Description | | --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | -| order_key | string | query | What to order results by. Can be any column in the queries table. | +| order_key | string | query | What to order results by. Can be any column in the reports table. | | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | -| 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. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet for the reports to be listed. When omitted, returns global reports. | | 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.) | -| platform | string | query | Return queries that are scheduled to run on this platform. One of: `"macos"`, `"windows"`, `"linux"` (case-insensitive). (Since queries cannot be scheduled to run on `"chrome"` hosts, it's not a valid value here) | +| merge_inherited | boolean | query | _Available in Fleet Premium_. If `true`, will include global reports in addition to fleet-level reports when filtering by `fleet_id`. (If no `fleet_id` is provided, this parameter is ignored.) | +| platform | string | query | Return reports that are scheduled to run on this platform. One of: `"macos"`, `"windows"`, `"linux"` (case-insensitive). (Since reports cannot be scheduled to run on `"chrome"` hosts, it's not a valid value here) | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | #### Example -`GET /api/v1/fleet/queries` +`GET /api/v1/fleet/reports` ##### Default response @@ -8616,8 +8688,49 @@ Returns a list of global queries or team queries. "created_at": "2021-01-04T21:19:57Z", "updated_at": "2021-01-04T21:19:57Z", "id": 1, - "name": "query1", - "description": "query", + "name": "report1", + "description": "report", + "query": "SELECT * FROM osquery_info", + "team_id": null, + "interval": 3600, + "platform": "darwin,windows,linux", + "min_osquery_version": "", + "automations_enabled": true, + "logging": "snapshot", + "saved": true, + "observer_can_run": true, + "discard_data": false, + "author_id": 1, + "author_name": "noah", + "author_email": "noah@example.com", + "labels_include_any": [], + "packs": [ + { + "created_at": "2021-01-05T21:13:04Z", + "updated_at": "2021-01-07T19:12:54Z", + "id": 1, + "name": "Pack", + "description": "Pack", + "platform": "", + "disabled": true + } + ], + "stats": { + "system_time_p50": 1.32, + "system_time_p95": 4.02, + "user_time_p50": 3.55, + "user_time_p95": 3.00, + "total_executions": 3920 + } + } + ], + "reports": [ + { + "created_at": "2021-01-04T21:19:57Z", + "updated_at": "2021-01-04T21:19:57Z", + "id": 1, + "name": "report1", + "description": "report", "query": "SELECT * FROM osquery_info", "team_id": null, "interval": 3600, @@ -8699,21 +8812,21 @@ Returns a list of global queries or team queries. } ``` -### Get query +### Get report -Returns the query specified by ID. +Returns the report specified by ID. -`GET /api/v1/fleet/queries/:id` +`GET /api/v1/fleet/reports/:id` #### Parameters | Name | Type | In | Description | | ---- | ------- | ---- | ------------------------------------------ | -| id | integer | path | **Required**. The id of the desired query. | +| id | integer | path | **Required**. The id of the desired report. | #### Example -`GET /api/v1/fleet/queries/31` +`GET /api/v1/fleet/reports/31` ##### Default response @@ -8763,22 +8876,22 @@ Returns the query specified by ID. } ``` -### Get query report +### Get report data -Returns the query report specified by ID. +Returns a specific report's data. -`GET /api/v1/fleet/queries/:id/report` +`GET /api/v1/fleet/report/:id/report` #### Parameters | Name | Type | In | Description | | --------- | ------- | ----- | ----------------------------------------------------------------------------------------- | | id | integer | path | **Required**. The ID of the desired query. | -| team_id | integer | query | Filter the query report to only include hosts that are associated with the team specified | +| fleet_id | integer | query | Filter the query report to only include hosts that are associated with the fleet specified | #### Example -`GET /api/v1/fleet/queries/31/report` +`GET /api/v1/fleet/reports/31/report` ##### Default response @@ -8787,6 +8900,7 @@ Returns the query report specified by ID. ```json { "query_id": 31, + "report_id": 31, "report_clipped": false, "results": [ { @@ -8847,24 +8961,24 @@ If a query has no results stored, then `results` will be an empty array: } ``` -> Note: osquery scheduled queries do not return errors, so only non-error results are included in the report. If you suspect a query may be running into errors, you can use the [live query](#run-live-query) endpoint to get diagnostics. +> Scheduled reports do not return errors, so only non-error results are included. If you suspect a report may be running into errors, you can use the [live report](#run-live-report) endpoint to get diagnostics. -### Get host's query report +### Get host's report data -Returns a query report for a single host. +Returns a specific report's data for a single host. -`GET /api/v1/fleet/hosts/:id/queries/:query_id` +`GET /api/v1/fleet/hosts/:id/reports/:report_id` #### Parameters | Name | Type | In | Description | | --------- | ------- | ----- | ------------------------------------------ | | id | integer | path | **Required**. The ID of the desired host. | -| query_id | integer | path | **Required**. The ID of the desired query. | +| report_id | integer | path | **Required**. The ID of the desired report. | #### Example -`GET /api/v1/fleet/hosts/123/queries/31` +`GET /api/v1/fleet/hosts/123/reports/31` ##### Default response @@ -8873,6 +8987,7 @@ Returns a query report for a single host. ```json { "query_id": 31, + "report_id": 31, "host_id": 1, "host_name": "foo", "last_fetched": "2021-01-19T17:08:31Z", @@ -8900,11 +9015,12 @@ Returns a query report for a single host. } ``` -If a query has no results stored for the specified host, then `results` will be an empty array: +If a report has no results stored for the specified host, then `results` will be an empty array: ```json { "query_id": 31, + "report_id": 31, "host_id": 1, "host_name": "foo", "last_fetched": "2021-01-19T17:08:31Z", @@ -8913,42 +9029,42 @@ If a query has no results stored for the specified host, then `results` will be } ``` -> Note: osquery scheduled queries do not return errors, so only non-error results are included in the report. If you suspect a query may be running into errors, you can use the [live query](#run-live-query) endpoint to get diagnostics. +> Scheduled reports do not return errors, so only non-error results are included in the report. If you suspect a report may be running into errors, you can use the [live report](#run-live-report) endpoint to get diagnostics. -### Create query +### Create report -Creates a global query or team query. +Creates a global report or fleet report. -`POST /api/v1/fleet/queries` +`POST /api/v1/fleet/reports` #### Parameters | Name | Type | In | Description | | ------------------------------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| name | string | body | **Required**. The name of the query. | -| query | string | body | **Required**. The query in SQL syntax. | +| name | string | body | **Required**. The name of the report. | +| query | string | body | **Required**. The SQL query for collecting report data. | | description | string | body | The query's description. | -| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | -| team_id | integer | body | _Available in Fleet Premium_. The parent team to which the new query should be added. If omitted, the query will be global. | -| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | -| platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If omitted, runs on all compatible platforms. | -| labels_include_any | array | body | _Available in Fleet Premium_. Labels, specified by label name, to target with this query. If specified, the query will run on hosts that match **any of these** labels. | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the report as a live report. This field is only relevant for the `observer` role. The `observer_plus` role can run any report and is not limited by this flag. | +| fleet_id | integer | body | _Available in Fleet Premium_. The fleet to which the new report should be added. If omitted, the report will be global. | +| interval | integer | body | The amount of time, in seconds, the report waits before running. Can be set to `0` to never run. Default: 0. | +| platform | string | body | The OS platforms where this report will run (other platforms ignored). Comma-separated string. If omitted, runs on all compatible platforms. | +| labels_include_any | array | body | _Available in Fleet Premium_. Labels, specified by label name, to target with this report. If specified, the report will run on hosts that match **any of these** labels. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | -| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | -| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | boolean | body | Whether to skip saving the latest query results for each host. Default: `false`. | +| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the report's `interval`. | +| logging | string | body | The type of log output for this report. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | +| discard_data | boolean | body | Whether to skip saving the latest results for each host. If set to `true`, data is still sent to the configured log destination if `automations_enabled`. Default: `false`. | #### Example -`POST /api/v1/fleet/queries` +`POST /api/v1/fleet/reports` ##### Request body ```json { - "name": "new_query", - "description": "This is a new query.", + "name": "new_report", + "description": "This is a new report.", "query": "SELECT * FROM osquery_info", "interval": 3600, // Once per hour "platform": "darwin,windows,linux", @@ -8991,34 +9107,58 @@ Creates a global query or team query. "labels_include_any": [ "Hosts with Docker installed" ] + }, + "report": { + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z", + "id": 288, + "name": "new_query", + "query": "SELECT * FROM osquery_info", + "description": "This is a new query.", + "team_id": null, + "interval": 3600, + "platform": "darwin,windows,linux", + "min_osquery_version": "", + "automations_enabled": true, + "logging": "snapshot", + "saved": true, + "author_id": 1, + "author_name": "", + "author_email": "", + "observer_can_run": true, + "discard_data": false, + "packs": [], + "labels_include_any": [ + "Hosts with Docker installed" + ] } } ``` -### Update query +### Update report -Modifies the query specified by ID. +Modifies the report specified by ID. -`PATCH /api/v1/fleet/queries/:id` +`PATCH /api/v1/fleet/reports/:id` #### Parameters | Name | Type | In | Description | | --------------------------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| id | integer | path | **Required.** The ID of the query. | -| name | string | body | The name of the query. | -| query | string | body | The query in SQL syntax. | -| description | string | body | The query's description. | -| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). | -| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. | -| platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If set to "", runs on all compatible platforms. | -| labels_include_any | list | body | _Available in Fleet Premium_. Labels, specified by label name, to target with this query. If specified, the query will run on hosts that match **any of these** labels. | +| id | integer | path | **Required.** The ID of the report. | +| name | string | body | The name of the report. | +| query | string | body | The report's SQL query. | +| description | string | body | The report's description. | +| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the report as a live report. This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag. | +| interval | integer | body | The amount of time, in seconds, the report waits before running. Can be set to `0` to never run. Default: 0. | +| platform | string | body | The OS platforms where this report will run (other platforms ignored). Comma-separated string. If set to "", runs on all compatible platforms. | +| labels_include_any | list | body | _Available in Fleet Premium_. Labels, specified by label name, to target with this report. If specified, the report will run on hosts that match **any of these** labels. | | min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. | -| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. | +| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the report's `interval`. | | logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. | -| discard_data | boolean | body | Whether to skip saving the latest query results for each host. | +| discard_data | boolean | body | Whether to skip saving the latest results for each host. If set to `true`, data is still sent to the configured log destination if `automations_enabled`. | -> Note that any of the following conditions will cause the existing query report to be deleted: +> Note that any of the following conditions will cause the existing report's data to be discarded: > - Updating the `query` (SQL) field > - Updating the filters for targeted hosts (`platform`, `min_osquery_version`, `labels_include_any`) > - Changing `discard_data` from `false` to `true` @@ -9026,13 +9166,13 @@ Modifies the query specified by ID. #### Example -`PATCH /api/v1/fleet/queries/2` +`PATCH /api/v1/fleet/reports/2` ##### Request body ```json { - "name": "new_title_for_my_query", + "name": "new_title_for_my_report", "interval": 3600, // Once per hour, "platform": "", "min_osquery_version": "", @@ -9074,68 +9214,92 @@ Modifies the query specified by ID. "Hosts with Docker installed", "macOS 13+" ] + }, + "report": { + "created_at": "2021-01-22T17:23:27Z", + "updated_at": "2021-01-22T17:23:27Z", + "id": 288, + "name": "new_title_for_my_query", + "description": "This is a new query.", + "query": "SELECT * FROM osquery_info", + "team_id": null, + "interval": 3600, + "platform": "", + "min_osquery_version": "", + "automations_enabled": false, + "logging": "snapshot", + "saved": true, + "author_id": 1, + "author_name": "noah", + "observer_can_run": true, + "discard_data": true, + "packs": [], + "labels_include_any": [ + "Hosts with Docker installed", + "macOS 13+" + ] } } ``` -### Delete query by name +### Delete report by name -Deletes the query specified by name. +Deletes the report specified by name. -`DELETE /api/v1/fleet/queries/:name` +`DELETE /api/v1/fleet/reports/:name` #### Parameters | Name | Type | In | Description | | ---- | ---------- | ---- | ------------------------------------ | -| name | string | path | **Required.** The name of the query. | -| team_id | integer | body | _Available in Fleet Premium_. The ID of the parent team of the query to be deleted. If omitted, Fleet will search among queries in the global context. | +| name | string | path | **Required.** The name of the report. | +| fleet_id | integer | body | _Available in Fleet Premium_. The ID of the report's fleet. If omitted, Fleet will search among only global reports. | #### Example -`DELETE /api/v1/fleet/queries/foo` +`DELETE /api/v1/fleet/reports/foo` ##### Default response `Status: 200` -### Delete query by ID +### Delete report by ID -Deletes the query specified by ID. +Deletes the report specified by ID. -`DELETE /api/v1/fleet/queries/id/:id` +`DELETE /api/v1/fleet/reports/id/:id` #### Parameters | Name | Type | In | Description | | ---- | ------- | ---- | ---------------------------------- | -| id | integer | path | **Required.** The ID of the query. | +| id | integer | path | **Required.** The ID of the report. | #### Example -`DELETE /api/v1/fleet/queries/id/28` +`DELETE /api/v1/fleet/reports/id/28` ##### Default response `Status: 200` -### Delete queries +### Delete reports -Deletes the queries specified by ID. Returns the count of queries successfully deleted. +Deletes the reports specified by ID. Returns the count of reports successfully deleted. -`POST /api/v1/fleet/queries/delete` +`POST /api/v1/fleet/reports/delete` #### Parameters | Name | Type | In | Description | | ---- | ----- | ---- | ------------------------------------- | -| ids | array | body | **Required.** The IDs of the queries. | +| ids | array | body | **Required.** The IDs of the reports. | #### Example -`POST /api/v1/fleet/queries/delete` +`POST /api/v1/fleet/reports/delete` ##### Request body @@ -9157,27 +9321,27 @@ Deletes the queries specified by ID. Returns the count of queries successfully d } ``` -### Run live query +### Run live report > This updated API endpoint replaced `GET /api/v1/fleet/queries/run` in Fleet 4.43.0, for improved compatibility with many HTTP clients. The [deprecated endpoint](https://github.com/fleetdm/fleet/blob/fleet-v4.42.0/docs/REST%20API/rest-api.md#run-live-query) is maintained for backwards compatibility. -Runs a live query against the specified hosts and responds with the results. +Runs a live report against the specified hosts and responds with the results. -The live query will stop if the request times out. Timeouts happen if targeted hosts haven't responded after the configured `FLEET_LIVE_QUERY_REST_PERIOD` (default 25 seconds) or if the `distributed_interval` agent option (default 10 seconds) is higher than the `FLEET_LIVE_QUERY_REST_PERIOD`. +The live report will stop if the request times out. Timeouts happen if targeted hosts haven't responded after the configured `FLEET_LIVE_QUERY_REST_PERIOD` (default 25 seconds) or if the `distributed_interval` agent option (default 10 seconds) is higher than the `FLEET_LIVE_QUERY_REST_PERIOD`. -`POST /api/v1/fleet/queries/:id/run` +`POST /api/v1/fleet/reports/:id/run` #### Parameters | Name | Type | In | Description | |-----------|-------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| query_id | integer | path | **Required**. The ID of the saved query to run. | +| report_id | integer | path | **Required**. The ID of the saved report to run. | | host_ids | array | body | **Required**. The IDs of the hosts to target. User must be authorized to target all of these hosts. | #### Example -`POST /api/v1/fleet/queries/123/run` +`POST /api/v1/fleet/reports/123/run` ##### Request body @@ -9192,6 +9356,7 @@ The live query will stop if the request times out. Timeouts happen if targeted h ```json { "query_id": 123, + "report_id": 123, "targeted_host_count": 4, "responded_host_count": 2, "results": [ @@ -9230,7 +9395,7 @@ The live query will stop if the request times out. Timeouts happen if targeted h The [schedule API endpoints](https://github.com/fleetdm/fleet/blob/f6631e27f56b6704c555adfb7a3bb8c6d1a74d98/docs/REST%20API/rest-api.md#schedule) are deprecated as of Fleet 4.35. They are maintained for backwards compatibility. -Please use the [queries](#queries) endpoints, which as of 4.35 have attributes such as `interval` and `platform` that enable scheduling. +Please use the [reports](#reports) endpoints, which as of 4.35 have attributes such as `interval` and `platform` that enable scheduling. --- @@ -9268,9 +9433,9 @@ By default, script runs time out after 5 minutes. You can modify this default in | ---- | ------- | ---- | -------------------------------------------- | | host_id | integer | body | **Required**. The ID of the host to run the script on. | | script_id | integer | body | The ID of the existing saved script to run. Only one of either `script_id`, `script_contents`, or `script_name` can be included. | -| script_contents | string | body | The contents of the script to run. Only one of either `script_id`, `script_contents`, or `script_name` can be included. Scripts must be less than 10,000 characters. To run scripts with more than 10k characters, save the script and use `script_id` or `script_name` and `team_id` instead. | -| script_name | integer | body | The name of the existing saved script to run. If specified, requires `team_id`. Only one of either `script_id`, `script_contents`, or `script_name` can be included in the request. | -| team_id | integer | body | The ID of the existing saved script to run. If specified, requires `script_name`. Only one of either `script_id`, `script_contents`, or `script_name` can be included in the request. | +| script_contents | string | body | The contents of the script to run. Only one of either `script_id`, `script_contents`, or `script_name` can be included. Scripts must be less than 10,000 characters. To run scripts with more than 10k characters, save the script and use `script_id` or `script_name` and `fleet_id` instead. | +| script_name | integer | body | The name of the existing saved script to run. If specified, requires `fleet_id`. Only one of either `script_id`, `script_contents`, or `script_name` can be included in the request. | +| fleet_id | integer | body | The ID of the fleet the existing saved script belongs to. If specified, requires `script_name`. Only one of either `script_id`, `script_contents`, or `script_name` can be included in the request. | > Note that if any combination of `script_id`, `script_contents`, and `script_name` are included in the request, this endpoint will respond with an error. @@ -9352,7 +9517,7 @@ The script will be added to each host's list of upcoming activities. | query | string | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. | | status | string | Host status. Can either be `new`, `online`, `offline`, `mia` or `missing`. | | label_id | number | ID of a label to filter by. | -| team_id | number | ID of the team to filter by. | +| fleet_id | number | ID of the fleet to filter by. | > Note that if a batch script is scheduled for the future using `not_before`, and hosts are targeted using `filters`, the script will run on any hosts matching the filters _at the time the batch script was added_. To see all targeted hosts, use the [List hosts targeted in batch script](#list-hosts-targeted-in-batch-script) endpoint. @@ -9405,7 +9570,7 @@ Returns a list of batch script executions. | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. Filters to batch script runs for the specified team. | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters to batch script runs for the specified fleet. | | status | string | query | Filters to batch script runs with this status. Either `"started"`, `"scheduled"`, or `"finished"`. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | @@ -9420,6 +9585,7 @@ Returns a list of batch script executions. ```json { "team_id": 123, + "fleet_id": 123, "status": "completed" } ``` @@ -9436,6 +9602,7 @@ Returns a list of batch script executions. "script_name": "my-script.sh", "batch_execution_id": "e797d6c6-3aae-11ee-be56-0242ac120002", "team_id": 123, + "fleet_id": 123, "not_before": "2025-07-01T15:00:00Z", "finished_at": "2025-07-06T15:00:00Z", "started_at": "2025-07-06T14:00:00Z", @@ -9489,6 +9656,7 @@ Returns a summary of a batch-run script, including host counts and current statu "script_id": 555, "script_name": "my-script.sh", "team_id": 123, + "fleet_id": 123, "not_before": "2025-07-01T15:00:00Z", "finished_at": "2025-07-06T15:00:00Z", "started_at": "2025-07-06T14:00:00Z", @@ -9555,7 +9723,7 @@ Returns a list hosts targeted in a batch script run, along with their script exe ### Create script -Uploads a script, making it available to run on hosts assigned to the specified team (or no team). +Uploads a script, making it available to run on hosts assigned to the specified fleet (or "Unassigned"). > You need to send a request of type `multipart/form-data`. @@ -9568,7 +9736,7 @@ Uploads a script, making it available to run on hosts assigned to the specified | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | script | file | body | **Required**. The file containing the script. | -| team_id | integer | body | _Available in Fleet Premium_. The team ID. If specified, the script will only be available to hosts assigned to this team. If not specified, the script will only be available to hosts on **no team**. | +| fleet_id | integer | body | _Available in Fleet Premium_. The fleet ID. If specified, the script will only be available to hosts assigned to this fleet. If not specified, the script will only be available for "Unassigned" hosts. | Script line endings are automatically converted from [CRLF to LF](https://en.wikipedia.org/wiki/Newline) for compatibility with both non-Windows shells and PowerShell. @@ -9579,7 +9747,7 @@ Script line endings are automatically converted from [CRLF to LF](https://en.wik ##### Request body ```http -team_id="1" +fleet_id="1" script="myscript.sh" ``` @@ -9661,7 +9829,7 @@ Deletes an existing script. | Name | Type | In | Description | | --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | -| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to filter scripts by. If not specified, it will filter only scripts that are available to hosts with no team. | +| fleet_id | integer | query | _Available in Fleet Premium_. The ID of the fleet to filter scripts by. If not specified, it will filter only scripts that are available for "Unassigned" hosts. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | @@ -9904,7 +10072,7 @@ Get a list of all software. | order_key | string | query | What to order results by. Allowed fields are `name` and `hosts_count`. Default is `hosts_count` (descending). | | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | | query | string | query | Search query keywords. Searchable fields include `title` and `cve`. | -| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified fleet. Use `0` to filter by "Unassigned" hosts. | | vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | | available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | | self_service | boolean | query | If `true` or `1`, only lists self-service software. Default is `false`. | @@ -9912,15 +10080,15 @@ Get a list of all software. | min_cvss_score | integer | query | _Available in Fleet Premium_. Filters to include only software with vulnerabilities that have a CVSS version 3.x base score higher than the specified value. | | max_cvss_score | integer | query | _Available in Fleet Premium_. Filters to only include software with vulnerabilities that have a CVSS version 3.x base score lower than what's specified. | | exploit | boolean | query | _Available in Fleet Premium_. If `true`, filters to only include software with vulnerabilities that have been actively exploited in the wild (`cisa_known_exploit: true`). Default is `false`. | -| platform | string | query | Filters software titles available for install by platforms. `team_id` must be specified to filter by platform. Options are: `"macos"` (alias of `"darwin"`), `"darwin"` `"windows"`, `"linux"`, `"chrome"`, `"ios"`, `"ipados"`. To show titles from multiple platforms, separate the platforms with commas (e.g. `?platform=darwin,windows`). | -| hash_sha256 | string | query | Filters to only include custom software packages (uploaded installers) with the specified SHA-256 hash. `team_id` must be specified to filter by hash. This allows checking if a specific package already exists before uploading. | -| package_name | string | query | Filters to only include custom software packages (uploaded installers) with the specified package filename. `team_id` must be specified to filter by package name. This allows checking if a specific package already exists before uploading. | +| platform | string | query | Filters software titles available for install by platforms. `fleet_id` must be specified to filter by platform. Options are: `"macos"` (alias of `"darwin"`), `"darwin"` `"windows"`, `"linux"`, `"chrome"`, `"ios"`, `"ipados"`. To show titles from multiple platforms, separate the platforms with commas (e.g. `?platform=darwin,windows`). | +| hash_sha256 | string | query | Filters to only include custom software packages (uploaded installers) with the specified SHA-256 hash. `fleet_id` must be specified to filter by hash. This allows checking if a specific package already exists before uploading. | +| package_name | string | query | Filters to only include custom software packages (uploaded installers) with the specified package filename. `fleet_id` must be specified to filter by package name. This allows checking if a specific package already exists before uploading. | | exclude_fleet_maintained_apps | boolean | query | If `true` or `1`, Fleet maintained apps will not be included in the list of `software_titles`. Default is `false` | #### Example -`GET /api/v1/fleet/software/titles?team_id=3&platform=darwin,windows` +`GET /api/v1/fleet/software/titles?fleet_id=3&platform=darwin,windows` ##### Default response @@ -9935,7 +10103,7 @@ Get a list of all software. "id": 12, "name": "Firefox.app", "display_name": "Firefox", - "icon_url":"/api/latest/fleet/software/titles/12/icon?team_id=3", + "icon_url":"/api/latest/fleet/software/titles/12/icon?fleet_id=3", "display_name": "", "software_package": { "platform": "darwin", @@ -10102,7 +10270,7 @@ Get a list of all software versions. | order_key | string | query | What to order results by. Allowed fields are `name`, `hosts_count`, `cve_published`, `cvss_score`, `epss_probability` and `cisa_known_exploit`. Default is `hosts_count` (descending). | | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | | query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. | -| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified fleet. Use `0` to filter by "Unassigned" hosts. | | vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | | min_cvss_score | integer | query | _Available in Fleet Premium_. Filters to include only software with vulnerabilities that have a CVSS version 3.x base score higher than the specified value. | | max_cvss_score | integer | query | _Available in Fleet Premium_. Filters to only include software with vulnerabilities that have a CVSS version 3.x base score lower than what's specified. | @@ -10191,7 +10359,7 @@ Returns a list of all operating systems. | Name | Type | In | Description | | --- | --- | --- | --- | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified fleet. Use `0` to filter by "Unassigned" hosts. | | platform | string | query | Filters the hosts to the specified platform | | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | | os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` | @@ -10256,11 +10424,11 @@ Returns information about the specified software. By default, `versions` are sor | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | id | integer | path | **Required.** The software title's ID. | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified fleet. Use `0` to filter by "Unassigned" hosts. | #### Example -`GET /api/v1/fleet/software/titles/12?team_id=3` +`GET /api/v1/fleet/software/titles/12?fleet_id=3` ##### Default response @@ -10352,7 +10520,7 @@ Returns information about the specified software. By default, `versions` are sor #### Example (app store app) -`GET /api/v1/fleet/software/titles/15?team_id=3` +`GET /api/v1/fleet/software/titles/15?fleet_id=3` ##### Default response @@ -10364,7 +10532,7 @@ Returns information about the specified software. By default, `versions` are sor "id": 15, "name": "Logic Pro", "display_name": "", - "icon_url": "/api/latest/fleet/software/titles/15/icon?team_id=3", + "icon_url": "/api/latest/fleet/software/titles/15/icon?fleet_id=3", "display_name": "", "bundle_identifier": "com.apple.logic10", "software_package": null, @@ -10405,7 +10573,7 @@ Returns information about the specified software. By default, `versions` are sor } ``` -`auto_update_enabled`, `auto_update_window_start` and `auto_update_window_end` will only be returned for iOS/iPadOS apps, and only when a `team_id` is specified in the request. +`auto_update_enabled`, `auto_update_window_start` and `auto_update_window_end` will only be returned for iOS/iPadOS apps, and only when a `fleet_id` is specified in the request. #### Example (Play Store app) @@ -10460,7 +10628,7 @@ Returns information about the specified software. By default, `versions` are sor #### Example (in-house iOS app) -`GET /api/v1/fleet/software/titles/24?team_id=3` +`GET /api/v1/fleet/software/titles/24?fleet_id=3` ##### Default response @@ -10523,7 +10691,7 @@ Returns information about the specified software version. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | id | integer | path | **Required.** The software version's ID. | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified fleet. Use `0` to filter by "Unassigned" hosts. | #### Example @@ -10583,7 +10751,7 @@ Retrieves information about the specified operating system (OS) version. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | id | integer | path | **Required.** The OS version's ID. | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified fleet. Use `0` to filter by "Unassigned" hosts. | | max_vulnerabilities | integer | query | Limits the number of `vulnerabilities` returned. (If omitted, returns all vulnerabilities.) For Linux OS's, doesn't limit the number of vulnerabilities returned in the `kernels` array. | ##### Default response @@ -10701,7 +10869,7 @@ Add a package (.pkg, .msi, .exe, .deb, .rpm, .tar.gz, .ipa) to install on Apple | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | software | file | body | **Required**. Installer package file or custom script file. Supported packages are `.pkg`, `.msi`, `.exe`, `.deb`, `.rpm`, `.tar.gz`, `.ipa`, `.sh`, and `.ps1`. | -| team_id | integer | body | The team ID. Adds a software package to the specified team. If not specified, it will add the software for hosts with no team. | +| fleet_id | integer | body | The fleet ID. Adds a software package to the specified fleet. If not specified, it will add the software for "Unassigned" hosts. | | install_script | string | body | Script that Fleet runs to install software. If not specified Fleet runs the [default install script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) for each package type if one exists. Required for `.tar.gz` and `.exe` (no default script). Not supported for `.sh` and `.ps1`. | | uninstall_script | string | body | Script that Fleet runs to uninstall software. If not specified Fleet runs the [default uninstall script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) for each package type if one exists. Required for `.tar.gz` and `.exe` (no default script). Not supported for `.sh` and `.ps1`. | | pre_install_query | string | body | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. Not supported for `.sh` and `.ps1`. | @@ -10724,7 +10892,7 @@ POST /api/v1/fleet/software/package ##### Request body ``` -team_id="1" +fleet_id="1" self_service="true" install_script="sudo installer -pkg /temp/FalconSensor-6.44.pkg -target /" pre_install_query"SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';" @@ -10790,7 +10958,7 @@ Update a package to install on macOS, Windows, Linux, iOS, or iPadOS hosts. | ---- | ------- | ---- | -------------------------------------------- | | id | integer | path | ID of the software title being updated. | | software | file | body | Installer package file or custom script file. Supported packages are `.pkg`, `.msi`, `.exe`, `.deb`, `.rpm`, `.tar.gz`, `.ipa`, `.sh`, and `.ps1`. | -| team_id | integer | body | **Required**. The team ID. Updates a software package in the specified team. | +| fleet_id | integer | body | **Required**. The fleet ID. Updates a software package in the specified fleet. | | display_name | string | body | Optional override for the default `name`. | | categories | array | body | Zero or more of the [supported categories](https://fleetdm.com/docs/configuration/yaml-files#supported-software-categories), used to group self-service software on your end users' **Fleet Desktop > My device** page. Software with no categories will be still be shown under **All**. | | install_script | string | body | Command that Fleet runs to install software. If not specified Fleet runs the [default install command](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) for each package type. Not supported for `.sh` and `.ps1`. | @@ -10813,7 +10981,7 @@ Add the `X-Fleet-Scripts-Encoded: base64` header line to parse `install_script`, ##### Request body ```http -team_id="1" +fleet_id="1" software="FalconSensor-6.44.pkg" self_service="true" display_name="CrowdStrike agent" @@ -10870,7 +11038,7 @@ Icon will be displayed in Fleet and on **Fleet Desktop > Self-service**. In the | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | id | integer | path | ID of the software title being updated. | -| team_id | integer | query | **Required**. The team ID. Updates a software icon in the specified team. | +| fleet_id | integer | query | **Required**. The fleet ID. Updates a software icon in the specified fleet. | | icon | file | body | Must be PNG format. It must be square with dimensions between 120x120 px and 1024x1024 px. | | hash_sha256 | string | body | SHA256 hash of an already-uploaded icon to use. If provided, `filename` is required and `icon` should be omitted. | | filename | string | body | Filename to record for the icon image, if `hash_sha256` was supplied. | @@ -10908,7 +11076,7 @@ Download the icon added via [Update software icon](#update-software-icon) or ico | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | id | integer | path | ID of the software title to get icon for. | -| team_id | integer | query | **Required**. The team ID. | +| fleet_id | integer | query | **Required**. The fleet ID. | This endpoint will redirect (302) to the Apple-hosted URL of an icon if an icon override isn't set and a VPP app is added for the title on the host's team. @@ -10958,7 +11126,7 @@ Delete a custom icon added via [Update software icon](#update-software-icon). Th > **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. -Returns the list of Apple App Store (VPP) apps that can be added to the specified team. If an app is already added to the team, it's excluded from the list. +Returns the list of Apple App Store (VPP) apps that can be added to the specified fleet. If an app is already added to the fleet, it's excluded from the list. `GET /api/v1/fleet/software/app_store_apps` @@ -10966,11 +11134,11 @@ Returns the list of Apple App Store (VPP) apps that can be added to the specifie | Name | Type | In | Description | | ------- | ---- | -- | ----------- | -| team_id | integer | query | **Required**. The team ID. | +| fleet_id | integer | query | **Required**. The fleet ID. | #### Example -`GET /api/v1/fleet/software/app_store_apps/?team_id=3` +`GET /api/v1/fleet/software/app_store_apps/?fleet_id=3` ##### Default response @@ -11022,7 +11190,7 @@ Add Apple App Store or Google Play store app. Apple apps must be added in Apple | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | app_store_id | string | body | **Required.** The ID of the Apple App Store app or Google Play app. | -| team_id | integer | body | **Required**. The team ID. Adds app from the store to the specified team. | +| fleet_id | integer | body | **Required**. The fleet ID. Adds app from the store to the specified fleet. | | platform | string | body | The platform of the app (`darwin`, `ios`, `ipados`, or `android`). Default is `darwin`. | | self_service | boolean | body | **Required if platform is Android**. Currently supported for macOS and Android apps. Specifies whether the app shows up in self-service and is available for install by the end user. For macOS shows up on **Fleet Desktop > My device** page, for Android in **Play Store** app in end user's work profile, and for iOS/iPadOS in [self-service web](https://fleetdm.com/learn-more-about/deploy-self-service-to-ios) app. | | ensure | string | form | For macOS only, if set to "present" (currently the only valid value if set), create a policy that triggers a software install only on hosts missing the software. | @@ -11084,7 +11252,7 @@ Modify an Apple App Store (VPP) or a Google Play app's options. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | -| team_id | integer | body | **Required**. The team ID. Edits Apple App Store or Android Play store app from the specified team. | +| fleet_id | integer | body | **Required**. The fleet ID. Edits Apple App Store or Android Play store app from the specified fleet. | | display_name | string | body | Optional override for the default `name`. | | self_service | boolean | body | **Required if platform is Android**. Currently supported for macOS and Android apps. Specifies whether the app shows up in self-service and is available for install by the end user. For macOS shows up on **Fleet Desktop > My device** page, and for Android in **Play Store** app in end user's work profile. | | categories | array | body | Zero or more of the [supported categories](https://fleetdm.com/docs/configuration/yaml-files#supported-software-categories), used to group self-service software on your end users' **Fleet Desktop > My device** page. Software with no categories will be still be shown under **All**. | @@ -11169,13 +11337,13 @@ List available Fleet-maintained apps. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | -| team_id | integer | query | If specified, each app includes the `software_title_id` if the software has already been added to that team. | +| fleet_id | integer | query | If specified, each app includes the `software_title_id` if the software has already been added to that fleet. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | #### Example -`GET /api/v1/fleet/software/fleet_maintained_apps?team_id=3` +`GET /api/v1/fleet/software/fleet_maintained_apps?fleet_id=3` ##### Default response @@ -11230,7 +11398,7 @@ Returns information about the specified Fleet-maintained app. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | id | integer | path | **Required.** The Fleet-maintained app's ID. | -| team_id | integer | query | If supplied, set `software_title_id` on the response when an installer or VPP app has already been added to that team for that software. | +| fleet_id | integer | query | If supplied, set `software_title_id` on the response when an installer or VPP app has already been added to that fleet for that software. | #### Example @@ -11273,7 +11441,7 @@ Add Fleet-maintained app so it's available for install. | Name | Type | In | Description | | ---- | ---- | -- | ----------- | | fleet_maintained_app_id | integer | body | **Required.** The ID of Fleet-maintained app. | -| team_id | integer | body | **Required**. The team ID. Adds Fleet-maintained app to the specified team. | +| fleet_id | integer | body | **Required**. The fleet ID. Adds Fleet-maintained app to the specified fleet. | | install_script | string | body | Command that Fleet runs to install software. If not specified Fleet runs default install command for each Fleet-maintained app. | | pre_install_query | string | body | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. | | post_install_script | string | body | The contents of the script to run after install. If the specified script fails (exit code non-zero) software install will be marked as failed and rolled back. | @@ -11323,7 +11491,7 @@ _Available in Fleet Premium._ | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | id | integer | path | **Required**. The ID of the software title to download software package.| -| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. | +| fleet_id | integer | query | **Required**. The fleet ID. Downloads a software package added to the specified fleet. | | alt | string | query | **Required**. If specified and set to `"media"`, downloads the specified software package. | #### Example @@ -11445,7 +11613,7 @@ _Available in Fleet Premium._ | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | software_title_id | integer | path | **Required**. The ID of the software title to download software package.| -| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. | +| fleet_id | integer | query | **Required**. The fleet ID. Downloads a software package added to the specified fleet. | | alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. | #### Example @@ -11479,7 +11647,7 @@ Deletes software that's available for install. This won't uninstall the software | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | | software_title_id | integer | path | **Required**. The ID of the software title to delete software available for install. | -| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. | +| fleet_id | integer | query | **Required**. The fleet ID. Deletes a software package added to the specified fleet. | #### Example @@ -11504,7 +11672,7 @@ Retrieves a list of all CVEs affecting software and/or OS versions. | Name | Type | In | Description | | --- | --- | --- | --- | -| team_id | integer | query | _Available in Fleet Premium_. Filters only include vulnerabilities affecting the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters only include vulnerabilities affecting the specified fleet. Use `0` to filter by "Unassigned" hosts. | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | | order_key | string | query | What to order results by. Allowed fields are: `cve`, `cvss_score`, `epss_probability`, `cve_published`, `created_at`, and `host_count`. Default is `created_at` (descending). | @@ -11554,7 +11722,7 @@ If no vulnerable OS versions or software were found, but Fleet is aware of the v | Name | Type | In | Description | |---------|---------|-------|------------------------------------------------------------------------------------------------------------------------------| | cve | string | path | The cve to get information about (format must be CVE-YYYY-<4 or more digits>, case-insensitive). | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified fleet. Use `0` to filter by "Unassigned" hosts. | `GET /api/v1/fleet/vulnerabilities/:cve` @@ -11613,7 +11781,7 @@ The `extension_for` field is included when set and when empty, at the same level ## Targets -In Fleet, targets are used to run queries against specific hosts or groups of hosts. Labels are used to create groups in Fleet. +In Fleet, targets are used to run reports against specific hosts or groups of hosts. Labels are used to create groups in Fleet. ### Search targets @@ -11629,7 +11797,7 @@ The returned lists are filtered based on the hosts the requesting user has acces | -------- | ------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | query | string | body | The search query. Searchable items include a host's hostname or IPv4 address and labels. | | query_id | integer | body | The saved query (if any) that will be run. The `observer_can_run` property on the query and the user's roles effect which targets are included. | -| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs, a `labels` with label IDs and/or a `teams` property with team IDs. | +| selected | object | body | The targets already selected. The object includes a `hosts` property which contains a list of host IDs, a `labels` with label IDs and/or a `fleets` property with fleet IDs. | #### Example @@ -11752,7 +11920,7 @@ The returned lists are filtered based on the hosts the requesting user has acces "count": 5 } ], - "teams": [ + "fleets": [ { "id": 1, "created_at": "2021-05-27T20:02:20Z", @@ -11775,21 +11943,21 @@ The returned lists are filtered based on the hosts the requesting user has acces --- -## Teams +## Fleets -- [List teams](#list-teams) -- [Get team](#get-team) -- [Create team](#create-team) -- [Update team](#update-team) -- [Add users to team](#add-users-to-team) -- [Update team's agent options](#update-teams-agent-options) -- [Delete team](#delete-team) +- [List fleets](#list-fleets) +- [Get fleet](#get-fleet) +- [Create fleet](#create-fleet) +- [Update fleet](#update-fleet) +- [Add users to fleet](#add-users-to-fleet) +- [Update fleet's agent options](#update-fleets-agent-options) +- [Delete fleet](#delete-fleet) -### List teams +### List fleets _Available in Fleet Premium_ -`GET /api/v1/fleet/teams` +`GET /api/v1/fleet/fleets` #### Parameters @@ -11797,13 +11965,13 @@ _Available in Fleet Premium_ | --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- | | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | -| order_key | string | query | What to order results by. Can be any column in the `teams` table. | +| order_key | string | query | What to order results by. Can be any column in the `fleets` table. | | order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `"asc"` and `"desc"`. Default is `"asc"`. | | query | string | query | Search query keywords. Searchable fields include `name`. | #### Example -`GET /api/v1/fleet/teams` +`GET /api/v1/fleet/fleets` ##### Default response @@ -11811,7 +11979,7 @@ _Available in Fleet Premium_ ```json { - "teams": [ + "fleets": [ { "id": 1, "created_at": "2021-07-28T15:58:21Z", @@ -11890,25 +12058,25 @@ _Available in Fleet Premium_ } ``` -### Get team +### Get fleet _Available in Fleet Premium_ -`GET /api/v1/fleet/teams/:id` +`GET /api/v1/fleet/fleets/:id` `mdm.macos_settings.custom_settings`, `mdm.windows_settings.custom_settings`, `scripts`, and `mdm.macos_setup` only include the configuration profiles, scripts, and setup experience settings applied using [Fleet's YAML](https://fleetdm.com/docs/configuration/yaml-files). To list profiles, scripts, or setup experience settings added in the UI or API, use the [List configuration profiles](https://fleetdm.com/docs/rest-api/rest-api#list-custom-os-settings-configuration-profiles), [List scripts](https://fleetdm.com/docs/rest-api/rest-api#list-scripts), or GET endpoints from [Setup experience](https://fleetdm.com/docs/rest-api/rest-api#setup-experience) instead. -"No team" will only return `id`, `name`, `webhook_settings.failing_policies_webhook`, `integrations.jira`, and `integrations.zendesk` fields. +"Unassigned" (id 0) will only return `id`, `name`, `webhook_settings.failing_policies_webhook`, `integrations.jira`, and `integrations.zendesk` fields. #### Parameters | Name | Type | In | Description | |------|---------|------|----------------------------------------------------------------------------------------------| -| id | integer | path | **Required.** The desired team's ID. Use `0` for "No team" (hosts not assigned to any team). | +| id | integer | path | **Required.** The desired fleet's ID. Use `0` for "Unassigned" hosts. | #### Example -`GET /api/v1/fleet/teams/1` +`GET /api/v1/fleet/fleets/1` ##### Default response @@ -11996,21 +12164,21 @@ _Available in Fleet Premium_ } ``` -### Create team +### Create fleet _Available in Fleet Premium_ -`POST /api/v1/fleet/teams` +`POST /api/v1/fleet/fleets` #### Parameters | Name | Type | In | Description | | ---- | ------ | ---- | ------------------------------ | -| name | string | body | **Required.** The team's name. | +| name | string | body | **Required.** The fleet's name. | #### Example -`POST /api/v1/fleet/teams` +`POST /api/v1/fleet/fleets` ##### Request body @@ -12064,28 +12232,28 @@ _Available in Fleet Premium_ } ``` -### Update team +### Update fleet _Available in Fleet Premium_ -`PATCH /api/v1/fleet/teams/:id` +`PATCH /api/v1/fleet/fleets/:id` #### Parameters | Name | Type | In | Description | | ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | integer | path | **Required.** The desired team's ID. Use `0` for "No team" (hosts not assigned to any team). **Note:** When using `id=0`, only `webhook_settings.failing_policies_webhook`, `integrations.jira`, and `integrations.zendesk` fields are supported in the request body. | -| name | string | body | The team's name. | -| host_ids | array | body | A list of hosts that belong to the team. | -| user_ids | array | body | A list of users on the team. | -| webhook_settings | object | body | Webhook settings for the team. See [webhook_settings](#webhook-settings2). | -| integrations | object | body | Integrations settings for the team. See [integrations](#integrations3) for details. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). | -| mdm | object | body | MDM settings for the team. See [mdm](#mdm2) for details. | -| host_expiry_settings | object | body | Host expiry settings for the team. See [host_expiry_settings](#host-expiry-settings2) for details. | +| id | integer | path | **Required.** The desired fleet's ID. Use `0` for "Unassigned" hosts. **Note:** When using `id=0`, only `webhook_settings.failing_policies_webhook`, `integrations.jira`, and `integrations.zendesk` fields are supported in the request body. | +| name | string | body | The fleet's name. | +| host_ids | array | body | A list of hosts that belong to the fleet. | +| user_ids | array | body | A list of users on the fleet. | +| webhook_settings | object | body | Webhook settings for the fleet. See [webhook_settings](#webhook-settings2). | +| integrations | object | body | Integrations settings for the fleet. See [integrations](#integrations3) for details. Note that integrations referenced here must already exist globally, created by a call to [Modify configuration](#modify-configuration). | +| mdm | object | body | MDM settings for the fleet. See [mdm](#mdm2) for details. | +| host_expiry_settings | object | body | Host expiry settings for the fleet. See [host_expiry_settings](#host-expiry-settings2) for details. | -#### Example (transfer hosts to a team) +#### Example (transfer hosts to a fleet) -`PATCH /api/v1/fleet/teams/1` +`PATCH /api/v1/fleet/fleets/1` ##### Request body @@ -12237,7 +12405,7 @@ _Available in Fleet Premium_ | Name | Type | Description | | ------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| enable_calendar_events | boolean | Whether or not calendar events are enabled for this team. | +| enable_calendar_events | boolean | Whether or not calendar events are enabled for this fleet. | | webhook_url | string | The URL to send a request to during calendar events, to trigger auto-remediation. | ##### Example request body @@ -12288,8 +12456,8 @@ _Available in Fleet Premium_ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to this team and are enrolled into Fleet's MDM will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to this team and are enrolled into Fleet's MDM will be forced to update their OS after this deadline (noon local time for hosts already on macOS 14 or above, 20:00 UTC for hosts on earlier macOS versions). | +| minimum_version | string | Hosts that belong to this fleet and have MDM turned on will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that belong to this fleet and have MDM turned on will be forced to update their OS after this deadline (7PM local time for hosts already on macOS 14 or above, 20:00 UTC for hosts on earlier macOS versions). | | update_new_hosts | string | macOS hosts that automatically enroll (ADE) are updated to [Apple's latest version](https://fleetdm.com/guides/enforce-os-updates) during macOS Setup Assistant. For backwards compatibility, if not specified, and `deadline` and `minimum_version` are set, `update_new_hosts` is set to `true`. Otherwise, `update_new_hosts` defaults to `false`. |
@@ -12300,8 +12468,8 @@ _Available in Fleet Premium_ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to this team will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to this team will be forced to update their OS after this deadline (noon local time). | +| minimum_version | string | Hosts that belong to this fleet will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that belong to this fleet will be forced to update their OS after this deadline (7PM local time). |
@@ -12312,8 +12480,8 @@ _Available in Fleet Premium_ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| minimum_version | string | Hosts that belong to this team will be prompted to update when their OS is below this version. | -| deadline | string | Hosts that belong to this team will be forced to update their OS after this deadline (noon local time). | +| minimum_version | string | Hosts that belong to this fleet will be prompted to update when their OS is below this version. | +| deadline | string | Hosts that belong to this fleet will be forced to update their OS after this deadline (7PM local time). |
@@ -12324,8 +12492,8 @@ _Available in Fleet Premium_ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| deadline_days | integer | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. | -| grace_period_days | integer | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. | +| deadline_days | integer | Hosts that belong to this fleet and have MDM turned on will have this number of days before updates are installed on Windows. | +| grace_period_days | integer | Hosts that belong to this fleet and have MDM turned on will have this number of days before Windows restarts to install updates. |
@@ -12336,7 +12504,7 @@ _Available in Fleet Premium_ | Name | Type | Description | | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| enable_disk_encryption | boolean | Hosts that belong to this team will have disk encryption enabled if set to true. | +| enable_disk_encryption | boolean | Hosts that belong to this fleet will have disk encryption enabled if set to true. | | custom_settings | array | Only intended to be used by [Fleet's YAML](https://fleetdm.com/docs/configuration/yaml-files). To add macOS configuration profiles using Fleet's API, use the [Create custom OS setting (configuration profile)](#create-custom-os-setting-configuration-profile) endpoint instead. |
@@ -12430,24 +12598,24 @@ _Available in Fleet Premium_ } ``` -### Add users to team +### Add users to fleet _Available in Fleet Premium_ -`PATCH /api/v1/fleet/teams/:id/users` +`PATCH /api/v1/fleet/fleets/:id/users` #### Parameters | Name | Type | In | Description | |------------------|---------|------|----------------------------------------------| -| id | integer | path | **Required.** The desired team's ID. | +| id | integer | path | **Required.** The desired fleet's ID. | | users | string | body | Array of users to add. | |   id | integer | body | The id of the user. | -|   role | string | body | The team role that the user will be granted. Options are: "admin", "maintainer", "observer", "observer_plus", and "gitops". | +|   role | string | body | The fleet role that the user will be granted. Options are: "admin", "maintainer", "observer", "observer_plus", and "gitops". | #### Example -`PATCH /api/v1/fleet/teams/1/users` +`PATCH /api/v1/fleet/fleets/1/users` ##### Request body @@ -12574,24 +12742,24 @@ _Available in Fleet Premium_ } ``` -### Update team's agent options +### Update fleet's agent options _Available in Fleet Premium_ -`POST /api/v1/fleet/teams/:id/agent_options` +`POST /api/v1/fleet/fleets/:id/agent_options` #### Parameters | Name | Type | In | Description | | --- | --- | --- | --- | -| id | integer | path | **Required.** The desired team's ID. | +| id | integer | path | **Required.** The desired fleet's ID. | | force | boolean | query | Force apply the options even if there are validation errors. | | dry_run | boolean | query | Validate the options and return any validation errors, but do not apply the changes. | -| _JSON data_ | object | body | The JSON to use as agent options for this team. See [Agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) for details. | +| _JSON data_ | object | body | The JSON to use as agent options for this fleet. See [Agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) for details. | #### Example -`POST /api/v1/fleet/teams/1/agent_options` +`POST /api/v1/fleet/fleets/1/agent_options` ##### Request body @@ -12663,21 +12831,21 @@ _Available in Fleet Premium_ } ``` -### Delete team +### Delete fleet _Available in Fleet Premium_ -`DELETE /api/v1/fleet/teams/:id` +`DELETE /api/v1/fleet/fleets/:id` #### Parameters | Name | Type | In | Description | | ---- | ------ | ---- | ------------------------------------ | -| id | integer | path | **Required.** The desired team's ID. | +| id | integer | path | **Required.** The desired fleet's ID. | #### Example -`DELETE /api/v1/fleet/teams/1` +`DELETE /api/v1/fleet/fleets/1` #### Default response @@ -12691,7 +12859,7 @@ _Available in Fleet Premium_ ### Translate IDs -Transforms a host name into a host id. For example, the Fleet UI use this endpoint when sending live queries to a set of hosts. +Transforms a host name into a host id. For example, the Fleet UI uses this endpoint when sending live reports to a set of hosts. `POST /api/v1/fleet/translate` @@ -12813,7 +12981,7 @@ Returns a list of all enabled users | page | integer | query | Page number of the results to fetch. | | query | string | query | Search query keywords. Searchable fields include `name` and `email`. | | per_page | integer | query | Results per page. | -| team_id | integer | query | _Available in Fleet Premium_. Filters the users to only include users in the specified team. | +| fleet_id | integer | query | _Available in Fleet Premium_. Filters the users to only include users in the specified fleet. | #### Example @@ -12842,7 +13010,7 @@ None. "mfa_enabled": false, "global_role": null, "api_only": false, - "teams": [ + "fleets": [ { "id": 1, "created_at": "0001-01-01T00:00:00Z", @@ -12889,9 +13057,9 @@ By default, the user will be forced to reset its password upon first login. | sso_enabled | boolean | body | Whether or not SSO is enabled for the user. | | mfa_enabled | boolean | body | _Available in Fleet Premium._ Whether or not the user must click a magic link emailed to them to log in, after they successfully enter their username and password. Incompatible with SSO and API-only users. | | api_only | boolean | body | User is an "API-only" user (cannot use web UI) if true. | -| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `global_role` is specified, `teams` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). | +| global_role | string | body | The role assigned to the user. If `global_role` is specified, `fleets` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). | | admin_forced_password_reset | boolean | body | Sets whether the user will be forced to reset its password upon first login (default=true) | -| teams | array | body | _Available in Fleet Premium_. The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). | +| fleets | array | body | _Available in Fleet Premium_. The fleets and respective roles assigned to the user. Should contain an array of objects in which each object includes the fleet's `id` and the user's `role` on each fleet. If `fleets` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). | #### Example @@ -12905,7 +13073,7 @@ By default, the user will be forced to reset its password upon first login. "email": "janedoe@example.com", "password": "test-123", "api_only": true, - "teams": [ + "fleets": [ { "id": 2, "role": "observer" @@ -12937,7 +13105,7 @@ By default, the user will be forced to reset its password upon first login. "mfa_enabled": false, "api_only": true, "global_role": null, - "teams": [ + "fleets": [ { "id": 2, "role": "observer" @@ -13020,7 +13188,7 @@ Creates a user account after an invited user provides registration information a "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [] + "fleets": [] } } ``` @@ -13110,7 +13278,7 @@ Returns all information about a specific user. "mfa_enabled": false, "global_role": "admin", "api_only": false, - "teams": [] + "fleets": [] } } ``` @@ -13148,8 +13316,8 @@ Returns all information about a specific user. | api_only | boolean | body | User is an "API-only" user (cannot use web UI) if true. | | password | string | body | The user's current password, required to change the user's own email or password (not required for an admin to modify another user). | | new_password| string | body | The user's new password. | -| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `global_role` is specified, `teams` cannot be specified. | -| teams | array | body | _Available in Fleet Premium_. The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `teams` is specified, `global_role` cannot be specified. | +| global_role | string | body | The role assigned to the user. If `global_role` is specified, `fleets` cannot be specified. | +| fleets | array | body | _Available in Fleet Premium_. The fleets and respective roles assigned to the user. Should contain an array of objects in which each object includes the fleet's `id` and the user's `role` on each fleet. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `fleets` is specified, `global_role` cannot be specified. | #### Example @@ -13182,12 +13350,12 @@ Returns all information about a specific user. "sso_enabled": false, "mfa_enabled": false, "api_only": false, - "teams": [] + "fleets": [] } } ``` -#### Example (modify a user's teams) +#### Example (modify a user's fleets) `PATCH /api/v1/fleet/users/2` @@ -13195,7 +13363,7 @@ Returns all information about a specific user. ```json { - "teams": [ + "fleets": [ { "id": 1, "role": "observer" @@ -13226,7 +13394,7 @@ Returns all information about a specific user. "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [ + "fleets": [ { "id": 2, "role": "observer" @@ -13303,7 +13471,7 @@ The selected user is logged out of Fleet and required to reset their password du "mfa_enabled": false, "sso_enabled": false, "global_role": "observer", - "teams": [] + "fleets": [] } } ``` @@ -13376,12 +13544,12 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token | Name | Type | In | Description | | ----------- | ------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| global_role | string | body | Role the user will be granted. Either a global role is needed, or a team role. | +| global_role | string | body | Role the user will be granted. Either a global role is needed, or a fleet role. | | email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. | | name | string | body | **Required.** The name of the invited user. | | sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. | | mfa_enabled | boolean | body | _Available in Fleet Premium._ Whether or not the invited user must click a magic link emailed to them to log in, after they successfully enter their username and password. Users can have SSO or MFA enabled, but not both. | -| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| fleets | array | body | _Available in Fleet Premium_. A list of the fleets the user is a member of. Each item includes the fleet's ID and the user's role in the specified fleet. | #### Example @@ -13396,7 +13564,7 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token "sso_enabled": false, "mfa_enabled": false, "global_role": null, - "teams": [ + "fleets": [ { "id": 2, "role": "observer" @@ -13424,7 +13592,7 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token "name": "John", "sso_enabled": false, "mfa_enabled": false, - "teams": [ + "fleets": [ { "id": 10, "created_at": "0001-01-01T00:00:00Z", @@ -13484,7 +13652,7 @@ Returns a list of the active invitations in Fleet. "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [] + "fleets": [] }, { "created_at": "0001-01-01T00:00:00Z", @@ -13495,7 +13663,7 @@ Returns a list of the active invitations in Fleet. "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [] + "fleets": [] } ] } @@ -13553,7 +13721,7 @@ Verify the specified invite. "sso_enabled": false, "mfa_enabled": false, "global_role": "admin", - "teams": [] + "fleets": [] } } ``` @@ -13582,12 +13750,12 @@ Verify the specified invite. | Name | Type | In | Description | | ----------- | ------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| global_role | string | body | Role the user will be granted. Either a global role is needed, or a team role. | +| global_role | string | body | Role the user will be granted. Either a global role is needed, or a fleet role. | | email | string | body | The email of the invited user. Updates on the email won't resend the invitation. | | name | string | body | The name of the invited user. | | sso_enabled | boolean | body | Whether or not SSO will be enabled for the invited user. | | mfa_enabled | boolean | body | _Available in Fleet Premium._ Whether or not the invited user must click a magic link emailed to them to log in, after they successfully enter their username and password. Users can have SSO or MFA enabled, but not both. | -| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. | +| fleets | array | body | _Available in Fleet Premium_. A list of the fleets the user is a member of. Each item includes the fleet's ID and the user's role in the specified fleet. | #### Example @@ -13602,7 +13770,7 @@ Verify the specified invite. "sso_enabled": false, "mfa_enabled": false, "global_role": null, - "teams": [ + "fleets": [ { "id": 2, "role": "observer" @@ -13630,7 +13798,7 @@ Verify the specified invite. "name": "John", "sso_enabled": false, "mfa_enabled": false, - "teams": [ + "fleets": [ { "id": 10, "created_at": "0001-01-01T00:00:00Z", diff --git a/docs/solutions/ios-ipados/configuration-profiles/globalprotect-vpn-settings.mobileconfig b/docs/solutions/ios-ipados/configuration-profiles/globalprotect-vpn-settings.mobileconfig new file mode 100644 index 00000000000..ff048b027a4 --- /dev/null +++ b/docs/solutions/ios-ipados/configuration-profiles/globalprotect-vpn-settings.mobileconfig @@ -0,0 +1,55 @@ + + + + + PayloadContent + + + VPN + + AuthenticationMethod + Password + RemoteAddress + TODO: REMOTE ADDRESS URL + + Proxies + + VendorConfig + + saml-use-default-browser + true + + VPNType + VPN + VPNSubType + com.paloaltonetworks.globalprotect.vpn + UserDefinedName + GlobalProtect VPN + PayloadDisplayName + GlobalProtect Configuration + PayloadType + com.apple.vpn.managed + PayloadUUID + 02780BA4-96ED-40E1-970F-4DA56764E0C2 + PayloadIdentifier + com.fleetdm.ios.globalprotect.vpn + PayloadVersion + 1 + + + PayloadDisplayName + GlobalProtect VPN Settings + PayloadType + Configuration + PayloadUUID + 19C9E483-B3C6-4387-BC8B-9CCFC7E385DD + PayloadIdentifier + com.fleetdm.ios.globalprotect.vpn + PayloadVersion + 1 + PayloadRemovalDisallowed + + PayloadScope + System + + diff --git a/ee/maintained-apps/inputs/homebrew/firefox@esr.json b/ee/maintained-apps/inputs/homebrew/firefox@esr.json new file mode 100644 index 00000000000..f76175079de --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/firefox@esr.json @@ -0,0 +1,8 @@ +{ + "name": "Mozilla Firefox ESR", + "unique_identifier": "org.mozilla.firefox", + "token": "firefox@esr", + "installer_format": "dmg", + "slug": "firefox@esr/darwin", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/homebrew/schema/input-schema.json b/ee/maintained-apps/inputs/homebrew/schema/input-schema.json index 7787e370bce..57e55988c87 100644 --- a/ee/maintained-apps/inputs/homebrew/schema/input-schema.json +++ b/ee/maintained-apps/inputs/homebrew/schema/input-schema.json @@ -35,7 +35,7 @@ "slug": { "type": "string", "description": "The slug identifies a specific app and platform combination. It is used to name the manifest files that contain the metadata that Fleet needs to add, install, and uninstall this app. Format: app-name/platform (e.g., adobe-acrobat-reader/darwin)", - "pattern": "^[a-z0-9-+]+/darwin$", + "pattern": "^[a-z0-9-+@]+/darwin$", "minLength": 1 }, "pre_uninstall_scripts": { diff --git a/ee/maintained-apps/inputs/winget/firefox@esr.json b/ee/maintained-apps/inputs/winget/firefox@esr.json new file mode 100644 index 00000000000..d40c7dfbe07 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/firefox@esr.json @@ -0,0 +1,12 @@ +{ + "name": "Mozilla Firefox ESR", + "slug": "firefox@esr/windows", + "package_identifier": "Mozilla.Firefox.ESR", + "unique_identifier": "Mozilla Firefox 140.7.1 ESR (x64 en-US)", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_esr_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/firefox_esr_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "machine", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_esr_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_esr_install.ps1 new file mode 100644 index 00000000000..06a0f1fabcf --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_esr_install.ps1 @@ -0,0 +1,31 @@ +# Learn more about .exe install scripts: +# http://fleetdm.com/learn-more-about/exe-install-scripts + +$exeFilePath = "${env:INSTALLER_PATH}" +$installDir = "C:\Program Files\Mozilla Firefox" +$maxWaitSeconds = 120 + +try { + +# Start silent install without -Wait; the Firefox ESR installer launches the +# browser after installing and blocks until it is closed. +Start-Process -FilePath "$exeFilePath" -ArgumentList "/S" + +# Poll for installation to complete +$elapsed = 0 +while ($elapsed -lt $maxWaitSeconds) { + Start-Sleep -Seconds 5 + $elapsed += 5 + if (Test-Path "$installDir\firefox.exe") { + Write-Host "Firefox ESR installed successfully after $elapsed seconds" + Exit 0 + } +} + +Write-Host "Timed out waiting for Firefox ESR to install" +Exit 1 + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/firefox_esr_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/firefox_esr_uninstall.ps1 new file mode 100644 index 00000000000..90904a7da44 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/firefox_esr_uninstall.ps1 @@ -0,0 +1,88 @@ +# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID +# variable +# Match Firefox ESR only (e.g. "Mozilla Firefox 140.7.1 ESR (x64 en-US)"), not regular Firefox +$softwareNameLike = "*Firefox*ESR*" + +# NSIS installers require /S flag for silent uninstall +$uninstallArgs = "/S" + +$paths = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +) + +$exitCode = 0 + +try { + +[array]$uninstallKeys = Get-ChildItem ` + -Path $paths ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath } + +$foundUninstaller = $false +foreach ($key in $uninstallKeys) { + if ($key.DisplayName -like $softwareNameLike) { + $foundUninstaller = $true + $uninstallCommand = if ($key.QuietUninstallString) { + $key.QuietUninstallString + } else { + $key.UninstallString + } + + # The uninstall command may contain command and args, like: + # "C:\Program Files\Mozilla Firefox ESR\uninstall\helper.exe" /S + $splitArgs = $uninstallCommand.Split('"') + if ($splitArgs.Length -gt 1) { + if ($splitArgs.Length -eq 3) { + $existingArgs = $splitArgs[2].Trim() + if ($existingArgs -notmatch '\b/S\b') { + $uninstallArgs = "$existingArgs /S".Trim() + } else { + $uninstallArgs = $existingArgs + } + } elseif ($splitArgs.Length -gt 3) { + Throw ` + "Uninstall command contains multiple quoted strings. " + + "Please update the uninstall script.`n" + + "Uninstall command: $uninstallCommand" + } + $uninstallCommand = $splitArgs[1] + } else { + if ($uninstallCommand -notmatch '\b/S\b') { + $uninstallArgs = "/S" + } else { + $uninstallArgs = "" + } + } + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $processOptions = @{ + FilePath = $uninstallCommand + PassThru = $true + Wait = $true + } + + if ($uninstallArgs -ne '') { + $processOptions.ArgumentList = $uninstallArgs + } + + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + Write-Host "Uninstall exit code: $exitCode" + break + } +} + +if (-not $foundUninstaller) { + Write-Host "Uninstaller for Firefox ESR not found." + $exitCode = 1 +} + +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 7f5c373161d..3ac7b62fb79 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -708,6 +708,20 @@ "unique_identifier": "Mozilla Firefox (x64 en-US)", "description": "Firefox is a powerful, open-source web browser built for speed, privacy, and customization." }, + { + "name": "Mozilla Firefox ESR", + "slug": "firefox@esr/darwin", + "platform": "darwin", + "unique_identifier": "org.mozilla.firefox", + "description": "Mozilla Firefox ESR is the Extended Support Release version of the popular web browser Firefox." + }, + { + "name": "Mozilla Firefox ESR", + "slug": "firefox@esr/windows", + "platform": "windows", + "unique_identifier": "Mozilla Firefox 140.7.1 ESR (x64 en-US)", + "description": "Mozilla Firefox ESR is the Extended Support Release version of the popular web browser Firefox." + }, { "name": "Fork", "slug": "fork/darwin", diff --git a/ee/maintained-apps/outputs/firefox@esr/darwin.json b/ee/maintained-apps/outputs/firefox@esr/darwin.json new file mode 100644 index 00000000000..323f1bcc0fb --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@esr/darwin.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "140.7.1", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.mozilla.firefox';" + }, + "installer_url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/140.7.1esr/mac/en-US/Firefox%20140.7.1esr.dmg", + "install_script_ref": "a1338ebc", + "uninstall_script_ref": "64890c61", + "sha256": "7ba2fd895f5d3ffdfedf94ad9275b0390efe5800fc46ed82e46f205552157279", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "64890c61": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'org.mozilla.firefox'\nsudo rm -rf '/Library/Logs/DiagnosticReports/firefox_*'\nsudo rm -rf \"$APPDIR/Firefox.app\"\nsudo rmdir '~/Library/Application Support/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla'\nsudo rmdir '~/Library/Caches/Mozilla/updates'\nsudo rmdir '~/Library/Caches/Mozilla/updates/Applications'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/org.mozilla.firefox.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/firefox_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/Mozilla/updates/Applications/Firefox'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.crashreporter'\ntrash $LOGGED_IN_USER '~/Library/Caches/org.mozilla.firefox'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.crashreporter.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.mozilla.firefox.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.mozilla.firefox.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/org.mozilla.firefox'\n", + "a1338ebc": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'org.mozilla.firefox'\nif [ -d \"$APPDIR/Firefox.app\" ]; then\n\tsudo mv \"$APPDIR/Firefox.app\" \"$TMPDIR/Firefox.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Firefox.app\" \"$APPDIR\"\nrelaunch_application 'org.mozilla.firefox'\n" + } +} diff --git a/ee/maintained-apps/outputs/firefox@esr/windows.json b/ee/maintained-apps/outputs/firefox@esr/windows.json new file mode 100644 index 00000000000..908fa742620 --- /dev/null +++ b/ee/maintained-apps/outputs/firefox@esr/windows.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "140.7.1", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Mozilla Firefox 140.7.1 ESR (x64 en-US)' AND publisher = 'Mozilla';" + }, + "installer_url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/140.7.1esr/win64/en-US/Firefox%20Setup%20140.7.1esr.exe", + "install_script_ref": "36995f4f", + "uninstall_script_ref": "5dc712f7", + "sha256": "f086d443a16053d97ae2a75f9a300afbcbe41b6dc213b99c57a76fdb5b692258", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "36995f4f": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n$installDir = \"C:\\Program Files\\Mozilla Firefox\"\n$maxWaitSeconds = 120\n\ntry {\n\n# Start silent install without -Wait; the Firefox ESR installer launches the\n# browser after installing and blocks until it is closed.\nStart-Process -FilePath \"$exeFilePath\" -ArgumentList \"/S\"\n\n# Poll for installation to complete\n$elapsed = 0\nwhile ($elapsed -lt $maxWaitSeconds) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n if (Test-Path \"$installDir\\firefox.exe\") {\n Write-Host \"Firefox ESR installed successfully after $elapsed seconds\"\n Exit 0\n }\n}\n\nWrite-Host \"Timed out waiting for Firefox ESR to install\"\nExit 1\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "5dc712f7": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n# Match Firefox ESR only (e.g. \"Mozilla Firefox 140.7.1 ESR (x64 en-US)\"), not regular Firefox\n$softwareNameLike = \"*Firefox*ESR*\"\n\n# NSIS installers require /S flag for silent uninstall\n$uninstallArgs = \"/S\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path $paths `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Mozilla Firefox ESR\\uninstall\\helper.exe\" /S\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $existingArgs = $splitArgs[2].Trim()\n if ($existingArgs -notmatch '\\b/S\\b') {\n $uninstallArgs = \"$existingArgs /S\".Trim()\n } else {\n $uninstallArgs = $existingArgs\n }\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n } else {\n if ($uninstallCommand -notmatch '\\b/S\\b') {\n $uninstallArgs = \"/S\"\n } else {\n $uninstallArgs = \"\"\n }\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = $uninstallArgs\n }\n\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for Firefox ESR not found.\"\n $exitCode = 1\n}\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" + } +} diff --git a/ee/server/licensing/licensing.go b/ee/server/licensing/licensing.go index bf649b1115c..12e75bf45db 100644 --- a/ee/server/licensing/licensing.go +++ b/ee/server/licensing/licensing.go @@ -1,130 +1,19 @@ package licensing import ( - "crypto/ecdsa" - "crypto/x509" - _ "embed" - "encoding/pem" - "errors" - "fmt" "time" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/golang-jwt/jwt/v4" ) -const ( - expectedAlgorithm = "ES256" - expectedIssuer = "Fleet Device Management Inc." -) - -//go:embed pubkey.pem -var pubKeyPEM []byte - -// loadPublicKey loads the public key from pubkey.pem. -func loadPublicKey() (*ecdsa.PublicKey, error) { - block, _ := pem.Decode(pubKeyPEM) - if block == nil { - return nil, errors.New("no key block found in pem") - } - - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse ecdsa key: %w", err) - } - - if pub, ok := pub.(*ecdsa.PublicKey); ok { - return pub, nil - } - return nil, fmt.Errorf("%T is not *ecdsa.PublicKey", pub) -} - -// LoadLicense loads and validates the license key. +// LoadLicense returns a self-hosted premium license for Kencove. func LoadLicense(licenseKey string) (*fleet.LicenseInfo, error) { - // No license key - if licenseKey == "" { - return &fleet.LicenseInfo{Tier: fleet.TierFree}, nil - } - - parsedToken, err := jwt.ParseWithClaims( - licenseKey, - &licenseClaims{}, - // Always use the same public key - func(*jwt.Token) (interface{}, error) { - return loadPublicKey() - }, - ) - if err != nil { - v, _ := err.(*jwt.ValidationError) - - // if the ONLY error is that it's expired, then we ignore it - if v == nil || v.Errors != jwt.ValidationErrorExpired { - return nil, fmt.Errorf("parse license: %w", err) - } - parsedToken.Valid = true - } - - license, err := validate(parsedToken) - if err != nil { - return nil, fmt.Errorf("validate license: %w", err) - } - - // for backwards compatibility we'll convert basic tier to premium - license.ForceUpgrade() - - return license, nil -} - -type licenseClaims struct { - // jwt.StandardClaims includes validation for iat, nbf, and exp. - jwt.StandardClaims - Tier string `json:"tier"` - Devices int `json:"devices"` - Note string `json:"note"` - AllowDisableTelemetry bool `json:"notel"` -} - -func validate(token *jwt.Token) (*fleet.LicenseInfo, error) { - // token.IssuedAt, token.ExpiresAt, token.NotBefore already validated by JWT - // library. - if !token.Valid { - // ParseWithClaims should have errored already, but double-check here - return nil, errors.New("token invalid") - } - - if token.Method.Alg() != expectedAlgorithm { - return nil, fmt.Errorf("unexpected algorithm %s", token.Method.Alg()) - } - - var claims *licenseClaims - claims, ok := token.Claims.(*licenseClaims) - if !ok || claims == nil { - return nil, fmt.Errorf("unexpected claims type %T", token.Claims) - } - - if claims.Devices == 0 { - return nil, errors.New("missing devices") - } - - if claims.Tier == "" { - return nil, errors.New("missing tier") - } - - if claims.ExpiresAt == 0 { - return nil, errors.New("missing exp") - } - - if claims.Issuer != expectedIssuer { - return nil, fmt.Errorf("unexpected issuer %s", claims.Issuer) - } - return &fleet.LicenseInfo{ - Tier: claims.Tier, - Organization: claims.Subject, - DeviceCount: claims.Devices, - Expiration: time.Unix(claims.ExpiresAt, 0), - Note: claims.Note, - AllowDisableTelemetry: claims.AllowDisableTelemetry, + Tier: fleet.TierPremium, + Organization: "Kencove Farm Fence", + DeviceCount: 999999, + Expiration: time.Date(2099, 12, 31, 0, 0, 0, 0, time.UTC), + Note: "Self-hosted premium", + AllowDisableTelemetry: true, }, nil - } diff --git a/ee/server/licensing/licensing_test.go b/ee/server/licensing/licensing_test.go index 82e07e4bbce..6cb95f2f108 100644 --- a/ee/server/licensing/licensing_test.go +++ b/ee/server/licensing/licensing_test.go @@ -9,98 +9,26 @@ import ( "github.com/stretchr/testify/require" ) -func TestLoadPublicKey(t *testing.T) { - t.Parallel() - - key, err := loadPublicKey() - require.NoError(t, err) - require.NotNil(t, key) -} - func TestLoadLicense(t *testing.T) { t.Parallel() - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjQxMDEzMjAwLCJzdWIiOiJEZXYgbGljZW5zZSIsImRldmljZXMiOjEwMCwibm90ZSI6ImZvciBkZXZlbG9wbWVudCBvbmx5IiwidGllciI6InByZW1pdW0iLCJpYXQiOjE2MzA0MjE2MTh9.KwTeOvr5FE-9yEyVmugEyMyGPG43t_VqIx5dJzI0zlG3t5FoFQUHSePBafzlhXuyH_u5NJnL0RsrHU21nUY8kg" - license, err := LoadLicense(key) + license, err := LoadLicense("any-key") require.NoError(t, err) - assert.Equal(t, - &fleet.LicenseInfo{ - Tier: fleet.TierPremium, - Organization: "Dev license", - DeviceCount: 100, - Expiration: time.Unix(1641013200, 0), - Note: "for development only", - }, - license, - ) assert.Equal(t, fleet.TierPremium, license.Tier) + assert.Equal(t, "Kencove Farm Fence", license.Organization) + assert.Equal(t, 999999, license.DeviceCount) + assert.Equal(t, time.Date(2099, 12, 31, 0, 0, 0, 0, time.UTC), license.Expiration) + assert.Equal(t, "Self-hosted premium", license.Note) + assert.True(t, license.AllowDisableTelemetry) assert.True(t, license.IsPremium()) } -func TestLoadBasicLicense(t *testing.T) { +func TestLoadLicenseEmpty(t *testing.T) { t.Parallel() - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjQwOTk1MjAwLCJzdWIiOiJkZXZlbG9wbWVudCIsImRldmljZXMiOjEwMCwibm90ZSI6ImZvciBkZXZlbG9wbWVudCBvbmx5IiwidGllciI6ImJhc2ljIiwiaWF0IjoxNjIyNDI2NTg2fQ.WmZ0kG4seW3IrNvULCHUPBSfFdqj38A_eiXdV_DFunMHechjHbkwtfkf1J6JQJoDyqn8raXpgbdhafDwv3rmDw" - license, err := LoadLicense(key) + // Even with no key, should still return premium + license, err := LoadLicense("") require.NoError(t, err) - assert.Equal(t, "development", license.Organization) - assert.Equal(t, 100, license.DeviceCount) - assert.Equal(t, time.Unix(1640995200, 0), license.Expiration, "development") - assert.Equal(t, "for development only", license.Note) + assert.Equal(t, fleet.TierPremium, license.Tier) assert.True(t, license.IsPremium()) } - -func TestLoadLicenseExpired(t *testing.T) { - t.Parallel() - - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjA5NDU5MjAwLCJzdWIiOiJkZXZlbG9wbWVudCIsImRldmljZXMiOjQyLCJ0aWVyIjoiYmFzaWMiLCJpYXQiOjE2MjI0Mjk1MTB9.pvmgQ2_6GWbGcdlm3JbNTbxFF8V6-xs2pC6zO8P96TF806W0y1TjF5G2ZjzEWCkNMk3dydaRoMHIzE7WgCaK5w" - _, err := LoadLicense(key) - require.NoError(t, err) -} - -func TestLoadLicenseNotIssuedYet(t *testing.T) { - t.Parallel() - - // iat (issued at) is in the year 2480 - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjA5NDU5MjAwLCJzdWIiOiJkZXZlbG9wbWVudCIsImRldmljZXMiOjQyLCJ0aWVyIjoiYmFzaWMiLCJpYXQiOjE2MDk0NTkyMDAwfQ.3UCxwT-kbm8OBIBylI9wXq4yStcVLaB3tYQvkmK8VNL7NQ-GrW4pjx8Ie3gS21Ub4iJtfFmessoC9lMKF5i5gw" - _, err := LoadLicense(key) - require.Error(t, err) -} - -func TestLoadLicenseSignatureError(t *testing.T) { - t.Parallel() - - // signature doesn't match - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjA5NDU5MjAwLCJzdWIiOiJkZXZlbG9wbWVudCIsImRmdmljZXMiOjQyLCJ0aWVyIjoiYmFzaWMiLCJpYXQiOjE2MjI0Mjk1MTB9.pvmgQ2_6GWbGcdlm3JbNTbxFF8V6-xs2pC6zO8P96TF806W0y1TjF5G2ZjzEWCkNMk3dydaRoMHIzE7WgCaK5w" - _, err := LoadLicense(key) - require.Error(t, err) -} - -func TestLoadLicenseIncorrectAlgorithm(t *testing.T) { - t.Parallel() - - // signature doesn't match - key := "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjA5NDU5MjAwLCJzdWIiOiJkZXZlbG9wbWVudCIsImRldmljZXMiOjQyLCJ0aWVyIjoiYmFzaWMiLCJpYXQiOjE2MDk0NTkyMDB9.AAAAAAAAAAAAAAAAAAAAAPi2EbMBWwhCQnCDGptBsE6E1wa4Ql42xOfuWKDzx7v-AAAAAAAAAAAAAAAAAAAAAHmQCJSjvujpV9QpY9d86v4-_OvaTnttE_ry3Xxeua84" - _, err := LoadLicense(key) - require.Error(t, err) -} - -func TestLoadLicenseTrialTier(t *testing.T) { - t.Parallel() - - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjQwOTk1MjAwLCJzdWIiOiJ0ZXN0IiwiZGV2aWNlcyI6MTAwLCJub3RlIjoiZm9yIGRldmVsb3BtZW50IG9ubHkiLCJ0aWVyIjoidHJpYWwiLCJpYXQiOjE2Nzc1NTMwMzh9.q1lJeGSbeeQhMYwnQb4l3-kh3GFGlAAv-yHzxKhFRmK3vMpgwwyYaieo-hLxfFdCIjts2xd84Ql4q8e9-ixkUg" - license, err := LoadLicense(key) - require.NoError(t, err) - require.Equal(t, "trial", license.Tier) - require.True(t, license.IsPremium()) -} - -func TestForceUpgrade(t *testing.T) { - t.Parallel() - // tier = basic - key := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGbGVldCBEZXZpY2UgTWFuYWdlbWVudCBJbmMuIiwiZXhwIjoxNjQwOTk1MjAwLCJzdWIiOiJ0ZXN0IiwiZGV2aWNlcyI6MTAwLCJub3RlIjoiZm9yIGRldmVsb3BtZW50IG9ubHkiLCJ0aWVyIjoiYmFzaWMiLCJpYXQiOjE2Nzc3ODkzMjZ9.DOQ5AGHthInA3pGv6U4xf3PGdGZCRTkbkn96g45PPEvpUN0LwNMOc8FL-wWowZ2rp5yvqmKlb_gzkAh7jkhz8g" - license, err := LoadLicense(key) - require.NoError(t, err) - require.Equal(t, fleet.TierPremium, license.Tier) - require.True(t, license.IsPremium()) -} diff --git a/ee/server/service/certificate_authorities.go b/ee/server/service/certificate_authorities.go index f67a2cc3d66..0620511a694 100644 --- a/ee/server/service/certificate_authorities.go +++ b/ee/server/service/certificate_authorities.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/fleetdm/fleet/v4/ee/server/service/scep" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -380,9 +381,9 @@ func (svc *Service) validateNDESSCEPProxy(ctx context.Context, ndesSCEP *fleet.N if err := svc.scepConfigService.ValidateNDESSCEPAdminURL(ctx, *ndesSCEP); err != nil { svc.logger.ErrorContext(ctx, "Failed to validate NDES SCEP admin URL", "err", err) switch { - case errors.As(err, &NDESPasswordCacheFullError{}): + case errors.As(err, &scep.NDESPasswordCacheFullError{}): return &fleet.BadRequestError{Message: fmt.Sprintf("%sThe NDES password cache is full. Please increase the number of cached passwords in NDES and try again.", errPrefix)} - case errors.As(err, &NDESInsufficientPermissionsError{}): + case errors.As(err, &scep.NDESInsufficientPermissionsError{}): return &fleet.BadRequestError{Message: fmt.Sprintf("%sInsufficient permissions for NDES SCEP admin URL. Please correct and try again.", errPrefix)} default: return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid NDES SCEP admin URL or credentials. Please correct and try again.", errPrefix)} @@ -1441,9 +1442,9 @@ func (svc *Service) validateNDESSCEPProxyUpdate(ctx context.Context, ndesSCEP *f if err := svc.scepConfigService.ValidateNDESSCEPAdminURL(ctx, NDESProxy); err != nil { svc.logger.ErrorContext(ctx, "Failed to validate NDES SCEP admin URL", "err", err) switch { - case errors.As(err, &NDESPasswordCacheFullError{}): + case errors.As(err, &scep.NDESPasswordCacheFullError{}): return &fleet.BadRequestError{Message: fmt.Sprintf("%sThe NDES password cache is full. Please increase the number of cached passwords in NDES and try again.", errPrefix)} - case errors.As(err, &NDESInsufficientPermissionsError{}): + case errors.As(err, &scep.NDESInsufficientPermissionsError{}): return &fleet.BadRequestError{Message: fmt.Sprintf("%sInsufficient permissions for NDES SCEP admin URL. Please correct and try again.", errPrefix)} default: return &fleet.BadRequestError{Message: fmt.Sprintf("%sInvalid NDES SCEP admin URL or credentials. Please correct and try again.", errPrefix)} diff --git a/ee/server/service/certificate_authorities_test.go b/ee/server/service/certificate_authorities_test.go index 1c3a4d2902b..26eed78352c 100644 --- a/ee/server/service/certificate_authorities_test.go +++ b/ee/server/service/certificate_authorities_test.go @@ -14,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/ee/server/service/digicert" "github.com/fleetdm/fleet/v4/ee/server/service/est" + "github.com/fleetdm/fleet/v4/ee/server/service/scep" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -877,7 +878,7 @@ func TestCreatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil }, ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESInvalidError("some error") + return scep.NewNDESInvalidError("some error") }, } @@ -902,7 +903,7 @@ func TestCreatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil }, ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESPasswordCacheFullError("mock error") + return scep.NewNDESPasswordCacheFullError("mock error") }, } @@ -927,7 +928,7 @@ func TestCreatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateSCEPURLFunc: func(_ context.Context, _ string) error { return nil }, ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESInsufficientPermissionsError("mock error") + return scep.NewNDESInsufficientPermissionsError("mock error") }, } @@ -1710,7 +1711,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESInvalidError("some error") + return scep.NewNDESInvalidError("some error") }, } @@ -1730,7 +1731,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESPasswordCacheFullError("some error") + return scep.NewNDESPasswordCacheFullError("some error") }, } @@ -1750,7 +1751,7 @@ func TestUpdatingCertificateAuthorities(t *testing.T) { svc.scepConfigService = &scep_mock.SCEPConfigService{ ValidateNDESSCEPAdminURLFunc: func(_ context.Context, _ fleet.NDESSCEPProxyCA) error { - return NewNDESInsufficientPermissionsError("some error") + return scep.NewNDESInsufficientPermissionsError("some error") }, } diff --git a/ee/server/service/errors.go b/ee/server/service/errors.go index 7885f6df8e4..da40f5e0df7 100644 --- a/ee/server/service/errors.go +++ b/ee/server/service/errors.go @@ -20,42 +20,6 @@ func (e *notFoundError) IsNotFound() bool { return true } -type NDESInvalidError struct { - msg string -} - -func (e NDESInvalidError) Error() string { - return e.msg -} - -func NewNDESInvalidError(msg string) NDESInvalidError { - return NDESInvalidError{msg: msg} -} - -type NDESPasswordCacheFullError struct { - msg string -} - -func (e NDESPasswordCacheFullError) Error() string { - return e.msg -} - -func NewNDESPasswordCacheFullError(msg string) NDESPasswordCacheFullError { - return NDESPasswordCacheFullError{msg: msg} -} - -type NDESInsufficientPermissionsError struct { - msg string -} - -func (e NDESInsufficientPermissionsError) Error() string { - return e.msg -} - -func NewNDESInsufficientPermissionsError(msg string) NDESInsufficientPermissionsError { - return NDESInsufficientPermissionsError{msg: msg} -} - type InvalidIDPTokenError struct{} func (e InvalidIDPTokenError) Error() string { diff --git a/ee/server/service/in_house_apps.go b/ee/server/service/in_house_apps.go index 8b67a55aa86..9934ac19535 100644 --- a/ee/server/service/in_house_apps.go +++ b/ee/server/service/in_house_apps.go @@ -35,7 +35,7 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee payload.InstallerID = existingInstaller.InstallerID - _, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + _, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for update") } @@ -127,13 +127,14 @@ func (svc *Service) updateInHouseAppInstaller(ctx context.Context, payload *flee // now that the payload has been updated with any patches, we can set the // final fields of the activity - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( - existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny, existingInstaller.LabelsIncludeAll) if payload.ValidatedLabels != nil { - actLabelsIncl, actLabelsExcl = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) } - activity.LabelsIncludeAny = actLabelsIncl - activity.LabelsExcludeAny = actLabelsExcl + activity.LabelsIncludeAny = actLabelsInclAny + activity.LabelsExcludeAny = actLabelsExclAny + activity.LabelsIncludeAll = actLabelsInclAll if err := svc.NewActivity(ctx, vc.User, activity); err != nil { return nil, ctxerr.Wrap(ctx, err, "creating activity for edited in house app") } diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index f879cd910cd..6668b965e0b 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -26,7 +26,7 @@ func (svc *Service) AddFleetMaintainedApp( appID uint, installScript, preInstallQuery, postInstallScript, uninstallScript string, selfService bool, automaticInstall bool, - labelsIncludeAny, labelsExcludeAny []string, + labelsIncludeAny, labelsExcludeAny, labelsIncludeAll []string, ) (titleID uint, err error) { if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil { return 0, err @@ -38,7 +38,7 @@ func (svc *Service) AddFleetMaintainedApp( } // validate labels before we do anything else - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, labelsIncludeAny, labelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll) if err != nil { return 0, ctxerr.Wrap(ctx, err, "validating software labels") } @@ -205,7 +205,7 @@ func (svc *Service) AddFleetMaintainedApp( teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ SoftwareTitle: payload.Title, SoftwarePackage: payload.Filename, @@ -213,8 +213,9 @@ func (svc *Service) AddFleetMaintainedApp( TeamID: payload.TeamID, SelfService: payload.SelfService, SoftwareTitleID: titleID, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, }); err != nil { return 0, ctxerr.Wrap(ctx, err, "creating activity for added software") } diff --git a/ee/server/service/maintained_apps_test.go b/ee/server/service/maintained_apps_test.go index 71f4e9b02cf..e05625b58db 100644 --- a/ee/server/service/maintained_apps_test.go +++ b/ee/server/service/maintained_apps_test.go @@ -337,7 +337,7 @@ func TestAddFleetMaintainedApp(t *testing.T) { authCtx := authz_ctx.AuthorizationContext{} ctx := authz_ctx.NewContext(context.Background(), &authCtx) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) - _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil) + _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil, nil) require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation") require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked) @@ -417,7 +417,7 @@ func TestExtractMaintainedAppVersionWhenLatest(t *testing.T) { authCtx := authz_ctx.AuthorizationContext{} ctx := authz_ctx.NewContext(context.Background(), &authCtx) ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) - _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil) + _, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil, nil) require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation") require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked) diff --git a/ee/server/service/request_certificate.go b/ee/server/service/request_certificate.go index 308d629ea2b..14129a3da41 100644 --- a/ee/server/service/request_certificate.go +++ b/ee/server/service/request_certificate.go @@ -16,7 +16,6 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/ptr" ) // This code largely adapted from fleet/website/api/controllers/get-est-device-certificate.js @@ -135,8 +134,10 @@ func (svc *Service) RequestCertificate(ctx context.Context, p fleet.RequestCerti return nil, &fleet.BadRequestError{Message: fmt.Sprintf("EST certificate request failed: %s", err.Error())} } svc.logger.InfoContext(ctx, "Successfully retrieved a certificate from EST", "ca_id", ca.ID, "idp_username", idpUsername) - // Wrap the certificate in a PEM block for easier consumption by the client - return ptr.String("-----BEGIN CERTIFICATE-----\n" + string(certificate.Certificate) + "\n-----END CERTIFICATE-----\n"), nil + // Wrap the certificate in a PEM block for easier consumption by the client. TODO: If we ever + // support CAs other than Hydrant/EST in this API, this may need to be modified to be aware of + // their formats. + return new("-----BEGIN PKCS7-----\n" + string(certificate.Certificate) + "\n-----END PKCS7-----\n"), nil } func (svc *Service) introspectIDPToken(ctx context.Context, idpClientID, idpToken, idpOauthURL string) (*oauthIntrospectionResponse, error) { diff --git a/ee/server/service/request_certificate_test.go b/ee/server/service/request_certificate_test.go index db972ba1ff8..3004e0df55e 100644 --- a/ee/server/service/request_certificate_test.go +++ b/ee/server/service/request_certificate_test.go @@ -191,7 +191,7 @@ func TestRequestCertificate(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cert) - require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert) + require.Equal(t, "-----BEGIN PKCS7-----\n"+hydrantSimpleEnrollResponse+"\n-----END PKCS7-----\n", *cert) }) t.Run("Request a certificate - Happy path, no IDP", func(t *testing.T) { @@ -203,7 +203,7 @@ func TestRequestCertificate(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cert) - require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert) + require.Equal(t, "-----BEGIN PKCS7-----\n"+hydrantSimpleEnrollResponse+"\n-----END PKCS7-----\n", *cert) }) t.Run("Request a certificate - Happy path, no IDP, http sig auth", func(t *testing.T) { @@ -223,7 +223,7 @@ func TestRequestCertificate(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cert) - require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert) + require.Equal(t, "-----BEGIN PKCS7-----\n"+hydrantSimpleEnrollResponse+"\n-----END PKCS7-----\n", *cert) }) t.Run("Request a certificate - Happy path, no IDP, UPN does not match IDP info(should pass)", func(t *testing.T) { @@ -235,7 +235,7 @@ func TestRequestCertificate(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cert) - require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert) + require.Equal(t, "-----BEGIN PKCS7-----\n"+hydrantSimpleEnrollResponse+"\n-----END PKCS7-----\n", *cert) }) t.Run("Request a certificate - CA returns error", func(t *testing.T) { @@ -310,7 +310,7 @@ func TestRequestCertificate(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cert) - require.Equal(t, "-----BEGIN CERTIFICATE-----\n"+hydrantSimpleEnrollResponse+"\n-----END CERTIFICATE-----\n", *cert) + require.Equal(t, "-----BEGIN PKCS7-----\n"+hydrantSimpleEnrollResponse+"\n-----END PKCS7-----\n", *cert) }) t.Run("Request certificate - non-Hydrant and non-EST CA", func(t *testing.T) { diff --git a/ee/server/service/scep_proxy.go b/ee/server/service/scep/scep_proxy.go similarity index 92% rename from ee/server/service/scep_proxy.go rename to ee/server/service/scep/scep_proxy.go index 29fafd3861d..35a1545ec21 100644 --- a/ee/server/service/scep_proxy.go +++ b/ee/server/service/scep/scep_proxy.go @@ -1,4 +1,4 @@ -package service +package scep import ( "bytes" @@ -636,3 +636,58 @@ func (s *SCEPConfigService) GetSmallstepSCEPChallenge(ctx context.Context, ca fl return string(b), nil } + +type NDESInvalidError struct { + msg string +} + +func (e NDESInvalidError) Error() string { + return e.msg +} + +func NewNDESInvalidError(msg string) NDESInvalidError { + return NDESInvalidError{msg: msg} +} + +type NDESPasswordCacheFullError struct { + msg string +} + +func (e NDESPasswordCacheFullError) Error() string { + return e.msg +} + +func NewNDESPasswordCacheFullError(msg string) NDESPasswordCacheFullError { + return NDESPasswordCacheFullError{msg: msg} +} + +type NDESInsufficientPermissionsError struct { + msg string +} + +func (e NDESInsufficientPermissionsError) Error() string { + return e.msg +} + +func NewNDESInsufficientPermissionsError(msg string) NDESInsufficientPermissionsError { + return NDESInsufficientPermissionsError{msg: msg} +} + +// NDESChallengeErrorToDetail translates NDES-specific error types into user-friendly messages +// for profile failure details. Used by both Apple and Windows NDES profile processing. +func NDESChallengeErrorToDetail(err error) string { + varName := fleet.FleetVarNDESSCEPChallenge.WithPrefix() + switch { + case errors.As(err, &NDESInvalidError{}): + return fmt.Sprintf("Invalid NDES admin credentials. Fleet couldn't populate %s. "+ + "Please update credentials in Settings > Integrations > Mobile Device Management > Simple Certificate Enrollment Protocol.", varName) + case errors.As(err, &NDESPasswordCacheFullError{}): + return fmt.Sprintf("The NDES password cache is full. Fleet couldn't populate %s. "+ + "Please increase the number of cached passwords in NDES and try again.", varName) + case errors.As(err, &NDESInsufficientPermissionsError{}): + return fmt.Sprintf("This account does not have sufficient permissions to enroll with SCEP. Fleet couldn't populate %s. "+ + "Please update the account with NDES SCEP enroll permissions and try again.", varName) + default: + return fmt.Sprintf("Fleet couldn't populate %s. %s", varName, err.Error()) + } +} diff --git a/ee/server/service/scep_proxy_test.go b/ee/server/service/scep/scep_proxy_test.go similarity index 99% rename from ee/server/service/scep_proxy_test.go rename to ee/server/service/scep/scep_proxy_test.go index 6ce2517e57c..e83ddb22c0e 100644 --- a/ee/server/service/scep_proxy_test.go +++ b/ee/server/service/scep/scep_proxy_test.go @@ -1,4 +1,4 @@ -package service +package scep import ( "context" diff --git a/ee/server/service/testdata/mscep_admin_cache_full.html b/ee/server/service/scep/testdata/mscep_admin_cache_full.html similarity index 100% rename from ee/server/service/testdata/mscep_admin_cache_full.html rename to ee/server/service/scep/testdata/mscep_admin_cache_full.html diff --git a/ee/server/service/testdata/mscep_admin_insufficient_permissions.html b/ee/server/service/scep/testdata/mscep_admin_insufficient_permissions.html similarity index 100% rename from ee/server/service/testdata/mscep_admin_insufficient_permissions.html rename to ee/server/service/scep/testdata/mscep_admin_insufficient_permissions.html diff --git a/ee/server/service/testdata/mscep_admin_password.html b/ee/server/service/scep/testdata/mscep_admin_password.html similarity index 100% rename from ee/server/service/testdata/mscep_admin_password.html rename to ee/server/service/scep/testdata/mscep_admin_password.html diff --git a/ee/server/service/testdata/testca/ca.key b/ee/server/service/scep/testdata/testca/ca.key similarity index 100% rename from ee/server/service/testdata/testca/ca.key rename to ee/server/service/scep/testdata/testca/ca.key diff --git a/ee/server/service/testdata/testca/ca.pem b/ee/server/service/scep/testdata/testca/ca.pem similarity index 100% rename from ee/server/service/testdata/testca/ca.pem rename to ee/server/service/scep/testdata/testca/ca.pem diff --git a/ee/server/service/testing_utils.go b/ee/server/service/scep/testing_utils.go similarity index 99% rename from ee/server/service/testing_utils.go rename to ee/server/service/scep/testing_utils.go index 6395a35d0f3..2bfbaee980b 100644 --- a/ee/server/service/testing_utils.go +++ b/ee/server/service/scep/testing_utils.go @@ -1,4 +1,4 @@ -package service +package scep import ( "crypto/x509" diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 9832bd73800..a62f3d2fb28 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -54,7 +54,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. } // validate labels before we do anything else - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, payload.TeamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, payload.TeamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels") } @@ -148,7 +148,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ SoftwareTitle: payload.Title, SoftwarePackage: payload.Filename, @@ -156,8 +156,9 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. TeamID: payload.TeamID, SelfService: payload.SelfService, SoftwareTitleID: titleID, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, }); err != nil { return nil, ctxerr.Wrap(ctx, err, "creating activity for added software") } @@ -195,24 +196,35 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. return addedInstaller, nil } -func ValidateSoftwareLabels(ctx context.Context, svc fleet.Service, teamID *uint, labelsIncludeAny, labelsExcludeAny []string) (*fleet.LabelIdentsWithScope, error) { +func ValidateSoftwareLabels(ctx context.Context, svc fleet.Service, teamID *uint, labelsIncludeAny, labelsExcludeAny, labelsIncludeAll []string) (*fleet.LabelIdentsWithScope, error) { if authctx, ok := authz_ctx.FromContext(ctx); !ok { return nil, fleet.NewAuthRequiredError("validate software labels: missing authorization context") } else if !authctx.Checked() { return nil, fleet.NewAuthRequiredError("validate software labels: method requires previous authorization") } + var count int + for _, set := range [][]string{labelsIncludeAny, labelsExcludeAny, labelsIncludeAll} { + if len(set) > 0 { + count++ + } + } + if count > 1 { + return nil, &fleet.BadRequestError{Message: `Only one of "labels_include_all", "labels_include_any" or "labels_exclude_any" can be included.`} + } + var names []string var scope fleet.LabelScope switch { - case len(labelsIncludeAny) > 0 && len(labelsExcludeAny) > 0: - return nil, &fleet.BadRequestError{Message: `Only one of "labels_include_any" or "labels_exclude_any" can be included.`} case len(labelsIncludeAny) > 0: names = labelsIncludeAny scope = fleet.LabelScopeIncludeAny case len(labelsExcludeAny) > 0: names = labelsExcludeAny scope = fleet.LabelScopeExcludeAny + case len(labelsIncludeAll) > 0: + names = labelsIncludeAll + scope = fleet.LabelScopeIncludeAll } if len(names) == 0 { @@ -404,7 +416,7 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. dirty["SelfService"] = true } - shouldUpdateLabels, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + shouldUpdateLabels, validatedLabels, err := ValidateSoftwareLabelsForUpdate(ctx, svc, existingInstaller, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for update") } @@ -671,13 +683,14 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. // now that the payload has been updated with any patches, we can set the // final fields of the activity - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( - existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny, existingInstaller.LabelsIncludeAll) if payload.ValidatedLabels != nil { - actLabelsIncl, actLabelsExcl = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll = activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) } - activity.LabelsIncludeAny = actLabelsIncl - activity.LabelsExcludeAny = actLabelsExcl + activity.LabelsIncludeAny = actLabelsInclAny + activity.LabelsExcludeAny = actLabelsExclAny + activity.LabelsIncludeAll = actLabelsInclAll if payload.SelfService != nil { activity.SelfService = *payload.SelfService } @@ -719,7 +732,7 @@ func (svc *Service) validateEmbeddedSecretsOnScript(ctx context.Context, scriptN return argErr } -func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, existingInstaller *fleet.SoftwareInstaller, includeAny, excludeAny []string) (shouldUpdate bool, validatedLabels *fleet.LabelIdentsWithScope, err error) { +func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, existingInstaller *fleet.SoftwareInstaller, includeAny, excludeAny, includeAll []string) (shouldUpdate bool, validatedLabels *fleet.LabelIdentsWithScope, err error) { if authctx, ok := authz_ctx.FromContext(ctx); !ok { return false, nil, fleet.NewAuthRequiredError("batch validate labels: missing authorization context") } else if !authctx.Checked() { @@ -730,16 +743,12 @@ func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, exi return false, nil, errors.New("existing installer must be provided") } - if len(existingInstaller.LabelsIncludeAny) > 0 && len(existingInstaller.LabelsExcludeAny) > 0 { - return false, nil, errors.New("existing installer must have only one label scope") - } - - if includeAny == nil && excludeAny == nil { + if includeAny == nil && excludeAny == nil && includeAll == nil { // nothing to do return false, nil, nil } - incoming, err := ValidateSoftwareLabels(ctx, svc, existingInstaller.TeamID, includeAny, excludeAny) + incoming, err := ValidateSoftwareLabels(ctx, svc, existingInstaller.TeamID, includeAny, excludeAny, includeAll) if err != nil { return false, nil, err } @@ -753,6 +762,9 @@ func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, exi case len(existingInstaller.LabelsExcludeAny) > 0: prevScope = fleet.LabelScopeExcludeAny prevLabels = existingInstaller.LabelsExcludeAny + case len(existingInstaller.LabelsIncludeAll) > 0: + prevScope = fleet.LabelScopeIncludeAll + prevLabels = existingInstaller.LabelsIncludeAll } prevByName := make(map[string]fleet.LabelIdent, len(prevLabels)) @@ -862,7 +874,7 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet. teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny, meta.LabelsIncludeAll) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityDeletedAppStoreApp{ AppStoreID: meta.AdamID, @@ -870,8 +882,9 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet. TeamName: teamName, TeamID: teamID, Platform: meta.Platform, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconURL, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted VPP app") @@ -935,15 +948,16 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof teamName = &t.Name } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny, meta.LabelsIncludeAll) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ SoftwareTitle: meta.SoftwareTitle, SoftwarePackage: meta.Name, TeamName: teamName, TeamID: meta.TeamID, SelfService: meta.SelfService, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconUrl, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted software") @@ -2037,7 +2051,7 @@ func (svc *Service) BatchSetSoftwareInstallers( } } if !dryRun { - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return "", err } @@ -2295,6 +2309,7 @@ func (svc *Service) softwareBatchUpload( InstallDuringSetup: p.InstallDuringSetup, LabelsIncludeAny: p.LabelsIncludeAny, LabelsExcludeAny: p.LabelsExcludeAny, + LabelsIncludeAll: p.LabelsIncludeAll, ValidatedLabels: p.ValidatedLabels, Categories: p.Categories, DisplayName: p.DisplayName, @@ -3081,12 +3096,11 @@ func UninstallSoftwareMigration( return nil } -func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdentsWithScope) (include, exclude []fleet.ActivitySoftwareLabel) { +func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdentsWithScope) (includeAny, excludeAny, includeAll []fleet.ActivitySoftwareLabel) { if validatedLabels == nil || len(validatedLabels.ByName) == 0 { - return nil, nil + return nil, nil, nil } - excludeAny := validatedLabels.LabelScope == fleet.LabelScopeExcludeAny labels := make([]fleet.ActivitySoftwareLabel, 0, len(validatedLabels.ByName)) for _, lbl := range validatedLabels.ByName { labels = append(labels, fleet.ActivitySoftwareLabel{ @@ -3094,26 +3108,35 @@ func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdent Name: lbl.LabelName, }) } - if excludeAny { - exclude = labels - } else { - include = labels + switch validatedLabels.LabelScope { + case fleet.LabelScopeIncludeAny: + includeAny = labels + case fleet.LabelScopeExcludeAny: + excludeAny = labels + case fleet.LabelScopeIncludeAll: + includeAll = labels } - return include, exclude + return includeAny, excludeAny, includeAll } -func activitySoftwareLabelsFromSoftwareScopeLabels(includeScopeLabels, excludeScopeLabels []fleet.SoftwareScopeLabel) (include, exclude []fleet.ActivitySoftwareLabel) { - for _, label := range includeScopeLabels { - include = append(include, fleet.ActivitySoftwareLabel{ +func activitySoftwareLabelsFromSoftwareScopeLabels(includeAnyScopeLabels, excludeAnyScopeLabels, includeAllScopeLabels []fleet.SoftwareScopeLabel) (includeAny, excludeAny, includeAll []fleet.ActivitySoftwareLabel) { + for _, label := range includeAnyScopeLabels { + includeAny = append(includeAny, fleet.ActivitySoftwareLabel{ + ID: label.LabelID, + Name: label.LabelName, + }) + } + for _, label := range excludeAnyScopeLabels { + excludeAny = append(excludeAny, fleet.ActivitySoftwareLabel{ ID: label.LabelID, Name: label.LabelName, }) } - for _, label := range excludeScopeLabels { - exclude = append(exclude, fleet.ActivitySoftwareLabel{ + for _, label := range includeAllScopeLabels { + includeAll = append(includeAll, fleet.ActivitySoftwareLabel{ ID: label.LabelID, Name: label.LabelName, }) } - return include, exclude + return includeAny, excludeAny, includeAll } diff --git a/ee/server/service/software_title_icons.go b/ee/server/service/software_title_icons.go index e93653cb534..0af0e104d96 100644 --- a/ee/server/service/software_title_icons.go +++ b/ee/server/service/software_title_icons.go @@ -211,6 +211,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") } @@ -228,6 +229,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") @@ -246,6 +248,7 @@ func generateEditActivityForSoftwareTitleIcon(ctx context.Context, svc *Service, SoftwareIconURL: &iconUrl, LabelsIncludeAny: activityDetailsForSoftwareTitleIcon.LabelsIncludeAny, LabelsExcludeAny: activityDetailsForSoftwareTitleIcon.LabelsExcludeAny, + LabelsIncludeAll: activityDetailsForSoftwareTitleIcon.LabelsIncludeAll, SoftwareTitleID: activityDetailsForSoftwareTitleIcon.SoftwareTitleID, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for software title icon") diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 32fd806c031..11cf3c859ab 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1616,12 +1616,15 @@ func (svc *Service) editTeamFromSpec( if !androidEnabledAndConfigured && len(spec.MDM.AndroidSettings.CustomSettings.Value) > 0 && !fleet.MDMProfileSpecsMatch(team.Config.MDM.AndroidSettings.CustomSettings.Value, spec.MDM.AndroidSettings.CustomSettings.Value) { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("android_settings.configuration_profiles", - `Couldn’t edit android_settings.configuration_profiles. `+fleet.ErrAndroidMDMNotConfigured.Error())) + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("android_settings.custom_settings", + `Couldn’t edit android_settings.custom_settings. `+fleet.ErrAndroidMDMNotConfigured.Error())) } team.Config.MDM.AndroidSettings.CustomSettings = spec.MDM.AndroidSettings.CustomSettings } + if spec.MDM.AndroidSettings.EnrollmentMode != "" { + team.Config.MDM.AndroidSettings.EnrollmentMode = spec.MDM.AndroidSettings.EnrollmentMode + } if spec.Scripts.Set { team.Config.Scripts = spec.Scripts diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index 673506239db..b169e5e0a4b 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -99,6 +99,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.MacOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -112,6 +113,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.IOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -125,6 +127,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: fleet.IPadOSPlatform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, AutoUpdateEnabled: payload.AutoUpdateEnabled, @@ -141,6 +144,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, Platform: payload.Platform, LabelsExcludeAny: payload.LabelsExcludeAny, LabelsIncludeAny: payload.LabelsIncludeAny, + LabelsIncludeAll: payload.LabelsIncludeAll, Categories: payload.Categories, DisplayName: payload.DisplayName, Configuration: payload.Configuration, @@ -184,7 +188,7 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, } } - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating software labels for batch adding vpp app") } @@ -578,7 +582,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee fmt.Sprintf("platform must be one of '%s', '%s', '%s', or '%s'", fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform, fleet.AndroidPlatform)) } - validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, appID.LabelsIncludeAny, appID.LabelsExcludeAny) + validatedLabels, err := ValidateSoftwareLabels(ctx, svc, teamID, appID.LabelsIncludeAny, appID.LabelsExcludeAny, appID.LabelsIncludeAll) if err != nil { return 0, ctxerr.Wrap(ctx, err, "validating software labels for adding vpp app") } @@ -757,7 +761,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee } } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(addedApp.ValidatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(addedApp.ValidatedLabels) act := fleet.ActivityAddedAppStoreApp{ AppStoreID: app.AdamID, @@ -767,8 +771,9 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appID flee SoftwareTitleId: addedApp.TitleID, TeamID: teamID, SelfService: app.SelfService, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, Configuration: app.Configuration, } @@ -912,9 +917,9 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID } var validatedLabels *fleet.LabelIdentsWithScope - if payload.LabelsExcludeAny != nil || payload.LabelsIncludeAny != nil { + if payload.LabelsExcludeAny != nil || payload.LabelsIncludeAny != nil || payload.LabelsIncludeAll != nil { var err error - validatedLabels, err = ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny) + validatedLabels, err = ValidateSoftwareLabels(ctx, svc, teamID, payload.LabelsIncludeAny, payload.LabelsExcludeAny, payload.LabelsIncludeAll) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels") } @@ -995,6 +1000,13 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID for _, l := range meta.LabelsIncludeAny { existingLabels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} } + + case len(meta.LabelsIncludeAll) > 0: + existingLabels.LabelScope = fleet.LabelScopeIncludeAll + existingLabels.ByName = make(map[string]fleet.LabelIdent, len(meta.LabelsIncludeAll)) + for _, l := range meta.LabelsIncludeAll { + existingLabels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} + } } var labelsChanged bool if validatedLabels != nil { @@ -1066,7 +1078,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID } } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(validatedLabels) + actLabelsInclAny, actLabelsExclAny, actLabelsInclAll := activitySoftwareLabelsFromValidatedLabels(validatedLabels) displayNameVal := ptr.ValOrZero(payload.DisplayName) @@ -1078,8 +1090,9 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID SoftwareTitle: meta.Name, AppStoreID: meta.AdamID, Platform: meta.Platform, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + LabelsIncludeAny: actLabelsInclAny, + LabelsExcludeAny: actLabelsExclAny, + LabelsIncludeAll: actLabelsInclAll, SoftwareIconURL: meta.IconURL, SoftwareDisplayName: displayNameVal, Configuration: appToWrite.Configuration, diff --git a/ee/server/service/vpp_test.go b/ee/server/service/vpp_test.go index 74ef2cfa0c3..b81184ccdab 100644 --- a/ee/server/service/vpp_test.go +++ b/ee/server/service/vpp_test.go @@ -32,6 +32,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: "my-fake-app", LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.MacOSPlatform, }, @@ -44,6 +45,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: "my-fake-app", LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.MacOSPlatform, }, @@ -70,6 +72,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: pkg, LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.AndroidPlatform, }, @@ -82,6 +85,7 @@ func TestBatchAssociateVPPApps(t *testing.T) { AppStoreID: pkg, LabelsExcludeAny: []string{}, LabelsIncludeAny: []string{}, + LabelsIncludeAll: []string{}, Categories: []string{}, Platform: fleet.AndroidPlatform, }, diff --git a/frontend/__mocks__/softwareMock.ts b/frontend/__mocks__/softwareMock.ts index a33f37453c6..e2a12f291ea 100644 --- a/frontend/__mocks__/softwareMock.ts +++ b/frontend/__mocks__/softwareMock.ts @@ -159,6 +159,7 @@ const DEFAULT_APP_STORE_APP_MOCK: IAppStoreApp = { failed: 3, }, labels_include_any: null, + labels_include_all: null, labels_exclude_any: null, }; @@ -183,6 +184,7 @@ const DEFAULT_APP_STORE_APP_ANDROID_MOCK: IAppStoreApp = { categories: null, labels_include_any: null, labels_exclude_any: null, + labels_include_all: null, configuration: '{ workProfileWidgets: "WORK_PROFILE_WIDGETS_ALLOWED" }', }; @@ -256,6 +258,7 @@ const DEFAULT_SOFTWARE_PACKAGE_MOCK: ISoftwarePackage = { url: "https://fakeurl.testpackageurlforfalconapp.fake/test/package", hash_sha256: "abcd1234", labels_include_any: null, + labels_include_all: null, labels_exclude_any: null, install_during_setup: undefined, }; diff --git a/frontend/components/icons/ABMIssueHosts.tsx b/frontend/components/icons/ABMIssueHosts.tsx new file mode 100644 index 00000000000..0f249053618 --- /dev/null +++ b/frontend/components/icons/ABMIssueHosts.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +const ABMIssueHosts = () => { + return ( + + + + + + + + + + + ); +}; + +export default ABMIssueHosts; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index 51d6a1c87c4..090bbd10a16 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -29,6 +29,7 @@ import Search from "./Search"; import LowDiskSpaceHosts from "./LowDiskSpaceHosts"; import MissingHosts from "./MissingHosts"; import TotalHosts from "./TotalHosts"; +import ABMIssueHosts from "./ABMIssueHosts"; import Lightbulb from "./Lightbulb"; import Apple from "./Apple"; @@ -97,6 +98,7 @@ export const ICON_MAP = { "low-disk-space-hosts": LowDiskSpaceHosts, "missing-hosts": MissingHosts, "total-hosts": TotalHosts, + "abm-issue-hosts": ABMIssueHosts, lightbulb: Lightbulb, info: Info, "info-outline": InfoOutline, diff --git a/frontend/interfaces/host_summary.ts b/frontend/interfaces/host_summary.ts index 3a16c7a3ada..957793712c8 100644 --- a/frontend/interfaces/host_summary.ts +++ b/frontend/interfaces/host_summary.ts @@ -15,5 +15,6 @@ export interface IHostSummary { new_count: number; missing_30_days_count?: number; // premium feature low_disk_space_count?: number; // premium feature + dep_assign_error_count?: number; // premium feature builtin_labels: ILabelSummary[]; } diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index 044e0ed0821..00af37af595 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -142,6 +142,7 @@ export interface ISoftwarePackage { automatic_install_policies?: ISoftwareInstallPolicy[] | null; install_during_setup?: boolean; labels_include_any: ILabelSoftwareTitle[] | null; + labels_include_all: ILabelSoftwareTitle[] | null; labels_exclude_any: ILabelSoftwareTitle[] | null; categories?: SoftwareCategory[] | null; fleet_maintained_app_id?: number | null; @@ -172,6 +173,7 @@ export interface IAppStoreApp { } | null; version?: string; labels_include_any: ILabelSoftwareTitle[] | null; + labels_include_all: ILabelSoftwareTitle[] | null; labels_exclude_any: ILabelSoftwareTitle[] | null; categories?: SoftwareCategory[] | null; configuration?: string; diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 7d029fecfd3..6724e742b76 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -141,6 +141,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { const [androidCount, setAndroidCount] = useState(0); const [missingCount, setMissingCount] = useState(0); const [lowDiskSpaceCount, setLowDiskSpaceCount] = useState(0); + const [abmIssueCount, setAbmIssueCount] = useState(0); const [showActivityFeedTitle, setShowActivityFeedTitle] = useState(false); const [softwareTitleDetail, setSoftwareTitleDetail] = useState< JSX.Element | string | null @@ -223,6 +224,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { if (isPremiumTier) { setMissingCount(data.missing_30_days_count || 0); setLowDiskSpaceCount(data.low_disk_space_count || 0); + setAbmIssueCount(data.dep_assign_error_count || 0); } const macHosts = data.platforms?.find( (platform: IHostSummaryPlatforms) => platform.platform === "darwin" @@ -590,6 +592,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { isPremiumTier={isPremiumTier} missingCount={missingCount} lowDiskSpaceCount={lowDiskSpaceCount} + abmIssueCount={abmIssueCount} selectedPlatformLabelId={selectedPlatformLabelId} /> diff --git a/frontend/pages/DashboardPage/cards/ABMIssueHosts/ABMIssueHosts.tsx b/frontend/pages/DashboardPage/cards/ABMIssueHosts/ABMIssueHosts.tsx new file mode 100644 index 00000000000..083f5a63ff8 --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ABMIssueHosts/ABMIssueHosts.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import PATHS from "router/paths"; + +import { getPathWithQueryParams } from "utilities/url"; + +import HostCountCard from "../HostCountCard"; + +const baseClass = "hosts-abm-issue"; + +interface IABMIssueHostsProps { + abmIssueCount: number; + selectedPlatformLabelId?: number; + currentTeamId?: number; +} + +export const abmIssueTooltip = (): JSX.Element => { + return ( + + Hosts that have Apple Business Manager (ABM) profile assignment issue. + Migration or new Mac setup won't work. + + ); +}; + +const ABMIssueHosts = ({ + abmIssueCount, + selectedPlatformLabelId, + currentTeamId, +}: IABMIssueHostsProps): JSX.Element | null => { + // build the manage hosts URL filtered by missing and platform + const queryParams = { + dep_profile_error: true, + fleet_id: currentTeamId, + }; + + const endpoint = selectedPlatformLabelId + ? PATHS.MANAGE_HOSTS_LABEL(selectedPlatformLabelId) + : PATHS.MANAGE_HOSTS; + const path = getPathWithQueryParams(endpoint, queryParams); + + return ( + + ); +}; + +export default ABMIssueHosts; diff --git a/frontend/pages/DashboardPage/cards/ABMIssueHosts/index.ts b/frontend/pages/DashboardPage/cards/ABMIssueHosts/index.ts new file mode 100644 index 00000000000..2a7638ef088 --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ABMIssueHosts/index.ts @@ -0,0 +1 @@ +export { default } from "./ABMIssueHosts"; diff --git a/frontend/pages/DashboardPage/cards/HostCountCard/HostCountCard.tsx b/frontend/pages/DashboardPage/cards/HostCountCard/HostCountCard.tsx index 3358b153fae..d08e3e538fd 100644 --- a/frontend/pages/DashboardPage/cards/HostCountCard/HostCountCard.tsx +++ b/frontend/pages/DashboardPage/cards/HostCountCard/HostCountCard.tsx @@ -13,7 +13,7 @@ interface IHostCountCard { title: string; iconName: IconNames; path: string; - tooltip?: string; + tooltip?: JSX.Element | string; notSupported?: boolean; className?: string; iconPosition?: "top" | "left"; diff --git a/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx index 3b3fd67acf3..c9080dfc8d0 100644 --- a/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx +++ b/frontend/pages/DashboardPage/sections/MetricsHostCounts/MetricsHostCounts.tsx @@ -6,6 +6,7 @@ import { PlatformValueOptions } from "utilities/constants"; import LowDiskSpaceHosts from "../../cards/LowDiskSpaceHosts"; import MissingHosts from "../../cards/MissingHosts"; import TotalHosts from "../../cards/TotalHosts"; +import ABMIssueHosts from "../../cards/ABMIssueHosts"; const baseClass = "metrics-host-counts"; @@ -16,6 +17,7 @@ interface IPlatformHostCountsProps { isPremiumTier?: boolean; missingCount: number; lowDiskSpaceCount: number; + abmIssueCount: number; selectedPlatformLabelId?: number; } @@ -26,6 +28,7 @@ const MetricsHostCounts = ({ isPremiumTier, missingCount, lowDiskSpaceCount, + abmIssueCount, selectedPlatformLabelId, }: IPlatformHostCountsProps): JSX.Element => { const TotalHostsCard = ( @@ -54,6 +57,16 @@ const MetricsHostCounts = ({ /> ); + // Does not render if abmIssueCount is 0 or undefined (e.g. on non-Apple platforms views) + // Currently all undefined is defaulted to 0 upstream + const ABMIssueHostsCard = abmIssueCount ? ( + + ) : null; + return (
{selectedPlatform === "all" && TotalHostsCard} @@ -66,6 +79,7 @@ const MetricsHostCounts = ({ {LowDiskSpaceHostsCard} )} + {ABMIssueHostsCard}
); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/EditAutoUpdateConfigModal.tests.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/EditAutoUpdateConfigModal.tests.tsx index 7debb4466cc..5a68e974a9d 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/EditAutoUpdateConfigModal.tests.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/EditAutoUpdateConfigModal.tests.tsx @@ -473,6 +473,7 @@ describe("Edit Auto Update Config Modal", () => { auto_update_enabled: false, labels_include_any: [], labels_exclude_any: [], + labels_include_all: [], fleet_id: 1, }); }); @@ -514,6 +515,7 @@ describe("Edit Auto Update Config Modal", () => { auto_update_window_end: "04:00", labels_include_any: [], labels_exclude_any: [], + labels_include_all: [], fleet_id: 1, }); }); @@ -547,6 +549,7 @@ describe("Edit Auto Update Config Modal", () => { auto_update_enabled: false, labels_include_any: [], labels_exclude_any: [], + labels_include_all: [], fleet_id: 1, }); }); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts index b8a105f75c5..58d54b34ff6 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/helpers.tests.ts @@ -13,6 +13,7 @@ describe("SoftwareTitleDetailsPage helpers", () => { software_package: { labels_include_any: null, labels_exclude_any: null, + labels_include_all: null, name: "TestPackage.pkg", title_id: 2, version: "1.0.0", @@ -79,6 +80,7 @@ describe("SoftwareTitleDetailsPage helpers", () => { icon_url: "https://example.com/icon.png", labels_exclude_any: null, labels_include_any: null, + labels_include_all: null, }, source: "apps", hosts_count: 10, diff --git a/frontend/pages/SoftwarePage/components/icons/Firefox.tsx b/frontend/pages/SoftwarePage/components/icons/Firefox.tsx index 6b0473dc862..8ff67dbb6fc 100644 --- a/frontend/pages/SoftwarePage/components/icons/Firefox.tsx +++ b/frontend/pages/SoftwarePage/components/icons/Firefox.tsx @@ -1,180 +1,14 @@ -import React from "react"; +import * as React from "react"; -import { uniqueId } from "lodash"; import type { SVGProps } from "react"; -const Firefox = (props: SVGProps) => { - // Create unique IDs for the SVG gradients - const gradientA = uniqueId("gradient-"); - const gradientB = uniqueId("gradient-"); - const gradientC = uniqueId("gradient-"); - const gradientD = uniqueId("gradient-"); - const gradientE = uniqueId("gradient-"); - const gradientF = uniqueId("gradient-"); - const gradientG = uniqueId("gradient-"); - const gradientH = uniqueId("gradient-"); - const gradientI = uniqueId("gradient-"); - const gradientJ = uniqueId("gradient-"); - const gradientK = uniqueId("gradient-"); - const gradientL = uniqueId("gradient-"); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; +const Firefox = (props: SVGProps) => ( + + + +); export default Firefox; diff --git a/frontend/pages/SoftwarePage/helpers.tsx b/frontend/pages/SoftwarePage/helpers.tsx index 6d005e1148b..a20c0c37daa 100644 --- a/frontend/pages/SoftwarePage/helpers.tsx +++ b/frontend/pages/SoftwarePage/helpers.tsx @@ -83,6 +83,7 @@ export const getTargetType = ( if (!softwareInstaller) return "All hosts"; return !softwareInstaller.labels_include_any && + !softwareInstaller.labels_include_all && !softwareInstaller.labels_exclude_any ? "All hosts" : "Custom"; @@ -94,9 +95,9 @@ export const getCustomTarget = ( ) => { if (!softwareInstaller) return "labelsIncludeAny"; - return softwareInstaller.labels_include_any - ? "labelsIncludeAny" - : "labelsExcludeAny"; + if (softwareInstaller.labels_include_any) return "labelsIncludeAny"; + if (softwareInstaller.labels_include_all) return "labelsIncludeAll"; + return "labelsExcludeAny"; }; // Used in EditSoftwareModal and PackageForm @@ -106,14 +107,23 @@ export const generateSelectedLabels = ( if ( !softwareInstaller || (!softwareInstaller.labels_include_any && + !softwareInstaller.labels_include_all && !softwareInstaller.labels_exclude_any) ) { return {}; } - const customTypeKey = softwareInstaller.labels_include_any - ? "labels_include_any" - : "labels_exclude_any"; + let customTypeKey: + | "labels_include_any" + | "labels_include_all" + | "labels_exclude_any"; + if (softwareInstaller.labels_include_any) { + customTypeKey = "labels_include_any"; + } else if (softwareInstaller.labels_include_all) { + customTypeKey = "labels_include_all"; + } else { + customTypeKey = "labels_exclude_any"; + } return ( softwareInstaller[customTypeKey]?.reduce>( @@ -145,7 +155,21 @@ export const generateHelpText = ( ); } - // this is the case for labelsExcludeAny + if (customTarget === "labelsIncludeAll") { + return !automaticInstall ? ( + <> + Software will only be available for install on hosts that{" "} + have all of these labels: + + ) : ( + <> + Software will only be installed on hosts that have all of these + labels: + + ); + } + + // labelsExcludeAny return !automaticInstall ? ( <> Software will only be available for install on hosts that{" "} @@ -164,11 +188,19 @@ export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [ { value: "labelsIncludeAny", label: "Include any", + helpText: "Hosts that have any of the selected labels.", + disabled: false, + }, + { + value: "labelsIncludeAll", + label: "Include all", + helpText: "Hosts that have all of the selected labels.", disabled: false, }, { value: "labelsExcludeAny", label: "Exclude any", + helpText: "Hosts that don't have any of the selected labels.", disabled: false, }, ]; diff --git a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/NDESForm/NDESForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/NDESForm/NDESForm.tsx index 0a79953c489..ffa3f662ced 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/NDESForm/NDESForm.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/CertificateAuthorities/components/NDESForm/NDESForm.tsx @@ -88,7 +88,8 @@ const NDESForm = ({ onChange={onInputChange} parseTarget placeholder="username@example.microsoft.com" - helpText="For NDES, this is the username in the down-level logon name format required to log in to the SCEP admin page." + helpText="For NDES, this is the username in the down-level logon name + format required to log in to the SCEP admin page. Okta generates this for you." /> For NDES, the password required to log in to the{" "} - Network Device Enrollment Service page. + Network Device Enrollment Service page. Okta generates this + for you. } /> diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index b1a66ad3ff1..317a7fcb4a4 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -81,6 +81,7 @@ import { PolicyResponse, } from "utilities/constants"; import { getNextLocationPath } from "utilities/helpers"; +import { strToBool } from "utilities/strings/stringUtils"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; @@ -311,6 +312,7 @@ const ManageHostsPage = ({ const scriptBatchExecutionStatus: ScriptBatchHostCountV1 = queryParams?.[HOSTS_QUERY_PARAMS.SCRIPT_BATCH_EXECUTION_STATUS] ?? (scriptBatchExecutionId ? "ran" : undefined); + const depProfileError = queryParams?.dep_profile_error; // ========= routeParams const { active_label: activeLabel, label_id: labelID } = routeParams; @@ -351,6 +353,7 @@ const ManageHostsPage = ({ // scriptBatchExecutionStatus // configProfileStatus || // configProfileUUID + // depProfileError const runScriptBatchFilterNotSupported = !!( // all above, except acceptable filters @@ -388,7 +391,8 @@ const ManageHostsPage = ({ scriptBatchExecutionId || scriptBatchExecutionStatus || configProfileStatus || - configProfileUUID + configProfileUUID || + depProfileError ) ); @@ -572,6 +576,7 @@ const ManageHostsPage = ({ configProfileUUID, scriptBatchExecutionStatus, scriptBatchExecutionId, + depProfileError: strToBool(depProfileError), }, ], ({ queryKey }) => hostsAPI.loadHosts(queryKey[0]), @@ -1098,6 +1103,8 @@ const ManageHostsPage = ({ newQueryParams[ HOSTS_QUERY_PARAMS.SCRIPT_BATCH_EXECUTION_ID ] = scriptBatchExecutionId; + } else if (depProfileError) { + newQueryParams.dep_profile_error = depProfileError; } router.replace( @@ -1337,6 +1344,7 @@ const ManageHostsPage = ({ osSettings: osSettingsStatus, diskEncryptionStatus, vulnerability, + depProfileError, }) : hostsAPI.transferToTeam(teamId, selectedHostIds); @@ -1839,7 +1847,8 @@ const ManageHostsPage = ({ lowDiskSpaceHosts || osSettingsStatus || diskEncryptionStatus || - vulnerability + vulnerability || + depProfileError ); return ( @@ -1986,6 +1995,7 @@ const ManageHostsPage = ({ scriptBatchExecutionId, scriptBatchRanAt: scriptBatchSummary?.created_at || null, scriptBatchScriptName: scriptBatchSummary?.script_name || null, + depProfileError, }} selectedLabel={selectedLabel} isOnlyObserver={isOnlyObserver} diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx index d13dd566fa5..73110cc1fdc 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tests.tsx @@ -1,7 +1,6 @@ import React from "react"; import { noop } from "lodash"; -import { render, screen, within } from "@testing-library/react"; - +import { render, screen, within, waitFor } from "@testing-library/react"; import { renderWithSetup } from "test/test-utils"; import FilterPill from "./FilterPill"; @@ -21,8 +20,8 @@ describe("Filter Pill Component", () => { ).toBeInTheDocument(); }); - it("renders a passed in string tooltip", () => { - render( + it("renders a passed in string tooltip", async () => { + const { user } = renderWithSetup( { /> ); - expect(screen.getByText("Test Tooltip")).toBeInTheDocument(); + await user.hover(screen.getByText("Test Pill")); + await waitFor(() => { + const tooltip = screen.getByText("Test Tooltip"); + expect(tooltip).toBeInTheDocument(); + }); }); - it("renders a passed in ReactNode tooltip", () => { - render( + it("renders a passed in ReactNode tooltip", async () => { + const { user } = renderWithSetup( This is a ReactNode

} @@ -42,7 +45,11 @@ describe("Filter Pill Component", () => { /> ); - expect(screen.getByText("This is a ReactNode")).toBeInTheDocument(); + await user.hover(screen.getByText("Test Pill")); + await waitFor(() => { + const tooltip = screen.getByText("This is a ReactNode"); + expect(tooltip).toBeInTheDocument(); + }); }); it("calls the onCancel callback when a user clicks on the remove button", async () => { diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx index ed3ec314886..b3133497bcf 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/FilterPill.tsx @@ -1,13 +1,13 @@ -import React, { ReactNode, useRef } from "react"; -import ReactTooltip from "react-tooltip"; +import React, { ReactNode, useRef, useState } from "react"; import classnames from "classnames"; import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement"; -import { COLORS } from "styles/var/colors"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; import { IconNames } from "components/icons"; +import TooltipWrapper from "components/TooltipWrapper"; +import { set } from "lodash"; interface IFilterPillProps { label: string; @@ -33,10 +33,30 @@ const FilterPill = ({ const pillText = useRef(null); const isTruncated = useCheckTruncatedElement(pillText); + const [tooltipContent, setTooltipContent] = useState(tooltipDescription); // if tooltipDescription not provided, behave like TooltipTruncatedText - const tooltipContent = - tooltipDescription ?? (isTruncated ? label : undefined); + if (isTruncated && !tooltipContent) { + setTooltipContent(label); + } + + const labelWithTooltip = tooltipContent ? ( + + + {label} + + + ) : ( + + {label} + + ); return (
{icon && } - - {label} - + {labelWithTooltip}
- {tooltipContent && ( - - {tooltipContent} - - )}
); diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss index 75a357af29a..3603e130d46 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss @@ -26,6 +26,7 @@ } &__tooltip-text { + display: block; @include ellipse-text(166px); } } diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index f9db2711667..86af432f59a 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -38,6 +38,7 @@ import { import Dropdown from "components/forms/fields/Dropdown"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; +import { abmIssueTooltip } from "pages/DashboardPage/cards/ABMIssueHosts/ABMIssueHosts"; import FilterPill from "../FilterPill"; import PoliciesFilter from "../PoliciesFilter"; @@ -92,6 +93,7 @@ interface IHostsFilterBlockProps { scriptBatchExecutionId?: string; scriptBatchRanAt: string | null; scriptBatchScriptName: string | null; + depProfileError: string; // string "true" as we don't handle booleans }; selectedLabel?: ILabel; isOnlyObserver?: boolean; @@ -153,6 +155,7 @@ const HostsFilterBlock = ({ scriptBatchExecutionId, scriptBatchRanAt, scriptBatchScriptName, + depProfileError, }, selectedLabel, isOnlyObserver, @@ -604,6 +607,17 @@ const HostsFilterBlock = ({ ); }; + const renderDepProfileError = () => { + return ( + handleClearFilter(["dep_profile_error"])} + /> + ); + }; + const showSelectedLabel = selectedLabel && selectedLabel.type !== "all" && @@ -628,7 +642,8 @@ const HostsFilterBlock = ({ bootstrapPackageStatus || vulnerability || (configProfileStatus && configProfileUUID && configProfile) || - (scriptBatchExecutionStatus && scriptBatchExecutionId) + (scriptBatchExecutionStatus && scriptBatchExecutionId) || + depProfileError ) { const renderFilterPill = () => { switch (true) { @@ -702,6 +717,8 @@ const HostsFilterBlock = ({ return renderConfigProfileStatusBlock(); case !!scriptBatchExecutionStatus && !!scriptBatchExecutionId: return renderScriptBatchExecutionBlock(); + case !!depProfileError: + return renderDepProfileError(); default: return null; } diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss index cd1dbdf8af5..fec6acaf86e 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/_styles.scss @@ -41,3 +41,11 @@ gap: $pad-small; } } +.hosts-filter-block__abm-issue-filter-pill { + .filter-pill__tooltip-text { + max-width: min-content; // Fit pill text without truncation + } + .react-tooltip { + max-width: 250px; + } +} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 19762c250ab..85f331c8696 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -1730,7 +1730,7 @@ describe("Host Actions Dropdown", () => { await user.hover(option); await waitFor(() => { expect( - screen.getByText(/Recovery Lock password is not available/i) + screen.getByText(/Recovery Lock password is unavailable/i) ).toBeInTheDocument(); }); }); diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 94c51573c90..777e4d8470f 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -542,7 +542,13 @@ const modifyOptions = ( ); if (rlpOption) { rlpOption.disabled = true; - rlpOption.tooltipContent = <>Recovery Lock password is not available.; + rlpOption.tooltipContent = ( + <> + Recovery Lock password is unavailable +
+ while pending or has failed. + + ); } } diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 13c37ca4006..2f4b1e3c4c9 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -34,12 +34,14 @@ import { TooltipContent } from "interfaces/dropdownOption"; import configAPI from "services/entities/config"; import globalPoliciesAPI, { + GlobalPoliciesAutomationType, IPoliciesCountQueryKey, IPoliciesQueryKey, } from "services/entities/global_policies"; import teamPoliciesAPI, { ITeamPoliciesCountQueryKey, ITeamPoliciesQueryKey, + AutomationType, } from "services/entities/team_policies"; import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; @@ -88,6 +90,7 @@ interface IManagePoliciesPageProps { order_key?: string; order_direction?: "asc" | "desc"; page?: string; + automation_type?: AutomationType; }; search: string; }; @@ -104,6 +107,16 @@ const [ "Could not update policy automations.", ]; +const AUTOMATION_TYPES: AutomationType[] = [ + "software", + "scripts", + "calendar", + "conditional_access", + "other", +]; + +const GLOBAL_AUTOMATION_TYPES: GlobalPoliciesAutomationType[] = ["other"]; + const baseClass = "manage-policies-page"; const ManagePolicyPage = ({ @@ -194,6 +207,21 @@ const ManagePolicyPage = ({ DEFAULT_SORT_DIRECTION)(); const page = queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0; + const initialAutomationFilter = (() => { + const automationQueryParam = queryParams.automation_type; + + if (!automationQueryParam) { + return null; + } + + const validValues = isAllTeamsSelected + ? GLOBAL_AUTOMATION_TYPES + : AUTOMATION_TYPES; + + return (validValues as string[]).includes(automationQueryParam) + ? automationQueryParam + : null; + })(); // Needs update on location change or table state might not match URL const [searchQuery, setSearchQuery] = useState(initialSearchQuery); @@ -205,16 +233,9 @@ const ManagePolicyPage = ({ const [sortDirection, setSortDirection] = useState< "asc" | "desc" | undefined >(initialSortDirection); - const [automationFilter, setAutomationFilter] = useState(null); - - // Maps frontend dropdown values to backend automation_type query param values - const AUTOMATION_FILTER_TO_API: Record = { - install_software: "software", - run_script: "scripts", - calendar_events: "calendar", - conditional_access: "conditional_access", - other_workflows: "other", - }; + const [automationFilter, setAutomationFilter] = useState< + AutomationType | GlobalPoliciesAutomationType | null + >(initialAutomationFilter); useEffect(() => { setLastEditedQueryPlatform(null); @@ -227,12 +248,14 @@ const ManagePolicyPage = ({ setSearchQuery(initialSearchQuery); setSortHeader(initialSortHeader); setSortDirection(initialSortDirection); + setAutomationFilter(initialAutomationFilter); }, [ location, isRouteOk, initialSearchQuery, initialSortHeader, initialSortDirection, + initialAutomationFilter, ]); useEffect(() => { @@ -271,6 +294,7 @@ const ManagePolicyPage = ({ query: searchQuery, orderDirection: sortDirection, orderKey: sortHeader, + automationType: automationFilter as GlobalPoliciesAutomationType, }, ], ({ queryKey }) => { @@ -292,6 +316,7 @@ const ManagePolicyPage = ({ { scope: "policiesCount", query: !isAllTeamsSelected ? "" : searchQuery, + automationType: automationFilter as GlobalPoliciesAutomationType, }, ], ({ queryKey }) => globalPoliciesAPI.getCount(queryKey[0]), @@ -327,9 +352,7 @@ const ManagePolicyPage = ({ teamId: teamIdForApi || 0, // no teams does inherit mergeInherited: true, - automationType: automationFilter - ? AUTOMATION_FILTER_TO_API[automationFilter] - : undefined, + automationType: automationFilter as AutomationType, }, ], ({ queryKey }) => { @@ -357,6 +380,7 @@ const ManagePolicyPage = ({ query: searchQuery, teamId: teamIdForApi || 0, // TODO: Fix number/undefined type mergeInherited: true, + automationType: automationFilter as AutomationType, }, ], ({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]), @@ -929,6 +953,21 @@ const ManagePolicyPage = ({ toggleDeletePoliciesModal, ]); + const onChangeAutomationFilter = (val: SingleValue) => { + const automationType = val?.value; + + const locationPath = getNextLocationPath({ + pathPrefix: PATHS.MANAGE_POLICIES, + queryParams: { + ...queryParams, + page: "0", + automation_type: automationType === "all" ? undefined : automationType, + }, + }); + + router?.push(locationPath); + }; + const policiesErrors = !isAllTeamsSelected ? teamPoliciesError : globalPoliciesError; @@ -974,7 +1013,7 @@ const ManagePolicyPage = ({ count?: number, policies?: IPolicyStats[] ) => { - // Hide count if fetching count || there are errors OR there are no policy results with no a search filter + // Hide count if fetching count || there are errors OR there are no policy results with no filters (search or automation dropdown) const isFetchingCount = !isAllTeamsSelected ? isFetchingTeamCountMergeInherited : isFetchingGlobalCount; @@ -982,7 +1021,7 @@ const ManagePolicyPage = ({ const hide = isFetchingCount || policiesErrors || - (!policyResults && searchQuery === ""); + (!policyResults && searchQuery === "" && !automationFilter); if (hide) { return null; @@ -1009,61 +1048,80 @@ const ManagePolicyPage = ({ ); }; - // Client-side filtering is still needed for global policies since the - // global policies endpoint does not support automation_type. Team policies - // use the server-side automation_type query param instead. - const filterGlobalPoliciesByAutomation = ( - policies: IPolicyStats[] - ): IPolicyStats[] => { - if (!automationFilter) return policies; - return policies.filter((p) => { - switch (automationFilter) { - case "install_software": - return !!p.install_software; - case "run_script": - return !!p.run_script; - case "calendar_events": - return p.calendar_events_enabled; - case "conditional_access": - return p.conditional_access_enabled; - case "other_workflows": - return currentAutomatedPolicies.includes(p.id); - default: - return true; - } - }); - }; - const automationFilterOptions: CustomOptionType[] = [ - { label: "All automations", value: "all" }, - { label: "Software", value: "install_software" }, - { label: "Scripts", value: "run_script" }, - { label: "Calendar", value: "calendar_events" }, - { label: "Conditional access", value: "conditional_access" }, - { label: "Other", value: "other_workflows" }, + { + label: "All policies", + value: "all", + helpText: "All policies added to Fleet.", + }, + { + label: "Software", + value: "software", + helpText: "Policies with software automation enabled.", + }, + { + label: "Scripts", + value: "scripts", + helpText: "Policies with script automation enabled.", + }, + { + label: "Calendar", + value: "calendar", + helpText: "Policies with calendar event automation enabled.", + }, + { + label: "Conditional access", + value: "conditional_access", + helpText: "Policies with conditional access automation enabled.", + }, + { + label: "Other", + value: "other", + helpText: "Policies with other automation enabled.", + }, ]; + const allPoliciesOption = automationFilterOptions[0]; // value: "all" + + const getSelectedFilterOption = () => { + if (!automationFilter) { + return allPoliciesOption; // Default to all policies option + } + return automationFilterOptions.find( + (opt) => opt.value === automationFilter + ); + }; + const renderAutomationFilter = isPremiumTier - ? () => ( - ) => { - const newFilter = - val?.value && val.value !== "all" ? val.value : null; - setAutomationFilter(newFilter); - // Reset to first page when filter changes - const locationPath = getNextLocationPath({ - pathPrefix: PATHS.MANAGE_POLICIES, - queryParams: { ...queryParams, page: "0" }, - }); - router?.push(locationPath); - }} - placeholder="Filter by automation" - options={automationFilterOptions} - variant="table-filter" - /> - ) + ? () => { + // Hide dropdown if there are errors OR there are no policy results with no filters (search or automation dropdown) + const hide = + policiesErrors || + (!policyResults && searchQuery === "" && !automationFilter); + + if (hide) { + return null; + } + + // No team ID = All fleets → only show "all" and "other" options + const optionsForTeam = teamIdForApi + ? automationFilterOptions + : automationFilterOptions.filter((opt) => + ["all", "other"].includes(opt.value as string) + ); + + return ( + + ); + } : undefined; const renderMainTable = () => { @@ -1076,15 +1134,9 @@ const ManagePolicyPage = ({ if (globalPoliciesError) { return ; } - const filteredGlobalPolicies = filterGlobalPoliciesByAutomation( - globalPolicies || [] - ); - const filteredGlobalCount = automationFilter - ? filteredGlobalPolicies.length - : globalPoliciesCount || 0; return ( renderPoliciesCountAndLastUpdated( - filteredGlobalCount, - filteredGlobalPolicies + globalPoliciesCount, + globalPolicies ) } - count={filteredGlobalCount} + count={globalPoliciesCount || 0} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} @@ -1115,11 +1167,7 @@ const ManagePolicyPage = ({ return ; } const displayedTeamPolicies = teamPolicies || []; - // When a filter is active, use the returned array length as the count - // since the count endpoint doesn't support automation_type yet. - const displayedTeamCount = automationFilter - ? displayedTeamPolicies.length - : teamPoliciesCountMergeInherited || 0; + return (
renderPoliciesCountAndLastUpdated( - displayedTeamCount, + teamPoliciesCountMergeInherited, displayedTeamPolicies ) } isPremiumTier={isPremiumTier} - count={displayedTeamCount} + count={teamPoliciesCountMergeInherited || 0} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index 576b9290d12..3c34b40e154 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -20,7 +20,7 @@ } &__filter-automation-dropdown { - min-width: 200px; + min-width: 277px; } &__manage-automations-wrapper { diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx index 8313f67c5e9..c6f65003583 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx @@ -21,7 +21,9 @@ import teamPoliciesAPI, { IPoliciesApiParams, IPoliciesCountApiParams, } from "services/entities/team_policies"; -import globalPoliciesAPI from "services/entities/global_policies"; +import globalPoliciesAPI, { + IGlobalPoliciesApiQueryParams, +} from "services/entities/global_policies"; import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; import { QueryablePlatform, isQueryablePlatform } from "interfaces/platform"; @@ -164,10 +166,12 @@ function PoliciesPaginatedList( orderDirection: "asc" as const, orderKey: DEFAULT_SORT_COLUMN, teamId, + automationType: undefined, }; countQueryKey = { query: "", teamId, + automationType: undefined, }; } else { policiesQueryKey = { @@ -178,11 +182,13 @@ function PoliciesPaginatedList( orderKey: DEFAULT_SORT_COLUMN, teamId, mergeInherited: false, + automationType: undefined, }; countQueryKey = { query: "", teamId, mergeInherited: false, + automationType: undefined, }; } @@ -206,11 +212,18 @@ function PoliciesPaginatedList( ILoadAllPoliciesResponse, Error, IFormPolicy[] - >([policiesQueryKey], () => globalPoliciesAPI.loadAllNew(policiesQueryKey), { - enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, - keepPreviousData: true, - select: marshallApiResponse, - }); + >( + [policiesQueryKey], + () => + globalPoliciesAPI.loadAllNew( + policiesQueryKey as IGlobalPoliciesApiQueryParams + ), + { + enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, + keepPreviousData: true, + select: marshallApiResponse, + } + ); // Team policies query const { data: teamData, isFetching: teamIsLoading } = useQuery< @@ -232,10 +245,20 @@ function PoliciesPaginatedList( IPoliciesCountResponse, Error, number - >([countQueryKey], () => globalPoliciesAPI.getCount(countQueryKey), { - enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, - select: (countResponse: IPoliciesCountResponse) => countResponse.count, - }); + >( + [countQueryKey], + () => + globalPoliciesAPI.getCount( + countQueryKey as Pick< + IGlobalPoliciesApiQueryParams, + "query" | "automationType" + > + ), + { + enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, + select: (countResponse: IPoliciesCountResponse) => countResponse.count, + } + ); // Team count query const { data: teamCount, isFetching: teamIsFetchingCount } = useQuery< diff --git a/frontend/services/entities/global_policies.ts b/frontend/services/entities/global_policies.ts index 0cf44c0ca4b..25e414b436b 100644 --- a/frontend/services/entities/global_policies.ts +++ b/frontend/services/entities/global_policies.ts @@ -11,21 +11,28 @@ import { buildQueryStringFromParams, convertParamsToSnakeCase, } from "utilities/url"; +import { AutomationType } from "./team_policies"; -interface IPoliciesApiParams { +export type GlobalPoliciesAutomationType = Exclude< + AutomationType, + "software" | "scripts" | "conditional_access" | "calendar" +>; + +export interface IGlobalPoliciesApiQueryParams { page?: number; perPage?: number; orderKey?: string; orderDirection?: "asc" | "desc"; query?: string; + automationType?: GlobalPoliciesAutomationType; } -export interface IPoliciesQueryKey extends IPoliciesApiParams { +export interface IPoliciesQueryKey extends IGlobalPoliciesApiQueryParams { scope: "globalPolicies"; } export interface IPoliciesCountQueryKey - extends Pick { + extends Pick { scope: "policiesCount"; } @@ -68,7 +75,8 @@ export default { orderKey = ORDER_KEY, orderDirection: orderDir = ORDER_DIRECTION, query, - }: IPoliciesApiParams): Promise => { + automationType, + }: IGlobalPoliciesApiQueryParams): Promise => { const { GLOBAL_POLICIES } = endpoints; const queryParams = { @@ -77,6 +85,7 @@ export default { orderKey, orderDirection: orderDir, query, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); @@ -87,11 +96,16 @@ export default { }, getCount: ({ query, - }: Pick): Promise => { + automationType, + }: Pick< + IGlobalPoliciesApiQueryParams, + "query" | "automationType" + >): Promise => { const { GLOBAL_POLICIES } = endpoints; const path = `${GLOBAL_POLICIES}/count`; const queryParams = { query, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); const queryString = buildQueryStringFromParams(snakeCaseParams); diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index b46175fbffe..0ad95f625e8 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -103,6 +103,7 @@ export interface ILoadHostsOptions { configProfileUUID?: string; scriptBatchExecutionStatus?: ScriptBatchHostCountV1; scriptBatchExecutionId?: string; + depProfileError?: boolean; } export interface IExportHostsOptions { @@ -139,6 +140,7 @@ export interface IExportHostsOptions { configProfileStatus?: string; scriptBatchExecutionStatus?: ScriptBatchHostCountV1; scriptBatchExecutionId?: string; + depProfileError?: boolean; } export interface IActionByFilter { @@ -167,6 +169,7 @@ export interface IActionByFilter { vulnerability?: string; scriptBatchExecutionStatus?: ScriptBatchHostCountV1; scriptBatchExecutionId?: string; + depProfileError?: boolean; } export interface IGetHostSoftwareResponse { @@ -360,11 +363,11 @@ export default { const configProfileStatus = options?.configProfileStatus; const scriptBatchExecutionStatus = options?.scriptBatchExecutionStatus; const scriptBatchExecutionId = options?.scriptBatchExecutionId; + const depProfileError = options?.depProfileError; if (!sortBy.length) { throw Error("sortBy is a required field."); } - const queryParams = { order_key: sortBy[0].key, order_direction: sortBy[0].direction, @@ -399,6 +402,7 @@ export default { configProfileStatus, scriptBatchExecutionStatus, scriptBatchExecutionId, + depProfileError, }), status, label_id: label, @@ -444,6 +448,7 @@ export default { configProfileUUID, scriptBatchExecutionStatus, scriptBatchExecutionId, + depProfileError, }: ILoadHostsOptions): Promise => { const label = getLabel(selectedLabels); const sortParams = getSortParams(sortBy); @@ -486,6 +491,7 @@ export default { configProfileUUID, scriptBatchExecutionStatus, scriptBatchExecutionId, + depProfileError, }), }; @@ -556,6 +562,7 @@ export default { osSettings, diskEncryptionStatus, vulnerability, + depProfileError, }: IActionByFilter) => { const { HOSTS_TRANSFER_BY_FILTER } = endpoints; return sendRequest("POST", HOSTS_TRANSFER_BY_FILTER, { @@ -583,6 +590,7 @@ export default { os_settings: osSettings, os_settings_disk_encryption: diskEncryptionStatus, vulnerability, + dep_profile_error: depProfileError, }, }); }, diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index e3542d7217f..ebd60aa310e 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -162,6 +162,7 @@ interface IAddFleetMaintainedAppPostBody { self_service?: boolean; automatic_install?: boolean; labels_include_any?: string[]; + labels_include_all?: string[]; labels_exclude_any?: string[]; categories: string[]; } @@ -175,6 +176,7 @@ export interface IAddAppStoreAppPostBody { // No automatic_install on add Android app automatic_install?: boolean; labels_include_any?: string[]; + labels_include_all?: string[]; labels_exclude_any?: string[]; categories?: SoftwareCategory[]; } @@ -185,6 +187,7 @@ export interface IEditAppStoreAppPostBody { self_service?: boolean; // No automatic_install on edit VPP or android app labels_include_any?: string[]; + labels_include_all?: string[]; labels_exclude_any?: string[]; categories?: SoftwareCategory[]; display_name?: string; @@ -219,6 +222,8 @@ const handleAndroidForm = ( const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets); if (formData.customTarget === "labelsIncludeAny") { body.labels_include_any = selectedLabels; + } else if (formData.customTarget === "labelsIncludeAll") { + body.labels_include_all = selectedLabels; } else { body.labels_exclude_any = selectedLabels; } @@ -250,6 +255,8 @@ const handleVppAppForm = (teamId: number, formData: ISoftwareVppFormData) => { const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets); if (formData.customTarget === "labelsIncludeAny") { body.labels_include_any = selectedLabels; + } else if (formData.customTarget === "labelsIncludeAll") { + body.labels_include_all = selectedLabels; } else { body.labels_exclude_any = selectedLabels; } @@ -299,6 +306,8 @@ const handleEditPackageForm = ( if (data.targetType === "All hosts") { if (orignalPackage.labels_include_any) { formData.append("labels_include_any", ""); + } else if (orignalPackage.labels_include_all) { + formData.append("labels_include_all", ""); } else { formData.append("labels_exclude_any", ""); } @@ -310,6 +319,8 @@ const handleEditPackageForm = ( let labelKey = ""; if (data.customTarget === "labelsIncludeAny") { labelKey = "labels_include_any"; + } else if (data.customTarget === "labelsIncludeAll") { + labelKey = "labels_include_all"; } else { labelKey = "labels_exclude_any"; } @@ -346,12 +357,15 @@ const handleAutoUpdateConfigAppStoreAppForm = ( const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets); if (formData.customTarget === "labelsIncludeAny") { body.labels_include_any = selectedLabels; + } else if (formData.customTarget === "labelsIncludeAll") { + body.labels_include_all = selectedLabels; } else { body.labels_exclude_any = selectedLabels; } } else { body.labels_exclude_any = []; body.labels_include_any = []; + body.labels_include_all = []; } }; @@ -371,12 +385,15 @@ const handleEditAppStoreAppForm = ( const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets); if (formData.customTarget === "labelsIncludeAny") { body.labels_include_any = selectedLabels; + } else if (formData.customTarget === "labelsIncludeAll") { + body.labels_include_all = selectedLabels; } else { body.labels_exclude_any = selectedLabels; } } else { body.labels_exclude_any = []; body.labels_include_any = []; + body.labels_include_all = []; } }; @@ -552,6 +569,8 @@ export default { let labelKey = ""; if (data.customTarget === "labelsIncludeAny") { labelKey = "labels_include_any"; + } else if (data.customTarget === "labelsIncludeAll") { + labelKey = "labels_include_all"; } else { labelKey = "labels_exclude_any"; } @@ -796,6 +815,8 @@ export default { const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets); if (formData.customTarget === "labelsIncludeAny") { body.labels_include_any = selectedLabels; + } else if (formData.customTarget === "labelsIncludeAll") { + body.labels_include_all = selectedLabels; } else { body.labels_exclude_any = selectedLabels; } diff --git a/frontend/services/entities/team_policies.ts b/frontend/services/entities/team_policies.ts index 6661cf2f238..39614a5ae71 100644 --- a/frontend/services/entities/team_policies.ts +++ b/frontend/services/entities/team_policies.ts @@ -11,6 +11,14 @@ import { } from "interfaces/policy"; import { API_NO_TEAM_ID } from "interfaces/team"; import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import { GlobalPoliciesAutomationType } from "./global_policies"; + +export type AutomationType = + | "software" + | "scripts" + | "calendar" + | "conditional_access" + | "other"; interface IPoliciesApiQueryParams { page?: number; @@ -18,7 +26,7 @@ interface IPoliciesApiQueryParams { orderKey?: string; orderDirection?: "asc" | "desc"; query?: string; - automationType?: string; + automationType?: AutomationType | GlobalPoliciesAutomationType; } export interface IPoliciesApiParams extends IPoliciesApiQueryParams { @@ -31,7 +39,10 @@ export interface ITeamPoliciesQueryKey extends IPoliciesApiParams { } export interface ITeamPoliciesCountQueryKey - extends Pick { + extends Pick< + IPoliciesApiParams, + "query" | "teamId" | "mergeInherited" | "automationType" + > { scope: "teamPoliciesCountMergeInherited" | "teamPoliciesCount"; } @@ -39,6 +50,7 @@ export interface IPoliciesCountApiParams { teamId: number; query?: string; mergeInherited?: boolean; + automationType?: AutomationType; } const ORDER_KEY = "name"; @@ -179,15 +191,17 @@ export default { query, teamId, mergeInherited = true, + automationType, }: Pick< IPoliciesCountApiParams, - "query" | "teamId" | "mergeInherited" + "query" | "teamId" | "mergeInherited" | "automationType" >): Promise => { const { TEAM_POLICIES } = endpoints; const path = `${TEAM_POLICIES(teamId)}/count`; const queryParams = { query, mergeInherited, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); const queryString = buildQueryStringFromParams(snakeCaseParams); diff --git a/frontend/templates/enroll-ota.html b/frontend/templates/enroll-ota.html index c620ccc07d4..ed0868a973e 100644 --- a/frontend/templates/enroll-ota.html +++ b/frontend/templates/enroll-ota.html @@ -214,7 +214,7 @@

How to enroll your device to Fleet

- + + + +