diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
index 45439ae92c..7955082fc7 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -11,6 +11,8 @@ assignees: ''
Thanks for filing an issue! Please use the prompts below to provide as much context as you can about your use case and motivations.
-->
+Gong snippet: TODO
+
## Problem
-- [ ] UI changes: TODO
-- [ ] CLI usage changes: TODO
-- [ ] REST API changes: TODO
-- [ ] Fleet's agent (fleetd) changes: TODO
-- [ ] Permissions changes: TODO
-- [ ] Changes to paid features or tiers: TODO
+- [ ] UI changes: TODO
+- [ ] CLI (fleetctl) usage changes: TODO
+- [ ] YAML changes: TODO
+- [ ] REST API changes: TODO
+- [ ] Fleet's agent (fleetd) changes: TODO
+- [ ] Activity changes: TODO
+- [ ] Permissions changes: TODO
+- [ ] Changes to paid features or tiers: TODO
+- [ ] Other reference documentation changes: TODO
+- [ ] Once shipped, requester has been notified
### Engineering
- [ ] Feature guide changes: TODO
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index af1f4818cf..cf6e9c46e2 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -9,6 +9,7 @@ If some of the following don't apply, delete the relevant line.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.
- [ ] Added/updated tests
+- [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration.
diff --git a/.github/workflows/build-and-push-fleetctl-docker.yml b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml
similarity index 51%
rename from .github/workflows/build-and-push-fleetctl-docker.yml
rename to .github/workflows/build-and-check-fleetctl-docker-and-deps.yml
index 8ae3c7069e..ff20260409 100644
--- a/.github/workflows/build-and-push-fleetctl-docker.yml
+++ b/.github/workflows/build-and-check-fleetctl-docker-and-deps.yml
@@ -1,13 +1,14 @@
-name: Build and push fleetdm/fleetctl Docker image
+name: Build fleetctl docker dependencies and check vulnerabilities
-# Manually trigger this workflow for now
on:
workflow_dispatch:
inputs:
image_tag:
- description: 'Docker image tag'
+ description: "Docker image tag"
required: true
type: string
+ schedule:
+ - cron: "0 6 * * *"
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
@@ -23,7 +24,7 @@ permissions:
contents: read
jobs:
- docker-push:
+ build-and-check:
runs-on: ubuntu-latest
environment: Docker Hub
permissions:
@@ -46,25 +47,46 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Install Go Dependencies
run: make deps-go
+ - name: Build fleetdm/wix
+ run: make wix-docker
+
+ - name: Build fleetdm/bomutils
+ run: make bomutils-docker
+
- name: Build fleetdm/fleetctl
run: make fleetctl-docker
- - name: Push to Docker
- run: |
- docker tag fleetdm/fleetctl fleetdm/fleetctl:${{ inputs.image_tag }}
- docker push fleetdm/fleetctl:${{ inputs.image_tag }}
-
- - name: Push To quay.io
- id: push-to-quay
- uses: redhat-actions/push-to-registry@9986a6552bc4571882a4a67e016b17361412b4df # v2.7.1
+ - name: Run Trivy vulnerability scanner on fleetdm/wix
+ uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
with:
- image: fleetdm/fleetctl
- tags: ${{ inputs.image_tag }}
- registry: quay.io/
- username: fleetdm+fleetreleaser
- password: ${{ secrets.QUAY_REGISTRY_PASSWORD }}
+ image-ref: "fleetdm/wix"
+ format: "table"
+ exit-code: "1"
+ ignore-unfixed: true
+ vuln-type: "os,library"
+ severity: "CRITICAL"
+
+ - name: Run Trivy vulnerability scanner on fleetdm/bomutils
+ uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
+ with:
+ image-ref: "fleetdm/bomutils"
+ format: "table"
+ exit-code: "1"
+ ignore-unfixed: true
+ vuln-type: "os,library"
+ severity: "CRITICAL"
+
+ - name: Run Trivy vulnerability scanner on fleetdm/fleetctl
+ uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
+ with:
+ image-ref: "fleetdm/fleetctl"
+ format: "table"
+ exit-code: "1"
+ ignore-unfixed: true
+ vuln-type: "os,library"
+ severity: "CRITICAL"
diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml
index ed18437c74..278f958b28 100644
--- a/.github/workflows/build-binaries.yaml
+++ b/.github/workflows/build-binaries.yaml
@@ -29,10 +29,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
# Set the Node.js version
- name: Set up Node.js ${{ vars.NODE_VERSION }}
@@ -40,9 +43,6 @@ jobs:
with:
node-version: ${{ vars.NODE_VERSION }}
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
-
- name: JS Dependency Cache
id: js-cache
uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2
diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml
index 09f296aece..002d2657f6 100644
--- a/.github/workflows/build-orbit.yaml
+++ b/.github/workflows/build-orbit.yaml
@@ -59,7 +59,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Build, codesign and notarize orbit
run: go run ./orbit/tools/build/build.go
diff --git a/.github/workflows/check-automated-doc.yml b/.github/workflows/check-automated-doc.yml
index c654e7ae4f..d289c55318 100644
--- a/.github/workflows/check-automated-doc.yml
+++ b/.github/workflows/check-automated-doc.yml
@@ -36,15 +36,16 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ vars.GO_VERSION }}
- name: Checkout Code
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with:
fetch-depth: 0
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
- name: Verify golang generated documentation is up-to-date
run: |
make generate-doc
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 246c6418a1..c69888f874 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -56,7 +56,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/deploy-bulk-operations-dashboard.yml b/.github/workflows/deploy-bulk-operations-dashboard.yml
new file mode 100644
index 0000000000..090e409fac
--- /dev/null
+++ b/.github/workflows/deploy-bulk-operations-dashboard.yml
@@ -0,0 +1,89 @@
+name: Deploy app to bulk operations dashboard pipeline on Heroku.
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - 'ee/bulk-operations-dashboard/**'
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ permissions:
+ contents: write # for Git to git push
+ if: ${{ github.repository == 'fleetdm/fleet' }}
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [14.x]
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
+ # Configure our access credentials for the Heroku CLI
+ - uses: akhileshns/heroku-deploy@79ef2ae4ff9b897010907016b268fd0f88561820 # v3.6.8
+ with:
+ heroku_api_key: ${{secrets.HEROKU_API_TOKEN_FOR_BOT_USER}}
+ heroku_app_name: "" # this has to be blank or it doesn't work
+ heroku_email: ${{secrets.HEROKU_EMAIL_FOR_BOT_USER}}
+ justlogin: true
+ - run: heroku auth:whoami
+
+ # Set the Node.js version
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ # Now start building!
+ # > …but first, get a little crazy for a sec and delete the top-level package.json file
+ # > i.e. the one used by the Fleet server. This is because require() in node will go
+ # > hunting in ancestral directories for missing dependencies, and since some of the
+ # > bundled transpiler tasks sniff for package availability using require(), this trips
+ # > up when it encounters another Node universe in the parent directory.
+ - run: rm -rf package.json package-lock.json node_modules/
+ # > Turns out there's a similar issue with how eslint plugins are looked up, so we
+ # > delete the top level .eslintrc file too.
+ - run: rm -f .eslintrc.js
+ # > And, as a change to the top-level fleetdm/fleet .gitignore on May 2, 2022 revealed,
+ # > we also need to delete the top level .gitignore file too, so that its rules don't
+ # > interfere with the committing and force-pushing we're doing as part of our deploy
+ # > script here. For more info, see: https://github.com/fleetdm/fleet/pull/5549
+ - run: rm -f .gitignore
+
+ # Get dependencies (including dev deps)
+ - run: cd ee/bulk-operations-dashboard/ && npm install
+
+ # Run sanity checks
+ - run: cd ee/bulk-operations-dashboard/ && npm test
+
+ # Compile assets
+ - run: cd ee/bulk-operations-dashboard/ && npm run build-for-prod
+
+ # Commit newly-built assets locally so we can push them to Heroku below.
+ # (This commit will never be pushed to GitHub- only to Heroku.)
+ # > The local config flags make this work in GitHub's environment.
+ - run: git add ee/bulk-operations-dashboard/.www
+ - run: git -c "user.name=GitHub" -c "user.email=github@example.com" commit -am 'AUTOMATED COMMIT - Deployed the latest, including modified HTML layouts and .sailsrc file that reference minified assets.'
+
+ # Configure the Heroku app we'll be deploying to
+ - run: heroku git:remote -a bulk-operations-dashboard
+ - run: git remote -v
+
+ # Deploy to Heroku (by pushing)
+ # > Since a shallow clone was grabbed, we have to "unshallow" it before forcepushing.
+ - run: echo "Unshallowing local repository…"
+ - run: git fetch --prune --unshallow
+ - run: echo "Deploying branch '${GITHUB_REF##*/}' to Heroku…"
+ - run: git push heroku +${GITHUB_REF##*/}:master
+ - name: 🌐 The dashboard has been deployed
+ run: echo '' && echo '--' && echo 'OK, done. It should be live momentarily.' && echo '(if you get impatient, check the Heroku dashboard for status)'
diff --git a/.github/workflows/deploy-fleet-website.yml b/.github/workflows/deploy-fleet-website.yml
index 9fc044e13b..371a0014f0 100644
--- a/.github/workflows/deploy-fleet-website.yml
+++ b/.github/workflows/deploy-fleet-website.yml
@@ -64,7 +64,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
# Download top-level dependencies and build Storybook in the website's assets/ folder
- run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose
diff --git a/.github/workflows/dogfood-deploy.yml b/.github/workflows/dogfood-deploy.yml
index 39f6983824..f17768eec7 100644
--- a/.github/workflows/dogfood-deploy.yml
+++ b/.github/workflows/dogfood-deploy.yml
@@ -51,14 +51,17 @@ jobs:
- id: fail-on-main
run: "false"
if: ${{ github.ref == 'main' }}
+
- uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
with:
role-to-assume: ${{env.AWS_IAM_ROLE}}
aws-region: ${{ env.AWS_REGION }}
+
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
+
- uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
with:
terraform_version: 1.6.3
@@ -77,6 +80,26 @@ jobs:
id: plan
run: terraform plan -no-color
continue-on-error: true
+ - name: Slack Notification
+ if: success()
+ uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
+ with:
+ payload: |
+ {
+ "text": "${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}",
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "🚀 🛠️ Dogfood deploy in progress\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
+ }
+ }
+ ]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
+ SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# first we'll scale everything down and create the new task definitions
- name: Terraform Apply
id: apply
diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml
index 8b2e3217c0..487cdac1ad 100644
--- a/.github/workflows/dogfood-gitops.yml
+++ b/.github/workflows/dogfood-gitops.yml
@@ -69,6 +69,7 @@ jobs:
DOGFOOD_GLOBAL_ENROLL_SECRET: ${{ secrets.DOGFOOD_GLOBAL_ENROLL_SECRET }}
DOGFOOD_SSO_ISSUER_URI: ${{ secrets.DOGFOOD_SSO_ISSUER_URI }}
DOGFOOD_SSO_METADATA: ${{ secrets.DOGFOOD_SSO_METADATA }}
+ DOGFOOD_MDM_SSO_METADATA_URL: ${{ secrets.DOGFOOD_MDM_SSO_METADATA_URL }}
DOGFOOD_FAILING_POLICIES_WEBHOOK_URL: ${{ secrets.DOGFOOD_FAILING_POLICIES_WEBHOOK_URL }}
DOGFOOD_VULNERABILITIES_WEBHOOK_URL: ${{ secrets.DOGFOOD_VULNERABILITIES_WEBHOOK_URL }}
DOGFOOD_WORKSTATIONS_ENROLL_SECRET: ${{ secrets.DOGFOOD_WORKSTATIONS_ENROLL_SECRET }}
diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml
index 4cab7da482..f4dfb2780e 100644
--- a/.github/workflows/fleet-and-orbit.yml
+++ b/.github/workflows/fleet-and-orbit.yml
@@ -62,7 +62,6 @@ jobs:
timeout-minutes: 60
strategy:
matrix:
- go-version: ["${{ vars.GO_VERSION }}"]
mysql: ["mysql:8.0.36"]
runs-on: ubuntu-latest
needs: gen
@@ -72,10 +71,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
+ go-version-file: 'go.mod'
# Set the Node.js version
- name: Set up Node.js ${{ vars.NODE_VERSION }}
@@ -83,9 +85,6 @@ jobs:
with:
node-version: ${{ vars.NODE_VERSION }}
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
-
- name: Start tunnel
env:
CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }}
@@ -111,7 +110,7 @@ jobs:
done
- name: Start Infra Dependencies
- run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose up -d mysql redis &
+ run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose up -d mysql redis &
- name: Install JS Dependencies
run: make deps-js
@@ -175,9 +174,6 @@ jobs:
# This job also makes sure the Fleet server is up and running.
set-enroll-secret:
timeout-minutes: 60
- strategy:
- matrix:
- go-version: ["${{ vars.GO_VERSION }}"]
runs-on: ubuntu-latest
needs: gen
steps:
@@ -186,13 +182,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Build Fleetctl
run: make fleetctl
@@ -218,9 +214,6 @@ jobs:
# Here we generate the Fleet Desktop and osqueryd targets for
# macOS which can only be generated from a macOS host.
build-macos-targets:
- strategy:
- matrix:
- go-version: ["${{ vars.GO_VERSION }}"]
# Set macOS version to '12' (current equivalent to macos-latest) for
# building the binary. This ensures compatibility with macOS version 13 and
# later, avoiding runtime errors on systems using macOS 13 or newer.
@@ -234,13 +227,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Build desktop.app.tar.gz and osqueryd.app.tar.gz
run: |
@@ -269,9 +262,6 @@ jobs:
# installed, and installing it is time consuming and unreliable.
run-tuf-and-gen-pkgs:
timeout-minutes: 60
- strategy:
- matrix:
- go-version: ["${{ vars.GO_VERSION }}"]
runs-on: ubuntu-latest
needs: [gen, build-macos-targets]
steps:
@@ -280,13 +270,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Download macos pre-built apps
id: download
diff --git a/.github/workflows/fleetctl-preview-latest.yml b/.github/workflows/fleetctl-preview-latest.yml
index dda4e0f73c..630cfd1dc3 100644
--- a/.github/workflows/fleetctl-preview-latest.yml
+++ b/.github/workflows/fleetctl-preview-latest.yml
@@ -53,7 +53,6 @@ jobs:
# - Unattended installation of Docker on macOS fails. (see
# https://github.com/docker/for-mac/issues/6450)
os: [ubuntu-latest]
- go-version: ['${{ vars.GO_VERSION }}']
runs-on: ${{ matrix.os }}
steps:
@@ -62,13 +61,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Build Fleetctl
run: make fleetctl
diff --git a/.github/workflows/fleetd-tuf.yml b/.github/workflows/fleetd-tuf.yml
index ebeca889da..7641589f10 100644
--- a/.github/workflows/fleetd-tuf.yml
+++ b/.github/workflows/fleetd-tuf.yml
@@ -30,16 +30,16 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ vars.GO_VERSION }}
-
- name: Checkout Code
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with:
fetch-depth: 0
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
- name: Update orbit/TUF.md
run: |
make fleetd-tuf
diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml
index d8269e9948..93e5a30fce 100644
--- a/.github/workflows/generate-desktop-targets.yml
+++ b/.github/workflows/generate-desktop-targets.yml
@@ -13,18 +13,13 @@ on:
- '.github/workflows/generate-desktop-targets.yml'
workflow_dispatch:
-# This allows a subsequently queued workflow run to interrupt previous runs
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
- cancel-in-progress: true
-
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
env:
- FLEET_DESKTOP_VERSION: 1.29.0
+ FLEET_DESKTOP_VERSION: 1.33.0
permissions:
contents: read
@@ -45,13 +40,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
-
- - name: Checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Import signing keys
env:
@@ -98,13 +93,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
-
- - name: Checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Generate fleet-desktop.exe
run: |
@@ -139,13 +134,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
-
- - name: Checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Generate desktop.tar.gz
run: |
@@ -167,13 +162,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
-
- - name: Checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Generate desktop.tar.gz
run: |
diff --git a/.github/workflows/generate-osqueryd-targets.yml b/.github/workflows/generate-osqueryd-targets.yml
index 12e08bddac..b27e074b6a 100644
--- a/.github/workflows/generate-osqueryd-targets.yml
+++ b/.github/workflows/generate-osqueryd-targets.yml
@@ -24,7 +24,7 @@ defaults:
shell: bash
env:
- OSQUERY_VERSION: 5.12.2
+ OSQUERY_VERSION: 5.13.1
permissions:
contents: read
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index df6b9792b7..3d3e95ed2c 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -38,7 +38,6 @@ jobs:
matrix:
# See #9943, we just need to add windows-latest here once all issues are fixed.
os: [ubuntu-latest, macos-latest]
- go-version: ['${{ vars.GO_VERSION }}']
runs-on: ${{ matrix.os }}
steps:
- name: Harden Runner
@@ -52,7 +51,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
+ go-version-file: 'go.mod'
- name: Install dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml
index 8de0089b62..6ba9aff8f0 100644
--- a/.github/workflows/goreleaser-fleet.yaml
+++ b/.github/workflows/goreleaser-fleet.yaml
@@ -20,7 +20,7 @@ permissions:
jobs:
goreleaser:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-20.04-4-cores
environment: Docker Hub
permissions:
contents: write
@@ -44,7 +44,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
# Set the Node.js version
- name: Set up Node.js ${{ vars.NODE_VERSION }}
diff --git a/.github/workflows/goreleaser-orbit.yaml b/.github/workflows/goreleaser-orbit.yaml
index 666f281120..e196901ead 100644
--- a/.github/workflows/goreleaser-orbit.yaml
+++ b/.github/workflows/goreleaser-orbit.yaml
@@ -5,11 +5,6 @@ on:
tags:
- "orbit-*" # For testing, use a pre-release tag like 'orbit-1.24.0-1'
-# This allows a subsequently queued workflow run to interrupt previous runs
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
- cancel-in-progress: true
-
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
@@ -56,7 +51,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-macos.yml # v1.20.0
@@ -95,7 +90,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux.yml # v1.20.0
@@ -128,7 +123,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux-arm64.yml # v1.20.0
@@ -161,7 +156,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
- name: Run GoReleaser
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-windows.yml # v1.20.0
diff --git a/.github/workflows/goreleaser-snapshot-fleet.yaml b/.github/workflows/goreleaser-snapshot-fleet.yaml
index 46c1da4193..927cf31be1 100644
--- a/.github/workflows/goreleaser-snapshot-fleet.yaml
+++ b/.github/workflows/goreleaser-snapshot-fleet.yaml
@@ -57,7 +57,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
+ go-version-file: 'go.mod'
# Set the Node.js version
- name: Set up Node.js ${{ vars.NODE_VERSION }}
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 015a464b4b..98c9cd3a59 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -264,13 +264,13 @@ jobs:
npm install -g fleetctl
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ vars.GO_VERSION }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Build Fleetctl
run: make fleetctl
diff --git a/.github/workflows/push-osquery-perf-to-ecr.yml b/.github/workflows/push-osquery-perf-to-ecr.yml
deleted file mode 100644
index 0760d03900..0000000000
--- a/.github/workflows/push-osquery-perf-to-ecr.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: Build docker image and publish to ECR
-
-on:
- workflow_dispatch:
- inputs:
- enroll_secret:
- description: 'Enroll Secret'
- required: true
- url:
- description: 'Fleet server URL'
- required: true
- host_count:
- description: 'Amount of hosts to emulate'
- required: true
- default: 20
- tag:
- description: 'docker image tag'
- required: true
- default: latest
-
-# This allows a subsequently queued workflow run to interrupt previous runs
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
- cancel-in-progress: true
-
-defaults:
- run:
- # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
- shell: bash
-
-permissions:
- contents: read
-
-jobs:
- build-docker:
- runs-on: ubuntu-latest
- steps:
- - name: Harden Runner
- uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
- with:
- egress-policy: audit
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
-
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@05b148adc31e091bafbaf404f745055d4d3bc9d2 # v1
- with:
- aws-access-key-id: ${{ secrets.LOADTEST_AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.LOADTEST_AWS_SECRET_ACCESS_KEY }}
- aws-region: us-east-2
-
- - name: Login to Amazon ECR
- id: login-ecr
- uses: aws-actions/amazon-ecr-login@2f9f10ea3fa2eed41ac443fee8bfbd059af2d0a4 # v1
-
- - name: Build, tag, and push image to Amazon ECR
- env:
- ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
- ECR_REPOSITORY: osquery-perf
- IMAGE_TAG: ${{ github.event.inputs.tag }}
- run: |
- docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg ENROLL_SECRET=${{ github.event.inputs.enroll_secret }} --build-arg HOST_COUNT=${{ github.event.inputs.host_count }} --build-arg SERVER_URL=${{ github.event.inputs.url }} -f Dockerfile.osquery-perf .
- docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
diff --git a/.github/workflows/release-fleetctl-docker-deps.yaml b/.github/workflows/release-fleetctl-docker-deps.yaml
new file mode 100644
index 0000000000..c751655d93
--- /dev/null
+++ b/.github/workflows/release-fleetctl-docker-deps.yaml
@@ -0,0 +1,84 @@
+# Builds and releases to production the fleetdm/bomutils:latest and fleetdm/wix:latest
+# docker images, which are the docker image dependencies of the fleetctl command.
+#
+# This is separate from Fleet releases because we only release
+# fleetdm/bomutils and fleetdm/wix only if we add new dependencies
+# or for security updates.
+name: Release fleetctl docker dependencies
+
+on:
+ push:
+ tags:
+ - "fleetctl-docker-deps-*"
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
+ shell: bash
+
+permissions:
+ contents: read
+
+jobs:
+ push_latest:
+ runs-on: ubuntu-latest
+ environment: Docker Hub
+ permissions:
+ contents: write
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
+
+ - name: Build fleetdm/wix
+ run: make wix-docker
+
+ - name: Build fleetdm/bomutils
+ run: make bomutils-docker
+
+ #
+ # After fleetdm/wix and fleetdm/bomutils are built,
+ # let's smoke test pkg/msi generation before pushing.
+ #
+
+ - name: Install Go Dependencies
+ run: make deps-go
+
+ - name: Build fleetctl
+ run: make fleetctl
+
+ - name: Build MSI
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ - name: Build PKG
+ run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ #
+ # Now push to production
+ #
+
+ - name: Push fleetdm/bomutils to docker hub
+ run: docker push fleetdm/bomutils:latest
+
+ - name: Push fleetdm/wix to docker hub
+ run: docker push fleetdm/wix:latest
diff --git a/.github/workflows/release-fleetd-base.yml b/.github/workflows/release-fleetd-base.yml
index d7b02cfcf7..9909901964 100644
--- a/.github/workflows/release-fleetd-base.yml
+++ b/.github/workflows/release-fleetd-base.yml
@@ -51,16 +51,16 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
- with:
- go-version: ${{ vars.GO_VERSION }}
-
- name: Checkout Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
+ - name: Install Go
+ uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
+ with:
+ go-version-file: 'go.mod'
+
- name: Check for fleetd component updates
id: check-for-fleetd-component-updates
run: |
diff --git a/.github/workflows/test-bulk-operations-dashboard-changes.yml b/.github/workflows/test-bulk-operations-dashboard-changes.yml
new file mode 100644
index 0000000000..cfbb26205d
--- /dev/null
+++ b/.github/workflows/test-bulk-operations-dashboard-changes.yml
@@ -0,0 +1,60 @@
+name: Test bulk operations dashboard changes
+
+on:
+ pull_request:
+ paths:
+ - 'ee/bulk-operations-dashboard/**'
+ - '.github/workflows/test-bulk-operations-dashboard-changes.yml'
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ permissions:
+ contents: read
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
+ # Set the Node.js version
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ with:
+ node-version: ${{ matrix.node-version }}
+
+
+ # Now start building!
+ # > …but first, get a little crazy for a sec and delete the top-level package.json file
+ # > i.e. the one used by the Fleet server. This is because require() in node will go
+ # > hunting in ancestral directories for missing dependencies, and since some of the
+ # > bundled transpiler tasks sniff for package availability using require(), this trips
+ # > up when it encounters another Node universe in the parent directory.
+ - run: rm -rf package.json package-lock.json node_modules/
+ # > Turns out there's a similar issue with how eslint plugins are looked up, so we
+ # > delete the top level .eslintrc file too.
+ - run: rm -f .eslintrc.js
+
+ # Get dependencies (including dev deps)
+ - run: cd ee/bulk-operations-dashboard/ && npm install
+
+ # Run sanity checks
+ - run: cd ee/bulk-operations-dashboard/ && npm test
+
+ # Compile assets
+ - run: cd ee/bulk-operations-dashboard/ && npm run build-for-prod
diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml
index 301645008e..2bd89ab82b 100644
--- a/.github/workflows/test-db-changes.yml
+++ b/.github/workflows/test-db-changes.yml
@@ -10,7 +10,7 @@ on:
paths:
- '**.go'
- 'server/datastore/mysql/schema.sql'
- - '.github/workflows/test-schema-changes.yml'
+ - '.github/workflows/test-db-changes.yml'
workflow_dispatch: # Manual
# This allows a subsequently queued workflow run to interrupt previous runs
@@ -35,18 +35,28 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ vars.GO_VERSION }}
- name: Checkout Code
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with:
fetch-depth: 0
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
- name: Start Infra Dependencies
# Use & to background this
- run: docker-compose up -d mysql_test &
+ run: docker compose up -d mysql_test &
+
+ - name: Wait for mysql
+ run: |
+ echo "waiting for mysql..."
+ until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
+ echo "."
+ sleep 1
+ done
+ echo "mysql is ready"
- name: Verify test schema changes
run: |
diff --git a/.github/workflows/test-fleetd-chrome.yml b/.github/workflows/test-fleetd-chrome.yml
index 47ba496ebb..8cbb0125f9 100644
--- a/.github/workflows/test-fleetd-chrome.yml
+++ b/.github/workflows/test-fleetd-chrome.yml
@@ -66,7 +66,8 @@ jobs:
npm test
- name: Upload to Codecov
- uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+ uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
directory: ./ee/fleetd-chrome/coverage
flags: fleetd-chrome
diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml
index 0f128b8025..1066843684 100644
--- a/.github/workflows/test-go.yaml
+++ b/.github/workflows/test-go.yaml
@@ -44,8 +44,7 @@ jobs:
matrix:
suite: ["integration", "core"]
os: [ubuntu-latest]
- go-version: ['${{ vars.GO_VERSION }}']
- mysql: ["mysql:8.0.36"]
+ mysql: ["mysql:8.0.36", "mysql:8.4.2"] # make sure to update supported versions docs when this changes
continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail
runs-on: ${{ matrix.os }}
@@ -59,18 +58,18 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ matrix.go-version }}
-
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Install Go
+ uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
+ with:
+ go-version-file: 'go.mod'
+
# Pre-starting dependencies here means they are ready to go when we need them.
- name: Start Infra Dependencies
# Use & to background this
- run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose -f docker-compose.yml -f docker-compose-redis-cluster.yml up -d mysql_test mysql_replica_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit smtp4dev_test &
+ run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose -f docker-compose.yml -f docker-compose-redis-cluster.yml up -d mysql_test mysql_replica_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit smtp4dev_test &
- name: Add TLS certificate for SMTP Tests
run: |
@@ -98,13 +97,13 @@ jobs:
- name: Wait for mysql
run: |
echo "waiting for mysql..."
- until docker-compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
+ until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
echo "."
sleep 1
done
echo "mysql is ready"
echo "waiting for mysql replica..."
- until docker-compose exec -T mysql_replica_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
+ until docker compose exec -T mysql_replica_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
echo "."
sleep 1
done
@@ -119,7 +118,6 @@ jobs:
else
RUN_TESTS_ARG=''
fi
-
GO_TEST_EXTRA_FLAGS="-v -race=$RACE_ENABLED -timeout=$GO_TEST_TIMEOUT $RUN_TESTS_ARG" \
TEST_LOCK_FILE_PATH=$(pwd)/lock \
NETWORK_TEST=1 \
@@ -132,13 +130,17 @@ jobs:
NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \
make test-go 2>&1 | tee /tmp/gotest.log
- # note: it's fine to upload multiple reports (one per matrix combination)
- # for the same run, see https://docs.codecov.com/docs/merging-reports
- - name: Upload to Codecov
- uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
+ - name: Create mysql identifier without colon
+ if: always()
+ run: |
+ echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV
+
+ - name: Save coverage
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
- files: coverage.txt
- flags: backend
+ name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-coverage
+ path: ./coverage.txt
+ if-no-files-found: error
- name: Generate summary of errors
if: failure()
@@ -156,10 +158,6 @@ jobs:
fi
GO_FAIL_SUMMARY=$GO_FAIL_SUMMARY envsubst < .github/workflows/config/slack_payload_template.json > ./payload.json
- # TODO: figure out a sane way to combine outputs from different matrix jobs
- # into a single slack notification, instead of sending one per job. This
- # problem already existed but now it's accentuated because we're running 4
- # jobs.
- name: Slack Notification
if: github.event.schedule == '0 4 * * *' && failure()
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
@@ -174,15 +172,32 @@ jobs:
- name: Upload test log
if: always()
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
- name: test-log
+ name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-test-log
path: /tmp/gotest.log
if-no-files-found: error
- name: Upload summary test log
if: always()
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
- name: summary-test-log
+ name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-summary-test-log
path: /tmp/summary.txt
+
+ # We upload all backend coverage in one step so that we're less like to end up in a situation with a partial coverage report.
+ upload-coverage:
+ needs: [test-go]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Download artifacts
+ uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
+ with:
+ pattern: '*-coverage'
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ flags: backend
diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml
index 9d63523737..15b4fd05ce 100644
--- a/.github/workflows/test-js.yml
+++ b/.github/workflows/test-js.yml
@@ -69,8 +69,9 @@ jobs:
yarn test:ci
- name: Upload to Codecov
- uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
+ uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend
lint-js:
diff --git a/.github/workflows/test-native-tooling-packaging.yml b/.github/workflows/test-native-tooling-packaging.yml
index db242cee0c..45a6e9abff 100644
--- a/.github/workflows/test-native-tooling-packaging.yml
+++ b/.github/workflows/test-native-tooling-packaging.yml
@@ -1,4 +1,4 @@
-# This workflow tests packaging of Fleet-osquery with the
+# This workflow tests generation of fleetd packages with the
# `fleetdm/fleetctl` Docker image.
name: Test native tooling packaging
@@ -21,6 +21,8 @@ on:
- 'tools/bomutils-docker/**'
- '.github/workflows/test-native-tooling-packaging.yml'
workflow_dispatch: # Manual
+ schedule:
+ - cron: "0 5 * * *"
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
@@ -41,7 +43,12 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
- go-version: ['${{ vars.GO_VERSION }}']
+ # build_type == 'remote' means this job will test the fleetdm/fleetctl:latest from Docker Hub.
+ # build_type == 'local' means this job will build the the image locally.
+ #
+ # TODO(lucas): We should only run 'remote' on schedule
+ # (adding conditionals to 'matrix' requires many tricks).
+ build_type: ["remote", "local"]
runs-on: ${{ matrix.os }}
steps:
@@ -50,18 +57,30 @@ jobs:
with:
egress-policy: audit
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ matrix.go-version }}
-
- name: Checkout Code
+ if: ${{ matrix.build_type == 'local' }}
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Install Go
+ if: ${{ matrix.build_type == 'local' }}
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
- name: Install Go Dependencies
+ if: ${{ matrix.build_type == 'local' }}
run: make deps-go
+ - name: Build fleetdm/wix
+ if: ${{ matrix.build_type == 'local' }}
+ run: make wix-docker
+
+ - name: Build fleetdm/bomutils
+ if: ${{ matrix.build_type == 'local' }}
+ run: make bomutils-docker
+
- name: Build fleetdm/fleetctl
+ if: ${{ matrix.build_type == 'local' }}
run: make fleetctl-docker
- name: Build DEB
@@ -87,3 +106,24 @@ jobs:
- name: Build PKG with Fleet Desktop
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+
+ - name: Slack Notification
+ if: github.event.schedule == '0 5 * * *' && failure()
+ uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
+ with:
+ payload: |
+ {
+ "text": "${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head.html_url }}",
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "⚠️ Tests on fleetdm/fleetctl docker image failed.\nhttps://github.com/fleetdm/fleet/actions/runs/${{ github.run_id }}"
+ }
+ }
+ ]
+ }
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
+ SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
diff --git a/.github/workflows/test-packaging-build-docker-deps.yml b/.github/workflows/test-packaging-build-docker-deps.yml
new file mode 100644
index 0000000000..d2f0cf7f95
--- /dev/null
+++ b/.github/workflows/test-packaging-build-docker-deps.yml
@@ -0,0 +1,94 @@
+# This workflow tests packaging of fleetd with the
+# `fleetctl package` command using locally built fleetdm/wix and fleetdm/bomutils images.
+#
+# It fetches the targets: orbit, osquery and fleet-desktop from the default
+# (Fleet's) TUF server, https://tuf.fleetctl.com.
+name: Test packaging with local fleetdm/wix and fleetdm/bomutils
+
+on:
+ push:
+ branches:
+ - main
+ - patch-*
+ - prepare-*
+ paths:
+ - "tools/bomutils-docker/**"
+ - "tools/wix-docker/**"
+ - ".github/workflows/test-packaging-build-docker-deps.yml"
+ pull_request:
+ paths:
+ - "tools/bomutils-docker/**"
+ - "tools/wix-docker/**"
+ - ".github/workflows/test-packaging-build-docker-deps.yml"
+ workflow_dispatch: # Manual
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
+ shell: bash
+
+permissions:
+ contents: read
+
+jobs:
+ test-packaging:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: "go.mod"
+
+ - name: Install Go Dependencies
+ run: make deps-go
+
+ - name: Build fleetctl
+ run: make fleetctl
+
+ - name: Build fleetdm/wix
+ run: make wix-docker
+
+ - name: Build fleetdm/bomutils
+ run: make bomutils-docker
+
+ - name: Build DEB
+ run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ - name: Build DEB with Fleet Desktop
+ run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+
+ - name: Build RPM
+ run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ - name: Build RPM with Fleet Desktop
+ run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+
+ - name: Build MSI
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ - name: Build MSI with Fleet Desktop
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+
+ - name: Build PKG
+ run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080
+
+ - name: Build PKG with Fleet Desktop
+ run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 7190314cb9..0d8ff6d9b0 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -1,7 +1,8 @@
-# This workflow tests packaging of Fleet-osquery with the
-# `fleetctl package` command. It fetches the targets: orbit,
-# osquery and fleet-desktop from the default (Fleet's) TUF server,
-# https://tuf.fleetctl.com.
+# This workflow tests packaging of fleetd with the
+# `fleetctl package` command.
+#
+# It fetches the targets: orbit, osquery and fleet-desktop from the default
+# (Fleet's) TUF server, https://tuf.fleetctl.com.
name: Test packaging
on:
@@ -47,81 +48,89 @@ jobs:
# `macos-latest` uses arm64 by default now, so please be careful when
# updating this version.
os: [ubuntu-latest, macos-13]
- go-version: ['${{ vars.GO_VERSION }}']
runs-on: ${{ matrix.os }}
steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
- - name: Harden Runner
- uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
- with:
- egress-policy: audit
+ - name: Run Colima
+ if: startsWith(matrix.os, 'macos')
+ timeout-minutes: 15
+ # notes:
+ # - docker to install the docker CLI and interact with the Colima
+ # container runtime
+ # - colima is pre-installed in macos-12 runners, but not in macos-13 or
+ # macos-14 runners
+ run: |
+ brew install docker
+ # The runners come with an old version of python@3.12 that fails to upgrade
+ # when python gets pulled in as a dep through the chain
+ # colima -> lima -> qemu -> glibc -> python@3.12
+ # Force upgrade it for now, remove once the problem is fixed
+ brew install --overwrite python@3.12
+ brew install colima
+ colima start --mount $TMPDIR:w
- - name: Pull fleetdm/wix
- # Run in background while other steps complete to speed up the workflow
- run: docker pull fleetdm/wix:latest &
+ - name: Pull fleetdm/wix
+ # Run in background while other steps complete to speed up the workflow
+ run: docker pull fleetdm/wix:latest
- - name: Run Colima
- if: startsWith(matrix.os, 'macos')
- timeout-minutes: 10
- # notes:
- # - docker to install the docker CLI and interact with the Colima
- # container runtime
- # - colima is pre-installed in macos-12 runners, but not in macos-13 or
- # macos-14 runners
- run: |
- brew install docker colima
- colima start --mount $TMPDIR:w
+ - name: Pull fleetdm/bomutils
+ # Run in background while other steps complete to speed up the workflow
+ run: docker pull fleetdm/bomutils:latest
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version: ${{ matrix.go-version }}
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: "go.mod"
- - name: Install wine and wix
- if: startsWith(matrix.os, 'macos')
- run: |
- ./scripts/macos-install-wine.sh -n
- wget https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip -nv -O wix.zip
- mkdir wix
- unzip wix.zip -d wix
- rm -f wix.zip
- echo wix installed at $(pwd)/wix
+ - name: Install wine and wix
+ if: startsWith(matrix.os, 'macos')
+ run: |
+ ./scripts/macos-install-wine.sh -n
+ wget https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip -nv -O wix.zip
+ mkdir wix
+ unzip wix.zip -d wix
+ rm -f wix.zip
+ echo wix installed at $(pwd)/wix
- # It seems faster not to cache Go dependencies
- - name: Install Go Dependencies
- run: make deps-go
+ # It seems faster not to cache Go dependencies
+ - name: Install Go Dependencies
+ run: make deps-go
- - name: Build fleetctl
- run: make fleetctl
+ - name: Build fleetctl
+ run: make fleetctl
- - name: Build DEB
- run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080
+ - name: Build DEB
+ run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080
- - name: Build DEB with Fleet Desktop
- run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+ - name: Build DEB with Fleet Desktop
+ run: ./build/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- - name: Build RPM
- run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080
+ - name: Build RPM
+ run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080
- - name: Build RPM with Fleet Desktop
- run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+ - name: Build RPM with Fleet Desktop
+ run: ./build/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- - name: Build MSI
- run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080
+ - name: Build MSI
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080
- - name: Build MSI with Fleet Desktop
- run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+ - name: Build MSI with Fleet Desktop
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- - name: Build PKG
- run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080
+ - name: Build PKG
+ run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080
- - name: Build PKG with Fleet Desktop
- run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
+ - name: Build PKG with Fleet Desktop
+ run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- - name: Build MSI (using local Wix)
- if: startsWith(matrix.os, 'macos')
- run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop --local-wix-dir ./wix
+ - name: Build MSI (using local Wix)
+ if: startsWith(matrix.os, 'macos')
+ run: ./build/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop --local-wix-dir ./wix
diff --git a/.github/workflows/test-yml-specs.yml b/.github/workflows/test-yml-specs.yml
index 75e46d6af0..fe8f3ecace 100644
--- a/.github/workflows/test-yml-specs.yml
+++ b/.github/workflows/test-yml-specs.yml
@@ -33,7 +33,6 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
- go-version: ['${{ vars.GO_VERSION }}']
runs-on: ${{ matrix.os }}
steps:
@@ -42,13 +41,13 @@ jobs:
with:
egress-policy: audit
+ - name: Checkout Code
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout Code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ go-version-file: 'go.mod'
- name: Run apply spec tests
run: |
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3e3ac95d6a..885f407fb8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -61,6 +61,21 @@
"--dev_license"
]
},
+ {
+ "name": "Fleet vuln_processing (licensed)",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "buildFlags": "-tags='full,fts5'",
+ "cwd": "${workspaceFolder}",
+ "program": "${workspaceFolder}/cmd/fleet",
+ "args": [
+ "vuln_processing",
+ "--dev",
+ "--logging_debug",
+ "--dev_license",
+ ]
+ },
{
"name": "Attach to a running Fleet server",
"type": "go",
diff --git a/16538-preserve-manage-query-automations-modal-state b/16538-preserve-manage-query-automations-modal-state
deleted file mode 100644
index d8ea5ca981..0000000000
--- a/16538-preserve-manage-query-automations-modal-state
+++ /dev/null
@@ -1,2 +0,0 @@
-- Fix a bug where the manage query automations modal would lose its state when the user clicks
- "Preview data"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cd42253b8..2e87456657 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,174 @@
+## Fleet 4.56.0 (Sep 7, 2024)
+
+### Endpoint operations
+
+- Added index to `query_results` DB table to speed up finding last query timestamp for a given query and host.
+- Added a link in the UI to the error message when a CSR can't be downloaded due to missing private key.
+- Added a disabled overlay to the Other Workflows modal on the policy page.
+- Improved performance of live queries to accommodate for higher volumes when utilizing zero-trust workflows.
+- Improved `fleetctl` gitops error message when trying to change team name to a team that already exists.
+
+### Device management
+
+- Added server support for multiple VPP tokens.
+- Added new endpoints and updated existing endpoints for managing multiple Apple Business Manager tokens.
+- Added support for S3 to store MDM bootstrap packages (uses the same bucket configuration as for software installers).
+- Added support to UI for self service VPP software.
+- Added backend and gitops support for self service VPP.
+- Added ability for MDM migrations if the host is manually enrolled to a 3rd party MDM.
+- Added an offline screen to the macOS MDM migration flow.
+- Added new ABM page to Fleet UI.
+- Added new VPP page to the fleet UI
+- Added support to track the Apple Business Manager "terms expired" API error per token, as well as a global flag that gets set as soon as one token has its terms expired.
+- Updated the instructions on "My device" for MDM migrations on pre-Sonoma macOS hosts.
+- Updated to allow multiple teams to be assigned to the same VPP Token.
+- Updated process so that deleting installed software or VPP app now makes it available for re-installation.
+- Updated to enforce minimum OS version settings during Apple Automated Device Enrollment (ADE).
+- Updated ABM ingestion so that deleted iOS/iPadOS host will continue to report to Fleet as long as host is in Apple Business Manager (ABM).
+- Updated so that refetching an offline iOS/iPadOS host will not add new MDM commands to the queue if previous refetch has not completed yet.
+- Updated UI so that downloading a software installer package now shows the browser's built-in progress bar.
+- Updated relevant documentation to include references to multiple ABM and VPP tokens.
+- Consolidated Automatic Enrollment and VPP settings under the MDM settings integration page.
+- Cleared apps associated with a VPP token if it's moved off of a team.
+
+### Vulnerability management
+
+- Added ALAS bulletins as vulnerability source for Amazon Linux (instead of OVAL for Amazon Linux 2, and adds support for Amazon Linux 1, 2022, and 2023).
+- Added matching rules for July and August Microsoft 365 security updates (https://learn.microsoft.com/en-us/officeupdates/microsoft365-apps-security-updates).
+- Added the following filters to `/software/titles` and `/software/versions` API endpoints: `exploit: bool`, `min_cvss_score: float`, `max_cvss_score: float`.
+- Updated software titles/versions tables to allow for filtering by vulnerabilities including severity and known exploit.
+- Updated to use empty CVE description when the NVD CVE feed doesn't include description entries (instead of panicking).
+- Updated matching software that is not installed by Fleet so that it shows up as 'Available for install' on host details page.
+- Updated base images of `fleetdm/fleetctl`, `fleetdm/bomutils` and `fleetdm/wix` to fix critical vulnerabilities found by Trivy.
+- Updated vulnerability scanning to use `macos` SW target for CPEs of homebrew packages.
+- Updated vulnerability scanning to not ignore software with non-ASCII en dash and em dash characters.
+- Updated `GET /api/v1/fleet/vulnerabilities/{cve}` endpoint to add validation of CVE format, and a 204 response. The 204 response indicates that the vulnerability is known to Fleet but not present on any hosts.
+- Updated the UI to add new empty states for searching vulnerabilities: invalid CVE format searched, a known CVE serached but not present on hosts, not a known CVE searched, exploited vulnerability empty state, operating systems empty state, new icons.
+
+### Bug fixes and improvements
+
+- Added support for MySQL 8.4.2 LTS.
+- Updated Go to go1.22.6.
+- Updated Fleet server to now accept arguments via stdin. This is useful for passing secrets that you don't want to expose as env vars, in the command line, or in the config file.
+- Updated text for "Turn on MDM" banners in UI.
+- Updated ABM host tooltip copy on the manage host page to clarify when host vitals will be available to view.
+- Updated copy on auotmatic enrollment modal on my device page.
+- Updated host details activities tooltip and empty state copy to reflect recently added capabilities.
+- Updated Fleet Free so users see a Premium feature message when clicking to add software.
+- Updated usage reporting to report statistics on new AI features, maintenance window, and `fleetd`.
+- Fixed bug where configuration profile was still showing the old label name after the name was updated.
+- Fixed a bug when a cached prepared statement gets deleted in the MySQL server itself without Fleet knowing.
+- Fixed a bug where the wrong API path was used to download a software installer.
+- Fixed the failing_host_count so it is never 0. This count is normally updated once an hour during cleanups_then_aggregation cron job.
+- Fixed CVE-2024-4030 in Vulncheck feed incorrectly targeting non-Windows hosts.
+- Fixed a bug where the "Self-service" filter for the list of software and the list of host's software did not take App Store apps into account.
+- Fixed a bug where the "My device" page in Fleet Desktop did not show the self-service software tab when App Store apps were available as self-install.
+- Fixed a bug where a software installer (a package or a VPP app) that has been installed on a host still shows up as "Available for install" and can still be requested to be installed after the host is transferred to a different team without that installer (or after the installer is deleted).
+- Fixed the "Available for install" filter in the host's software page so that installers that were requested to be installed on the host (regardless of installation status) also show up in the list.
+- Fixed UI popup messages bleeding off viewport in some cases.
+- Fixed an issue with the scheduling of cron jobs at startup if the job has never run, which caused it to be delayed.
+- Fixed UI to display the label names in case-insensitive alphabetical order.
+
+## Fleet 4.55.2 (Sep 05, 2024)
+
+### Bug fixes
+
+- Removed validation of APNS certificate from server startup. This was no longer necessary because we now allow for APNS certificates to be renewed in the UI.
+- Fixed logic to properly catch and log APNs errors.
+
+## Fleet 4.55.1 (Aug 15, 2024)
+
+### Bug fixes
+
+- Added a disabled overlay to the Other Workflows modal on the policy page.
+- Updated text for "Turn on MDM" banners in UI.
+- Fixed a bug when a cached prepared statement got deleted in the MySQL server itself without Fleet knowing.
+- Continued with an empty CVE description when the NVD CVE feed didn't include description entries (instead of panicking).
+- Scheduled maintenance events are now scheduled over calendar events marked "Free" (not busy) in Google Calendar.
+- Fixed a bug where the wrong API path was used to download a software installer.
+- Improved fleetctl gitops error message when trying to change team name to a team that already exists.
+- Updated ABM (Apple Business Manager) host tooltip copy on the manage host page to clarify when host vitals will be available to view.
+- Added index to query_results DB table to speed up finding the last query timestamp for a given query and host.
+- Displayed the label names in case-insensitive alphabetical order in the fleet UI.
+
+## Fleet 4.55.0 (Aug 8, 2024)
+
+**NOTE:** Beginning with v4.55.0, Fleet no longer supports MySQL 5.7 because it has reached [end of life](https://mattermost.com/blog/mysql-5-7-reached-eol-upgrade-to-mysql-8-x-today/#:~:text=In%20October%202023%2C%20MySQL%205.7,to%20upgrade%20to%20MySQL%208.). The minimum version supported is MySQL 8.0.36.
+
+### Endpoint Operations
+
+- Added support for generating `fleetd` packages for Linux ARM64.
+- Added new `fleetctl package` --arch flag.
+- Updated `fleetctl package` command to remove the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
+- Updated maintenance window descriptions to update regularly to match the failing policy description/resolution.
+- Updated maintenance windows using Google Calendar so that calendar events are now recreated within 30 seconds if deleted or moved to the past.
+ - Fleet server watches for potential changes for up to 1 week after original event time. If event is moved forward more than 1 week, then after 1 week Fleet server will check for event changes once every 30 minutes.
+ - **NOTE:** These near real-time updates may add additional load to the Google Calendar API, so it is recommended to use API usage alerts or other monitoring methods.
+
+### Device Management
+
+- Integrated [Escrow Buddy](https://github.com/macadmins/escrow-buddy) to add enforcement of FileVault during the MacOS Setup Assistant process for hosts that are
+enrolled into teams (or no team) with disk encryption turned on. Thank you [homebysix](https://github.com/homebysix) and team!
+- Updated `fleetd` to use [Escrow Buddy](https://github.com/macadmins/escrow-buddy) to rotate FileVault keys. Removed or modified internal API endpoints documented in the API for contributors.
+- Added OS updates support to iOS/iPadOS devices.
+- Added iOS and iPadOS device details refetch triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint.
+- Added iOS and iPadOS user-installed apps to Fleet.
+- Added iOS and iPadOS apps to be installed using Apple's VPP (Volume Purchase Program) to Fleet.
+- Added support for VPP to GitOps.
+- Added the `POST /mdm/apple/vpp_token`, `DELETE /mdm/apple/vpp_token` and `GET /vpp` endpoints and related functionality.
+- Added new `GET /software/app_store_apps` and `POST /software/app_store_apps` endpoints and associated functionality.
+- Added the associated VPP apps to the `GET /software/titles` and `GET /software/titles/:id` endpoints.
+- Added the associated VPP apps to the `GET /hosts/:id/software` and `GET /device/:token/software` endpoints.
+- Added support to delete a VPP app from a team in `DELETE /software/titles/:software_title_id/available_for_install`.
+- Added `exclude_software` query parameter to "Get host by identifier" API.
+- Added ability to add/remove/disable apps with VPP in the Fleet UI.
+- Added a warning banner to the UI if the uploaded VPP token is about to expire/has expired.
+- Added UI updates for VPP feature on host software and my device pages.
+- Added global activity support for VPP-related activities.
+- Added UI features for managing VPP apps for iPadOS and iOS hosts.
+- Updated profile activities to include iOS and iPadOS.
+- Updated Fleet UI to show OS version compliance on host details page.
+- Added support for "No teams" on all software pages including adding software installers.
+- Added DB migration to support VPP software features.
+- Added DB migration to migrate older team configurations to the new version that includes both installers and App Store apps.
+- Linux lock/unlock scripts now make use of pam_nologin to keep AD users locked out.
+- Installed software list now includes Linux .deb packages that are 'on hold'.
+- Added a special-case to properly name the Notion .exe Windows installer the same as how it will be reported by osquery post-install.
+- Increased threshold to renew Apple SCEP certificates for MDM enrollments to 180 days.
+
+### Vulnerability Management
+
+- Fixed CVEs identified as 'Rejected' in NVD not matching against software.
+- Fixed false negative vulnerabilities with IntelliJ IDEA CE and PyCharm CE installed via Homebrew.
+
+### Bug fixes and improvements
+
+- Dropped support for MySQL 5.7 and raised minimum required to MySQL 8.0.36.
+- Updated software pre-install to use new GitOps format for query.
+- Updated UI tooltips for pending OS settings.
+- Fixed a styling issue in the controls > OS settings > disk encryption table.
+- Fixed a bug in `fleetctl preview` that was causing it to fail if Docker was installed without support for the deprecated `docker-compose` CLI.
+- Fixed an issue where the app-wide warning banners were not showing on the initial page load.
+- Fixed a bug where the hosts page would sometimes allow excess pagination.
+- Fixed a bug where software install results could not be retrieved for deleted hosts in the activity feed.
+- Fixed path that was incorrect for the download software installer package endpoint `GET /software/titles/:software_title_id/package`.
+- Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set.
+- Fixed a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly processed by 4.54.0.
+- Fixed a bug where software install results could not be retrieved for deleted hosts in the activity feed.
+- Fixed a bug where a software installer (a package or a VPP app) that has been installed on a host still shows up as "Available for install" and can still be requested to be installed after the host is transferred to a different team without that installer (or after the installer is deleted).
+
## Fleet 4.54.1 (Jul 24, 2024)
### Bug fixes
-* Fixed a startup bug by performing an early restart of orbit if an agent options setting has changed.
-* Implemented a small refactor of orbit subsystems.
-* Removed the `--version` flag from the `fleetctl package` command. The version of the package can now be controlled by the `--orbit-channel` flag.
-* Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set .
-* In `fleetctl package` command, removed the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
-* Fixed a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly processed by 4.54.0.
-* Re-enabled cached logins after windows Unlock.
+
+- Fixed a startup bug by performing an early restart of orbit if an agent options setting has changed.
+- Implemented a small refactor of orbit subsystems.
+- Removed the `--version` flag from the `fleetctl package` command. The version of the package can now be controlled by the `--orbit-channel` flag.
+- Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set .
+- In `fleetctl package` command, removed the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
+- Fixed a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly processed by 4.54.0.
+- Re-enabled cached logins after windows Unlock.
+
## Fleet 4.54.0 (Jul 17, 2024)
### Endpoint Operations
@@ -93,19 +254,19 @@
### Bug fixes
-* Updated fleetctl get queries/labels/hosts descriptions.
-* Fixed exporting CSVs with fields that contain commas to render properly.
-* Fixed link to fleetd uninstall instructions in "Delete device" modal.
-* Rendered only one banner on the my device page based on priority order.
-* Hidden query delete checkboxes from team observers.
-* Fixed issue where the Fleet UI could not be used to renew the ABM token after the ABM user who created the token was deleted.
-* Fixed an issue where special characters in HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall broke the "installer_utils.ps1 -uninstallOrbit" step in the Windows MSI installer.
-* Fixed counts for hosts with low disk space in summary page.
-* Fleet UI fixes: Hide CTA on inherited queries/policies from team level users.
-* Updated software updated timestamp tooltip.
-* Fixed issue where some Windows applications were getting matched against Windows OS vulnerabilities.
-* Fixed crash in `fleetd` installer on Windows if there are registry keys with special characters on the system.
-* Fixed UI capitalizations.
+- Updated fleetctl get queries/labels/hosts descriptions.
+- Fixed exporting CSVs with fields that contain commas to render properly.
+- Fixed link to fleetd uninstall instructions in "Delete device" modal.
+- Rendered only one banner on the my device page based on priority order.
+- Hidden query delete checkboxes from team observers.
+- Fixed issue where the Fleet UI could not be used to renew the ABM token after the ABM user who created the token was deleted.
+- Fixed an issue where special characters in HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall broke the "installer_utils.ps1 -uninstallOrbit" step in the Windows MSI installer.
+- Fixed counts for hosts with low disk space in summary page.
+- Fleet UI fixes: Hide CTA on inherited queries/policies from team level users.
+- Updated software updated timestamp tooltip.
+- Fixed issue where some Windows applications were getting matched against Windows OS vulnerabilities.
+- Fixed crash in `fleetd` installer on Windows if there are registry keys with special characters on the system.
+- Fixed UI capitalizations.
## Fleet 4.53.0 (Jun 25, 2024)
diff --git a/CODEOWNERS b/CODEOWNERS
index a1102215a4..153161e7ee 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -39,19 +39,9 @@
go.sum @fleetdm/go
go.mod @fleetdm/go
/cmd/ @fleetdm/go
-/orbit/ @lucasmrod @getvictor @roperzh @gillespi314
/server/ @fleetdm/go
-/server/service/handler.go @lucasmrod @getvictor @roperzh @gillespi314
-/server/mdm/ @roperzh @gillespi314 @lucasmrod @georgekarrv
-/server/worker/ @lucasmrod @getvictor @roperzh @gillespi314
-/server/vulnerabilities/ @lucasmrod @mostlikelee @getvictor
-/server/cron/ @getvictor @lucasmrod @roperzh @mostlikelee
-/ee/fleetd-chrome @lucasmrod @getvictor @RachelElysia
-/ee/vulnerability-dashboard @eashaw
-/ee/cis @sharon-fdm @lucasmrod @RachelElysia @jacobshandling
-/ee/server/calendar @lucasmrod @getvictor @jacobshandling
-/ee/server/service @roperzh @gillespi314 @lucasmrod @getvictor
-/scripts/mdm @roperzh @gillespi314 @jahzielv @dantecatalfamo
+/ee/server/ @fleetdm/go
+/orbit/ @lucasmrod @roperzh @lukeheath @georgekarrv @sharon-fdm
##############################################################################################
# 🚀 React files and other files related to the core product frontend.
@@ -66,9 +56,9 @@ go.mod @fleetdm/go
# FUTURE: Look for a way to not have this notify every single person in this "github team".
##############################################################################################
-/infrastructure/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
-/charts/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
-/terraform/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
+/infrastructure/ @rfairburn @ksatter @lukeheath @edwardsb @georgekarrv
+/charts/ @rfairburn @ksatter @lukeheath @edwardsb @georgekarrv
+/terraform/ @rfairburn @ksatter @lukeheath @edwardsb @georgekarrv
/it-and-security/ @noahtalerman @lukeheath @spokanemac @getvictor
##############################################################################################
@@ -76,8 +66,8 @@ go.mod @fleetdm/go
#
# (see website/config/custom.js for DRIs of other paths not listed here)
##############################################################################################
-/docs @eashaw
-/docs/REST\ API/rest-api.md @lukeheath # « REST API reference documentation
+/docs @rachaelshaw @lukeheath
+/docs/REST\ API/rest-api.md @rachaelshaw @lukeheath # « REST API reference documentation
/docs/Contributing/API-for-contributors.md @lukeheath # « Advanced / contributors-only API reference documentation
/schema @eashaw # « Data tables (osquery/fleetd schema) documentation
/docs/Deploy/_kubernetes/ @dherder # « Kubernetes best practice
@@ -105,13 +95,13 @@ go.mod @fleetdm/go
/handbook/README.md @mikermcneil
/handbook/company/open-positions.yml @sampfluger88
/handbook/company/product-groups.md @lukeheath
-/handbook/business-operations/README.md @sampfluger88
-/handbook/business-operations/business-operations.rituals.yml @sampfluger88
-/handbook/business-operations/Application-security.md @lukeheath
-/handbook/business-operations/security-audits.md @lukeheath
-/handbook/business-operations/security-policies.md @lukeheath
-/handbook/business-operations/security.md @lukeheath
-/handbook/business-operations/vendor-questionnaires.md @lukeheath
+/handbook/finance/README.md @sampfluger88
+/handbook/finance/finance.rituals.yml @sampfluger88
+/handbook/digital-experience/application-security.md @lukeheath
+/handbook/digital-experience/security-audits.md @lukeheath
+/handbook/digital-experience/security-policies.md @lukeheath
+/handbook/digital-experience/security.md @lukeheath
+/handbook/digital-experience/vendor-questionnaires.md @lukeheath
/handbook/digital-experience @sampfluger88
/handbook/customer-success @sampfluger88
/handbook/demand @sampfluger88
@@ -137,43 +127,7 @@ go.mod @fleetdm/go
##############################################################################################
# 🚀 GitHub workflows
##############################################################################################
-/.github/workflows/README.md @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/goreleaser-fleet.yaml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/update-certs.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/codeql-analysis.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/codeql.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/scorecards-analysis.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/integration.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/fleetctl-preview.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/fleetctl-preview-latest.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/goreleaser-orbit.yaml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/trivy-scan.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/goreleaser-snapshot-fleet.yaml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/build-and-push-fleetctl-docker.yml @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/fleetd-tuf.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/generate-desktop-targets.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-yml-specs.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/build-binaries.yaml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/fleet-and-orbit.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/build-orbit.yaml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/generate-osqueryd-targets.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-packaging.yml @lucasmrod @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/release-helm.yaml @rfairburn @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/pr-helm.yaml @rfairburn @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/tfvalidate.yml @rfairburn @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/dogfood-deploy.yml @rfairburn @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-db-changes.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-go.yaml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/golangci-lint.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-native-tooling-packaging.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/check-tuf-timestamps.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-puppet.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/generate-nudge-targets.yml @roperzh @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-js.yml @ghernandez345 @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/dogfood-gitops.yml @getvictor @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/test-fleetd-chrome.yml @getvictor @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/release-fleetd-chrome.yml @getvictor @lukeheath @georgekarrv @sharon-fdm
-/.github/workflows/release-fleetd-chrome-beta.yml @getvictor @lukeheath @georgekarrv @sharon-fdm
+/.github/workflows/ @lukeheath @georgekarrv @sharon-fdm
# ℹ️ But wait, there's more!
# See the comments up top to learn where else DRIs and maintainers are configured.
diff --git a/Dockerfile-desktop-linux b/Dockerfile-desktop-linux
index b0ff52c050..c17d3894d0 100644
--- a/Dockerfile-desktop-linux
+++ b/Dockerfile-desktop-linux
@@ -1,4 +1,4 @@
-FROM --platform=linux/amd64 golang:1.22.4-bullseye@sha256:067c5c7fe6d79f900c5ebe8351166356d6e3bbfcc6f807030e89b9a929252273
+FROM --platform=linux/amd64 golang:1.23.1-bullseye@sha256:45b43371f21ec51276118e6806a22cbb0bca087ddd54c491fdc7149be01035d5
LABEL maintainer="Fleet Developers"
RUN mkdir -p /usr/src/fleet
diff --git a/Dockerfile.osquery-perf b/Dockerfile.osquery-perf
deleted file mode 100644
index 89331d9312..0000000000
--- a/Dockerfile.osquery-perf
+++ /dev/null
@@ -1,16 +0,0 @@
-FROM golang:1.22.4-alpine3.20@sha256:ace6cc3fe58d0c7b12303c57afe6d6724851152df55e08057b43990b927ad5e8
-
-ARG ENROLL_SECRET
-ARG HOST_COUNT
-ARG SERVER_URL
-
-ENV ENROLL_SECRET ${ENROLL_SECRET}
-ENV HOST_COUNT ${HOST_COUNT}
-ENV SERVER_URL ${SERVER_URL}
-
-COPY ./cmd/osquery-perf/agent.go ./go.mod ./go.sum ./cmd/osquery-perf/mac10.14.6.tmpl /osquery-perf/
-WORKDIR /osquery-perf/
-RUN go mod download
-RUN go build -o osquery-perf
-
-CMD ./osquery-perf -enroll_secret $ENROLL_SECRET -host_count $HOST_COUNT -server_url $SERVER_URL
diff --git a/Makefile b/Makefile
index 2e7c317baa..9e63214ebd 100644
--- a/Makefile
+++ b/Makefile
@@ -74,6 +74,7 @@ define HELP_TEXT
make generate-go - Generate and bundle required go code
make generate-js - Generate and bundle required js code
make generate-dev - Generate and bundle required code in a watch loop
+ make generate-doc - Generate updated API documentation for activities, osquery flags
make clean - Clean all build artifacts
make clean-assets - Clean assets only
@@ -221,6 +222,12 @@ docker-push-release: docker-build-release
fleetctl-docker: xp-fleetctl
docker build -t fleetdm/fleetctl --platform=linux/amd64 -f tools/fleetctl-docker/Dockerfile .
+bomutils-docker:
+ cd tools/bomutils-docker && docker build -t fleetdm/bomutils --platform=linux/amd64 -f Dockerfile .
+
+wix-docker:
+ cd tools/wix-docker && docker build -t fleetdm/wix --platform=linux/amd64 -f Dockerfile .
+
.pre-binary-bundle:
rm -rf build/binary-bundle
mkdir -p build/binary-bundle/linux
@@ -281,7 +288,7 @@ binary-arch: .pre-binary-arch .pre-binary-bundle .pre-fleet
# Drop, create, and migrate the e2e test database
e2e-reset-db:
- docker-compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
+ docker compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
./build/fleet prepare db --mysql_address=localhost:3307 --mysql_username=root --mysql_password=toor --mysql_database=e2e
e2e-setup:
@@ -312,7 +319,7 @@ e2e-serve-premium: e2e-reset-db
# Usage:
# make e2e-set-desktop-token host_id=1 token=foo
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'
+ 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'
changelog:
sh -c "find changes -type f | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md"
@@ -347,7 +354,7 @@ fleetd-tuf:
# Reset the development DB
db-reset:
- docker-compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
+ docker compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
./build/fleet prepare db --dev
# Back up the development DB to file
diff --git a/README.md b/README.md
index 7e8651a618..3c23f067c3 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,6 @@ Fleet has no ambition to replace all of your other tools. (Though it might repl
Fleet plays well with Munki, Chef, Puppet, and Ansible, as well as with security tools like Crowdstrike and SentinelOne. For example, you can use the free version of Fleet to quickly report on what hosts are _actually_ running your EDR agent.
-While most folks prefer to use one or the other, Fleet can also coexist peacefully with Rapid7 and other agent-based vulnerability scanners. This can be useful during migrations.
-
#### Free as in free
The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is [independently backed](https://linkedin.com/company/fleetdm) and actively maintained with the help of many amazing [contributors](https://github.com/fleetdm/fleet/graphs/contributors).
diff --git a/docs/Using Fleet/Automations.md b/articles/automations.md
similarity index 92%
rename from docs/Using Fleet/Automations.md
rename to articles/automations.md
index e124fc779e..478b870556 100644
--- a/docs/Using Fleet/Automations.md
+++ b/articles/automations.md
@@ -40,6 +40,9 @@ Host status automations send a webhook request if a configured percentage of hos
Fleet sends these webhook requests once per day by default.
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/enroll-chromebooks.md b/articles/chrome-os.md
similarity index 85%
rename from docs/Using Fleet/enroll-chromebooks.md
rename to articles/chrome-os.md
index 2c1684cc5f..d74dfb89c1 100644
--- a/docs/Using Fleet/enroll-chromebooks.md
+++ b/articles/chrome-os.md
@@ -1,8 +1,6 @@
# ChromeOS
For visibility on ChromeOS hosts, Fleet provides the fleetd Chrome extension which provides similar functionality as osquery on other operating systems.
-## Adding ChromeOS hosts to Fleet
-
To learn how to add ChromeOS hosts to Fleet, visit [here](https://fleetdm.com/docs/using-fleet/adding-hosts#enroll-chromebooks).
> The fleetd Chrome browser extension is supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). It is not intended for non-ChromeOS hosts with the Chrome browser installed.
@@ -23,6 +21,10 @@ By default, the hostname for a Chromebook host will be blank. The hostname can b
## Debugging ChromeOS
To learn how to debug the Fleetd Chrome extension, visit [here](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Testing-and-local-development.md#fleetd-chrome-extension).
-
-
-
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/CIS-Benchmarks.md b/articles/cis-benchmarks.md
similarity index 91%
rename from docs/Using Fleet/CIS-Benchmarks.md
rename to articles/cis-benchmarks.md
index 9942eb0e3c..905d62efba 100644
--- a/docs/Using Fleet/CIS-Benchmarks.md
+++ b/articles/cis-benchmarks.md
@@ -11,7 +11,7 @@ Fleet has implemented native support for CIS Benchmarks for the following platfo
- Windows 10 Enterprise
- Windows 11 Enterprise
-[Where possible](#limitations), each CIS Benchmark is implemented with a [policy query](./REST-API.md#policies) in Fleet.
+[Where possible](#limitations), each CIS Benchmark is implemented with a [policy query](https://fleetdm.com/docs/rest-api/rest-api#policies) in Fleet.
These benchmarks are intended to gauge your organization's security posture, rather than the current state of a given host. A host may fail a CIS Benchmark policy despite having the correct settings enabled if there is no configuration profile or Group Policy Object (GPO) in place to enforce the setting. For example, this is the query for **CIS - Ensure FileVault Is Enabled (MDM Required)**:
@@ -95,7 +95,7 @@ Following are the requirements to use the CIS Benchmarks in Fleet:
- Devices must be running [`fleetd`](https://fleetdm.com/docs/using-fleet/orbit), Fleet's lightweight agent.
- Some CIS Benchmarks explicitly involve verifying MDM-based controls, so devices must be enrolled to an MDM solution.
-- On macOS, the orbit component of fleetd must have "Full Disk Access", see [Grant Full Disk Access to Osquery on macOS](./Adding-hosts.md#grant-full-disk-access-to-osquery-on-macos).
+- On macOS, the orbit component of fleetd must have "Full Disk Access", see [Grant Full Disk Access to Osquery on macOS](https://fleetdm.com/guides/enroll-hosts#grant-full-disk-access-to-osquery-on-macos).
## Limitations
@@ -111,7 +111,9 @@ In August 2023, we completed scale testing on 10k Windows hosts and 70k macOS ho
Detailed results are [here](https://docs.google.com/document/d/1OSpyzMkHjVhG_-EIBkLu7X3hj_XfVASGl3IXIYChpck/edit?usp=sharing).
-
-
-
-
+
+
+
+
+
+
diff --git a/articles/configuring-default-teams-for-devices-in-fleet.md b/articles/configuring-default-teams-for-devices-in-fleet.md
new file mode 100644
index 0000000000..1b22d16424
--- /dev/null
+++ b/articles/configuring-default-teams-for-devices-in-fleet.md
@@ -0,0 +1,46 @@
+# Configuring default teams for macOS, iOS, and iPadOS devices in Fleet
+
+Fleet allows you to configure default teams for macOS, iOS, and iPadOS devices as they automatically enroll in your instance. This ensures that devices are assigned to the correct teams and receive the appropriate apps and configuration profiles at enrollment.
+
+## Why configure default teams?
+
+The ability to assign default teams during device enrollment helps streamline the deployment process. Each device is automatically placed in its correct group, ensuring it receives the necessary configuration profiles and apps without requiring manual assignment.
+
+### Configuring default teams in Fleet
+
+Follow these steps to assign default teams to your devices:
+
+1. **Navigate to automatic enrollment settings**:
+
+ - Go to **Settings > Integrations > Mobile device management (MDM)**, and locate the **Automatic enrollment** section.
+
+2. **Edit the ABM token**:
+
+ - Click **Edit** next to the ABM token for which you want to configure default teams.
+
+3. **Assign default teams**:
+
+ - In the modal, use the dropdowns to select the appropriate default team for each platform (macOS, iOS, and iPadOS).
+
+4. **Save your changes**:
+
+ - After selecting the teams, click **Save** to apply the changes. New devices will be automatically assigned to the selected teams upon enrollment.
+
+## Benefits of configuring default teams
+
+1. **Streamlined deployment**: Devices are configured and ready for use immediately after enrollment, reducing manual setup time.
+
+2. **Reduced errors**: Automating team assignments helps avoid misconfigurations and ensures that the right profiles and apps are installed on the correct devices.
+
+## Conclusion
+
+Configuring default teams in Fleet simplifies the enrollment and management of Apple devices, ensuring that each device is assigned to the correct team immediately upon enrollment. This feature reduces manual setup tasks for IT teams by automating the assignment of configuration profiles and apps based on team specifications. By streamlining the deployment process and minimizing errors, configuring default teams ensures that devices are ready to use right out of the box, helping organizations save time and maintain consistency across their device fleet.
+
+For organizations managing a large number of macOS, iOS, or iPadOS devices, this feature plays a crucial role in automating routine tasks, increasing efficiency, and improving the overall deployment experience. It enables teams to focus on more critical tasks and be confident that newly enrolled devices are correctly configured. For more information on using Fleet, please refer to the [Fleet documentation](https://fleetdm.com/docs) and [guides](https://fleetdm.com/guides).
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/MDM-custom-OS-settings.md b/articles/custom-os-settings.md
similarity index 84%
rename from docs/Using Fleet/MDM-custom-OS-settings.md
rename to articles/custom-os-settings.md
index bcc30c022a..aadafc1f84 100644
--- a/docs/Using Fleet/MDM-custom-OS-settings.md
+++ b/articles/custom-os-settings.md
@@ -1,6 +1,6 @@
# Custom OS settings
-In Fleet you can enforce OS settings on your your macOS, iOS, iPadOS, and Windows hosts using configuration profiles.
+In Fleet you can enforce OS settings like security restrictions, screen lock, Wi-Fi etc., on your your macOS, iOS, iPadOS, and Windows hosts using configuration or device profiles.
## Enforce OS settings
@@ -36,7 +36,9 @@ In the top box, with "Verified," "Verifying," "Pending," and "Failed" statuses,
In the list of hosts, click on an individual host and click the **OS settings** item to see the status for a specific setting.
-
-
+
+
+
+
+
-
diff --git a/articles/debunk-the-cross-platform-myth.md b/articles/debunk-the-cross-platform-myth.md
new file mode 100644
index 0000000000..80d81c919c
--- /dev/null
+++ b/articles/debunk-the-cross-platform-myth.md
@@ -0,0 +1,59 @@
+# Debunk the cross-platform myth
+
+Conventional wisdom holds that cross-platform device management is a nightmare. It’s no surprise—most solutions out there are cobbled together with bolted-on features that never quite mesh. If you’ve tried managing a mixed fleet of macOS, Windows, and Linux devices, you might have some scars to show for it. But here’s the thing: it doesn’t have to be that way. Fleet is built differently, and it’s time to debunk the myth that cross-platform management has to suck.
+
+## Cross-platform pain points
+
+The skepticism around cross-platform device management is real, and for good reason. Many IT teams have been burned by solutions that promise seamless management across different operating systems but deliver only frustration and complexity. Solutions that often leave a trail of disappointed admins in their wake, often forcing you to manage the tools more than the devices. Fleet flips that script by letting you interact directly with each operating system’s native features. Whether Apple’s macOS, Microsoft’s Windows, or various Linux distributions, Fleet provides a consistent management experience without forcing you to “talk Windows” to your Macs or vice versa.
+
+
+## Managing every OS like it’s your favorite
+
+Fleet introduces familiar concepts like custom attributes and dynamic grouping but adapts them to work with the nuances of each operating system. This means you can manage your macOS, Windows, and Linux devices without juggling multiple management platforms or dealing with convoluted workarounds. Everything is streamlined in one open-source platform, giving you direct access to the data and events from each OS.
+
+By working directly with native operating system features, Fleet ensures you don’t lose low-level control or compromise on capabilities. Instead of managing multiple MDM solutions, you can focus on managing your devices—regardless of OS.
+
+For example:
+
+* **Operating systems**: You can enforce OS updates with Declarative Device Management (DDM), Nudge, and Windows Update from one console.
+* **Automated enrollment**: Drop-ship devices to your end users with Apple Business Manager or Autopilot and let them set up their own accounts. No IT help is needed.
+* **Config management**: Manage settings with configuration profiles for Apple and device profiles for Windows. Use a canary team to test changes before they go live.
+* **App management**: Automatically keep applications and plugins secure and up-to-date. Install the software end users need or let them install it themselves via self-service.
+* **Scripts and events**: Easily manage and version control your custom script library. Execute shell and PowerShell scripts when computers drift from the baseline.
+* **Keep up with Apple**: Fleet's team and community stay current on the latest features and releases from all supported platform vendors, not just Apple.
+
+## Switching platforms is disruptive
+
+It’s understandable to be cautious about adopting a new management solution, especially if you’re concerned about the time and effort involved in switching. However, Fleet is designed with ease of transition in mind. Our platform integrates seamlessly with your existing tools and workflows, minimizing disruption. Plus, with our comprehensive documentation and responsive community support, you’ll have everything you need to get up and running quickly. Fleet’s flexible deployment options let you start small and scale at your pace, ensuring a smooth, controlled migration.
+
+
+
+## One platform, many possibilities
+
+Fleet isn’t just about making cross-platform management tolerable—it’s about making it genuinely effective. With Fleet, you can enforce OS updates, automate device enrollment, manage configurations, and keep applications secure, all from one place. You can also deploy Fleet yourself at any time; it’s 100% source-available, meaning you can look at the source code for how any part of it works.
+
+And because Fleet is open-source, it’s designed with flexibility and transparency in mind. You can tailor it to fit your organization’s needs, whether you’re managing a few hundred devices or tens of thousands.
+
+
+
+Mad props to how easy making a deploy pkg of the agent was. I wish everyone made stuff that easy.
+
+
+
+_Wes Whetstone, Staff CPE at Stripe_
+
+
+## The takeaway
+
+Cross-platform management doesn’t have to be the headache it’s been in the past. Fleet is here to simplify how you manage your devices, no matter what mix of operating systems you’re dealing with. It’s time to let go of the myth that managing different platforms means managing different tools. With Fleet, you can have everything you need in one place—without the anxiety.
+
+Ready to get started?
+
+Visit our [start page](https://fleetdm.com/start) to begin your journey.
+
+
+
+
+
+
+
diff --git a/articles/deploy-fleet-on-cloudgov.md b/articles/deploy-fleet-on-cloudgov.md
index c075ab6c9b..18f0c238a8 100644
--- a/articles/deploy-fleet-on-cloudgov.md
+++ b/articles/deploy-fleet-on-cloudgov.md
@@ -1,7 +1,5 @@
# Deploy Fleet on Cloud.gov (Cloud Foundry)
-> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
-

Cloud.gov is a [FEDRAMP moderate Platform-as-a-Service
diff --git a/articles/deploy-security-agents.md b/articles/deploy-security-agents.md
new file mode 100644
index 0000000000..20d6cd28ab
--- /dev/null
+++ b/articles/deploy-security-agents.md
@@ -0,0 +1,97 @@
+# Deploy security agents
+
+
+
+Fleet [v4.50.0](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.50.0) introduced the ability to upload and deploy security agents to your hosts. Beyond a [bootstrap package](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#bootstrap-package) at enrollment, deploying security agents allows you to specify and verify device configuration using a pre-enrollment osquery query and customization of the install and post-install scripts, allowing for key and license deployment and configuration. This guide will walk you through the steps to upload, configure, and install a security agent to hosts in your fleet.
+
+## Prerequisites
+
+* Fleet [v4.50.0](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.50.0).
+* `fleetd` 1.25.0 deployed via MDM or built with the `--scripts-enabled` flag.
+* An S3 bucket [configured](https://fleetdm.com/docs/configuration/fleet-server-configuration#s-3-software-installers-bucket) to store the installers.
+* Increase any load balancer timeouts to at least 5 minutes for the following endpoints:
+ * [Add software](https://fleetdm.com/docs/rest-api/rest-api#add-software).
+ * [Batch-apply software](https://fleetdm.com/docs/rest-api/rest-api#add-software).
+
+## Step-by-step instructions
+
+### Access security agent installers
+
+To access and manage security agents in Fleet:
+
+* **Navigate to the Software page**: Click on the "Software" tab in the main navigation menu.
+* **Select a team**: Click on the dropdown at the top left of the page.
+* **Find your software**: using the filters on the top of the table, you can choose between:
+ * “Available for install” filters software that can be installed on your hosts.
+ * “Self-service” filters software that end users can install from Fleet Desktop.
+* **Select security agent installer**: Click on a software package to view details and access additional actions for the agent installer.
+
+### Add a security agent to a team
+
+* **Navigate to the Software page**: Click on the "Software" tab in the main navigation menu.
+* **Select a team**: Select a team or the "No team" team to add a security agent.
+
+> Security agents cannot be added to "All teams"
+
+* Click the “Add Software” button in the top right corner, and a modal will appear.
+* Choose a file to upload. `.pkg`, `.msi`, `.exe`, or `.deb` files are supported.
+* After selecting a file, a default install script will be pre-filled. If the security agent requires a custom installation process, this script can be edited.
+* To allow users to install the software from Fleet Desktop, check the “Self-service” checkbox.
+* To customize the conditions, click on “Advanced options”:
+ * **Pre-install condition**: A pre-install condition is a valid osquery SQL statement that will be evaluated on the host before installing the software. If provided, the installation will proceed only if the query returns any value.
+ * **Post-install script** A post-install script will run after the installation is complete, allowing you to configure the security agent right after installation. If this script returns a non-zero exit code, the installation will fail, and `fleetd` will attempt to uninstall the software.
+
+### Install a security agent on a host
+
+After an installer is added to a team, it can be installed on hosts via the UI.
+
+* **Navigate to the Hosts page**: Click on the "Hosts" tab in the main navigation menu.
+* **Navigate to the Host details page**: Click the host you want to install the security agent.
+* **Navigate to the Host software tab**: In the host details, search for the tab named “Software”
+* **Find your security agent**: Use the search bar and filters to search for your security agent.
+* **Install the security agent on the host**: In the leftmost row of the table, click on “Actions” > “Install.”
+* **Track installation status**: by either
+ * Checking the “Install status” in the host software table.
+ * Navigate to the “Details” tab on the host details page and check the activity log.
+
+### Edit a security agent
+
+Security agent installers can’t be edited via the UI. To modify an installer, remove it from the UI and add a new one.
+
+### Remove a security agent from a team
+
+* **Navigate to the Software page**: Click on the "Software" tab in the main navigation menu.
+* **Select a team**: Select a team or the "No team" team to add a security agent.
+* **Find your software**: using the filters on the top of the table, you can choose between:
+ * “Available for install” filters software can be installed on your hosts.
+ * “Self-service” filters software that users can install from Fleet Desktop.
+* **Select security agent installer**: Click on a software package to view details.
+* **Remove security agent installer**: From the Actions menu, select "Delete." Click the "Delete" button on the modal.
+
+> Removing a security agent from a team will not uninstall the agent from the existing host(s).
+
+### Manage security agents with the REST API
+
+Fleet also provides a REST API for managing software programmatically. The API allows you to add, update, retrieve, list, and delete software. Detailed documentation on Fleet's [REST API is available](https://fleetdm.com/docs/rest-api/rest-api#software).
+
+### Manage security agents with GitOps
+
+Installers for security agents can be managed via `fleetctl` using [GitOps](https://fleetdm.com/docs/using-fleet/gitops).
+
+Please refer to the documentation specific to [managing software with GitOps](https://fleetdm.com/docs/using-fleet/gitops#software). For a real-world example, [see how we manage software at Fleet](https://github.com/fleetdm/fleet/tree/main/it-and-security/teams).
+
+
+## Conclusion
+
+Deploying security agents with Fleet is straightforward and ensures your hosts are protected with the latest security measures. This guide has shown you how to access, add, and install security agents, as well as manage them using the REST API and `fleetctl`. Following these steps can effectively equip your fleet with the necessary security tools.
+
+See Fleet's [documentation](https://fleetdm.com/docs/using-fleet) and additional [guides](https://fleetdm.com/guides) for more details on advanced setups, software features, and vulnerability detection.
+
+
+
+
+
+
+
+
+
diff --git a/articles/discovering-chrome-ai-using-fleet.md b/articles/discovering-chrome-ai-using-fleet.md
new file mode 100644
index 0000000000..3c39b370b3
--- /dev/null
+++ b/articles/discovering-chrome-ai-using-fleet.md
@@ -0,0 +1,69 @@
+# Discovering Chrome AI using Fleet
+
+
+
+# Discovering AI in Chrome with Fleet
+
+Staying ahead of technological innovations is crucial for individuals and organizations. Google Chrome, one of the most widely used web browsers, continually evolves to incorporate new features, including artificial intelligence (AI). This article will guide you through detecting if AI capabilities have been enabled in Chrome on macOS using Fleet.
+
+## Introduction to Chrome AI innovations
+
+Google Chrome has integrated AI to enhance user experience by providing intelligent suggestions, improving search results, and offering in-browser assistance. Visit the [Chrome AI Innovations page](https://www.google.com/chrome/ai-innovations/) for more information.
+
+## Using Fleet to discover AI features in Chrome
+
+Fleet, a comprehensive device management and security tool, allows organizations to monitor installed software configurations and enabled features on endpoints and servers. Investigating this data enables Fleet admins to build SQL queries for detection.
+
+### Step 1: Understanding Chrome's preferences JSON file
+
+On macOS, Chrome stores user settings and configurations in a JSON file at the following path:
+
+```
+/Users//Library/Application Support/Google/Chrome/Default/Preferences
+```
+
+### Step 2: Identifying AI-related settings
+
+Chrome AI-related preferences are stored in the `optimization_guide` section of the Chrome Preferences file. The `tab_organization_setting_state` key / value field will signify if AI features are enabled.
+
+`jq` is a lightweight and powerful command-line tool for parsing, filtering, and manipulating JSON data. It can extract and parse information from JSON files at specific key / value fields.
+
+In this case, `jq` is used to locate and read the value of the `tab_organization_setting_state` key within the Chrome Preferences file. This knowledge allows an admin to craft a Fleet query for reporting the state of the Chrome AI settings.
+
+- If enabled, the setting will return `1`.
+
+
+
+```
+% jq '.optimization_guide.tab_organization_setting_state' /Users//Library/Application\ Support/Google/Chrome/Default/Preferences
+1
+```
+
+- If disabled, the setting will return `2`.
+
+
+
+```
+% jq '.optimization_guide.tab_organization_setting_state' /Users//Library/Application\ Support/Google/Chrome/Default/Preferences
+2
+```
+
+### Step 3: Query the JSON file with Fleet
+
+To detect Chrome AI features in Fleet, use a SQL query like the following:
+
+```
+SELECT fullkey,path FROM parse_json WHERE path LIKE '/Users/%/Library/Application Support/Google/Chrome/Default/Preferences' AND fullkey='optimization_guide/tab_organization_setting_state';
+```
+
+### Conclusion
+
+Fleet's powerful querying abilities allow you to monitor features like these across all of your devices.
+
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/downgrading-fleet.md b/articles/downgrade-fleet.md
similarity index 87%
rename from docs/Using Fleet/downgrading-fleet.md
rename to articles/downgrade-fleet.md
index 34f8fd04ff..0874e27c66 100644
--- a/docs/Using Fleet/downgrading-fleet.md
+++ b/articles/downgrade-fleet.md
@@ -1,4 +1,4 @@
-# Downgrading from Fleet Premium
+# Downgrade from Fleet Premium
Follow these steps to downgrade your Fleet instance from Fleet Premium.
@@ -34,8 +34,9 @@ Follow these steps to downgrade your Fleet instance from Fleet Premium.
1. Remove your license key from your Fleet configuration. Documentation on where the license key is located in your configuration is [here](https://fleetdm.com/docs/deploying/configuration#license).
2. Restart your Fleet server.
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/Using Fleet/MDM-disk-encryption.md b/articles/enforce-disk-encryption.md
similarity index 76%
rename from docs/Using Fleet/MDM-disk-encryption.md
rename to articles/enforce-disk-encryption.md
index 8d8278763f..8dd2b6419f 100644
--- a/docs/Using Fleet/MDM-disk-encryption.md
+++ b/articles/enforce-disk-encryption.md
@@ -1,4 +1,4 @@
-# Disk encryption
+# Enforce disk encryption
_Available in Fleet Premium_
@@ -8,7 +8,9 @@ In Fleet, you can enforce disk encryption for your macOS and Windows hosts.
When disk encryption is enforced, hosts’ disk encryption keys will be stored in Fleet.
-For Windows hosts, disk encryption is enforced on the C: volume (default system/OS drive).
+For macOS hosts that automatically enroll, disk encryption is enforced during Setup Assistant.
+
+For Windows, disk encryption is enforced on the C: volume (default system/OS drive).
## Enforce disk encryption
@@ -54,15 +56,13 @@ How to view the disk encryption key:
## Migrate macOS hosts
-When migrating macOS hosts another MDM solution, in order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must take action.
+When migrating macOS hosts from another MDM solution, in order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must log out or restart their device.
-If the host already had disk encryption turned on, the user will need to input their password.
+Share [these guided instructions](https://fleetdm.com/guides/mdm-migration#how-to-turn-on-disk-encryption) with your end users.
-If the host did not already have disk encryption turned on, the user will need to log out or restart their computer.
-
-Share [these guided instructions](./MDM-migration-guide.md#how-to-turn-on-disk-encryption) with your end users.
-
-
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/MDM-OS-updates.md b/articles/enforce-os-updates.md
similarity index 56%
rename from docs/Using Fleet/MDM-OS-updates.md
rename to articles/enforce-os-updates.md
index 6986593c76..de3fdbc83d 100644
--- a/docs/Using Fleet/MDM-OS-updates.md
+++ b/articles/enforce-os-updates.md
@@ -1,18 +1,14 @@
-# OS updates
+# Enforce OS updates
_Available in Fleet Premium_
-In Fleet you can enforce OS updates on your macOS and Windows hosts remotely.
-
-## Enforce OS updates
-
-You can enforce OS updates using the Fleet UI, Fleet API, or [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
+In Fleet, you can enforce OS updates on your macOS, Windows, iOS, and iPadOS hosts remotely using the Fleet UI, Fleet API, or [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
Fleet UI:
1. Head to the **Controls** > **OS updates** tab.
-2. To enforce OS updates for macOS, select **macOS** and set a **Minimum version** and **Deadline**.
+2. To enforce OS updates for macOS, iOS, or iPadOS, select the platform and set a **Minimum version** and **Deadline**.
3. For Windows, select **Windows** and set a **Deadline** and **Grace period**.
@@ -22,21 +18,22 @@ Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-ap
### macOS
-When a minimum version is enforced, the end users see a native macOS notification (DDM) once per day. Users can choose to update ahead of the deadline or schedule it for that night. 24 hours before the deadline, the notification appears hourly and ignores Do Not Disturb. One hour before the deadline, the notification appears every 30 minutes, and then every 10 minutes.
+When a minimum version is enforced, the end users see a native macOS notification (DDM) once per day. Users can choose to update ahead of the deadline or schedule it for that night. 24 hours before the deadline, the notification appears hourly and ignores Do Not Disturb. One hour before the deadline, the notification appears every 30 minutes and then every 10 minutes.
If the host was turned off when the deadline passed, the update will be scheduled an hour after it’s turned on.
-### macOS (below version 14.0)
+For macOS devices that use Automated Device Enrollment (ADE), if the device is below the specified minimum version, it will be required to update to the latest [available version](#available-macos-ios-and-ipados-versions) during ADE before device setup and enrollment can proceed.
-End users are encouraged to update macOS (via [Nudge](https://github.com/macadmins/nudge)).
+### iOS and iPadOS
-
+End users will see a notification in their Notification Center after the deadline when a minimum version is enforced. They can’t use their iPhone or iPad until the OS update is installed.
-| | > 1 day before deadline | < 1 day before deadline | Past deadline |
-| ------------------------------------ | ----------------------- | ----------------------- | --------------------- |
-| Nudge window frequency | Once a day at 8pm GMT | Once every 2 hours | Immediately on login |
-| End user can defer | ✅ | ✅ | ❌ |
-| Nudge window is dismissible | ✅ | ✅ | ❌ |
+For iOS and iPadOS devices that use Automated Device Enrollment (ADE), if the device is below the specified
+minimum version, it will be required to update to the latest [available version](#available-macos-ios-and-ipados-versions) during ADE before device setup and enrollment can proceed.
+
+### Available macOS, iOS, and iPadOS versions
+
+The Apple Software Lookup Service (available at [https://gdmf.apple.com/v2/pmv](https://gdmf.apple.com/v2/pmv)) is the official resource for obtaining a list of publicly available updates, upgrades, and Rapid Security Responses. Make sure to use versions available in GDMF; otherwise, the update will not be scheduled.
### Windows
@@ -50,7 +47,21 @@ If an end user was on vacation when the deadline passed, the end user is given a
Fleet enforces OS updates for quality and feature updates. Read more about the types of Windows OS updates in the Microsoft documentation [here](https://learn.microsoft.com/en-us/windows/deployment/update/get-started-updates-channels-tools#types-of-updates).
-
-
-
-
+### macOS (below version 14.0)
+
+End users are encouraged to update macOS (via [Nudge](https://github.com/macadmins/nudge)).
+
+
+
+| | > 1 day before deadline | < 1 day before deadline | Past deadline |
+| ------------------------------------ | ----------------------- | ----------------------- | --------------------- |
+| Nudge window frequency | Once a day at 8pm GMT | Once every 2 hours | Immediately on login |
+| End user can defer | ✅ | ✅ | ❌ |
+| Nudge window is dismissible | ✅ | ✅ | ❌ |
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/enroll-hosts.md b/articles/enroll-hosts.md
similarity index 96%
rename from docs/Using Fleet/enroll-hosts.md
rename to articles/enroll-hosts.md
index 6112eec51e..0635855596 100644
--- a/docs/Using Fleet/enroll-hosts.md
+++ b/articles/enroll-hosts.md
@@ -1,7 +1,5 @@
# Enroll hosts
-## Introduction
-
Fleet gathers information from an [osquery](https://github.com/osquery/osquery) agent installed on each of your hosts. The recommended way to install osquery is using fleetd.
You can enroll macOS, Windows or Linux hosts via the [CLI](#cli) or [UI](#ui). To learn how to enroll Chromebooks, see [Enroll Chromebooks](#enroll-chromebooks).
@@ -14,9 +12,9 @@ Fleet supports the [latest version of osquery](https://github.com/osquery/osquer
> You must have `fleetctl` installed. [Learn how to install `fleetctl`](https://fleetdm.com/docs/using-fleet/fleetctl-cli#installing-fleetctl).
-The `fleetctl package` command is used to generate Fleet's agent (fleetd).
+The `fleetctl package` command is used to generate Fleet's agent (fleetd) install package..
-The `--type` flag is used to specify the fleetd installer type:
+The `--type` flag is used to specify the fleetd installer type. Note that Windows can only generate an MSI package:
- macOS: .pkg
- Windows: .msi
- Linux: .deb or .rpm
@@ -39,7 +37,7 @@ To generate Fleet's agent (fleetd) in Fleet UI:
1. Go to the **Hosts** page, and select **Add hosts**.
2. Select the tab for your desired platform (e.g. macOS).
-3. A CLI command with all necessary flags will be generated. Copy and run the command with [fleetctl](https://fleetdm.com/docs/using-fleet/fleetctl-cli) installed.
+3. A CLI command with all necessary flags to generate an install package will be generated. Copy and run the command with [fleetctl](https://fleetdm.com/docs/using-fleet/fleetctl-cli) installed.
### Enroll host to a specific team
@@ -54,7 +52,7 @@ You can use your software management tool of choice to distribute Fleet's agent
### Fleet Desktop
-[Fleet Desktop](./Fleet-desktop.md) is a menu bar icon available on macOS, Windows, and Linux that gives your end users visibility into the security posture of their machine.
+[Fleet Desktop](https://fleetdm.com/guides/fleet-desktop) is a menu bar icon available on macOS, Windows, and Linux that gives your end users visibility into the security posture of their machine.
You can include Fleet Desktop in Fleet's agent (fleetd) by including `--fleet-desktop` in the `fleetctl package` command.
@@ -379,6 +377,9 @@ but can result in a large volume of error logs. In fleetd v1.15.1, we added an e
Applying the environmental variable `"FLEETD_SILENCE_ENROLL_ERROR"=1` on a host will silence fleetd enrollment errors if a `--fleet-url` is not present.
This variable is read at launch and will require a restart of the Orbit service if it is not set before installing `fleetd` v1.15.1.
-
+
+
+
+
+
-
diff --git a/articles/filtering-software-by-vulnerability.md b/articles/filtering-software-by-vulnerability.md
new file mode 100644
index 0000000000..8c8326bbc6
--- /dev/null
+++ b/articles/filtering-software-by-vulnerability.md
@@ -0,0 +1,44 @@
+# Filtering software by vulnerability in Fleet
+
+
+
+## Introduction
+
+Fleet has introduced a powerful new feature that allows you to filter software by its associated vulnerabilities, helping you prioritize patches more effectively. Whether you're managing hundreds or thousands of software titles, this feature makes it easier to identify and address the most critical vulnerabilities in your environment.
+
+This filtering capability is particularly useful in environments where patch management is critical to your security posture. By filtering software based on vulnerability severity and known exploits, you can first ensure that the most critical issues are addressed, enhancing your overall security strategy.
+
+## Prerequisites
+
+* Fleet version 4.56 or later
+* Premium users have access to advanced filters by severity level and known exploited vulnerabilities
+
+### Filtering Software by Vulnerability
+
+1. **Navigate to the Software page**: In your Fleet dashboard, go to the **Software** tab. This will display a list of all the software detected in your environment.
+
+2. **Add filters**: Click on the **Add Filters** button. This will open options for filtering the software list based on specific criteria.
+
+3. **Choose severity level**: From the dropdown menu, select the **Severity level** of vulnerabilities you're interested in. This allows you to focus on software with the highest severity of vulnerabilities, such as "Critical" or "High."
+
+4. **Toggle "Has known exploit"**: You can refine your filter by toggling the **Has known exploit** option. This will filter the software list to show only those with vulnerabilities that have known exploits, enabling you to prioritize these for patching.
+
+5. **Review filtered results**: Once you've applied your filters, the software list will update to show only the software that meets your criteria. This filtered view will help you prioritize which software needs immediate attention in your patching strategy.
+
+### Using the REST API to filter software for vulnerabilities
+
+Fleet provides a REST API to filter software for vulnerabilities, allowing you to integrate this functionality into your automated workflows. Learn more about Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api#vulnerabilities).
+
+## Conclusion
+
+The new software filtering feature in Fleet makes it easier than ever to manage vulnerabilities in your software environment. You can better protect your organization from potential threats by prioritizing patches based on severity and known exploits. Explore the API capabilities to integrate this feature into your broader security workflows.
+
+For more tips and detailed guides, don’t forget to check out the Fleet [documentation](https://fleetdm.com/docs/get-started/why-fleet).
+
+
+
+
+
+
+
+
diff --git a/articles/fleet-4.55.0.md b/articles/fleet-4.55.0.md
new file mode 100644
index 0000000000..309e0e70df
--- /dev/null
+++ b/articles/fleet-4.55.0.md
@@ -0,0 +1,132 @@
+# Fleet 4.55.0 | MySQL 8, arm64 support, FileVault improvements, VPP support.
+
+
+
+Fleet 4.55.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.55.0) or continue reading to get the highlights.
+For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs.
+
+## Highlights
+
+* MySQL 8 support, MySQL 5.7 sunsets
+* FileVault key rotation with Escrow Buddy
+* FileVault enforcement at enrollment
+* Arm64 support
+* VPP app support for macOS
+* "No team" software support
+
+### MySQL 8 support, MySQL 5.7 sunsets
+
+Fleet has updated its database compatibility by adding support for MySQL 8, while simultaneously dropping support for MySQL 5.7. This change aligns Fleet with the latest advancements in database technology, offering enhanced performance, security, and features available in MySQL 8. Organizations using Fleet are encouraged to upgrade their database systems to MySQL 8 to take full advantage of these improvements. By focusing on the latest supported versions, Fleet ensures that its platform remains robust, secure, and well-equipped to handle the demands of modern IT environments while phasing out older versions that may not provide the same level of performance or security.
+
+### FileVault key rotation with Escrow Buddy
+
+Fleet now includes support for FileVault key rotation using [Escrow Buddy](https://github.com/macadmins/escrow-buddy), a tool developed by the Netflix Client Systems Engineering team for the MacAdmins community to securely manage and rotate FileVault recovery keys on macOS devices. This feature allows IT administrators to automate the process of rotating FileVault keys, ensuring that encrypted macOS hosts remain secure while maintaining access control. By integrating with Escrow Buddy, Fleet enables seamless key management, reducing the administrative burden of manually rotating keys and enhancing the overall security posture of macOS environments. This update reflects Fleet's commitment to providing robust security tools that integrate with trusted community resources, ensuring organizations can efficiently manage device encryption and recovery processes.
+
+### FileVault enforcement at enrollment
+
+Fleet now supports enforcing FileVault encryption during the enrollment process for macOS devices, ensuring that all newly enrolled Macs are automatically encrypted. This feature enhances security by mandating that FileVault is enabled as part of the initial device setup, reducing the risk of unencrypted data on managed endpoints. By integrating FileVault enforcement into the enrollment workflow, Fleet helps organizations maintain a consistent security posture across their macOS fleet, ensuring compliance with internal policies and regulatory requirements. This update underscores Fleet's commitment to providing comprehensive security management tools that protect sensitive data and simplify the administration of macOS devices.
+
+### Arm64 support
+
+Fleet now includes support for Linux hosts running on the arm64 architecture. This update enables organizations to integrate a broader range of devices into their Fleet management system, ensuring comprehensive oversight and control across diverse hardware environments. By supporting arm64 Linux hosts, Fleet caters to the growing use of ARM-based systems in various sectors, allowing IT administrators to manage these devices with the same level of detail and efficiency as traditional x86-based hosts. This aligns with Fleet's commitment to providing versatile and inclusive device management solutions, empowering users to maintain a unified and efficient IT infrastructure.
+
+### VPP app support for macOS
+
+Fleet now supports installing Volume Purchase Program (VPP) apps from the Apple App Store on macOS devices. This feature enables IT administrators to deploy and manage apps purchased through Apple's VPP directly to macOS hosts, streamlining the process of distributing essential software across the organization. By integrating VPP app installations into Fleet, organizations can ensure that licensed applications are efficiently deployed to the appropriate devices, improving software management and compliance. This update enhances Fleet's capabilities in managing macOS environments, offering a more seamless and centralized approach to app distribution for enterprise and educational settings.
+
+### "No team" software support
+
+Fleet now supports adding software to the "No team" team, providing greater flexibility in managing software across an organization's devices. This feature allows administrators to deploy and manage software that applies universally without being restricted to specific teams. By adding software to the "No team" team, IT teams can ensure that essential tools and applications are available across all devices, regardless of their team assignment. This update simplifies the management of widely used software and enhances the ability to maintain consistency and compliance across the entire fleet. It reflects Fleet's commitment to offering versatile solutions that cater to diverse organizational needs and streamline device management processes.
+
+## Changes
+
+**NOTE:** Beginning with v4.55.0, Fleet no longer supports MySQL 5.7 because it has reached [end of life](https://mattermost.com/blog/mysql-5-7-reached-eol-upgrade-to-mysql-8-x-today/#:~:text=In%20October%202023%2C%20MySQL%205.7,to%20upgrade%20to%20MySQL%208.). The minimum version supported is MySQL 8.0.36.
+
+### Endpoint Operations
+
+- Added support for generating `fleetd` packages for Linux ARM64.
+- Added new `fleetctl package` --arch flag.
+- Updated `fleetctl package` command to remove the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
+- Updated maintenance window descriptions to update regularly to match the failing policy description/resolution.
+- Updated maintenance windows using Google Calendar so that calendar events are now recreated within 30 seconds if deleted or moved to the past.
+ - Fleet server watches for potential changes for up to 1 week after original event time. If event is moved forward more than 1 week, then after 1 week Fleet server will check for event changes once every 30 minutes.
+ - **NOTE:** These near real-time updates may add additional load to the Google Calendar API, so it is recommended to use API usage alerts or other monitoring methods.
+
+### Device Management
+
+- Integrated [Escrow Buddy](https://github.com/macadmins/escrow-buddy) to add enforcement of FileVault during the MacOS Setup Assistant process for hosts that are
+enrolled into teams (or no team) with disk encryption turned on. Thank you homebysix and team!
+- Added OS updates support to iOS/iPadOS devices.
+- Added iOS and iPadOS device details refetch triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint.
+- Added iOS and iPadOS user-installed apps to Fleet.
+- Added iOS and iPadOS apps to be installed using Apple's VPP (Volume Purchase Program) to Fleet.
+- Added support for VPP to GitOps.
+- Added the `POST /mdm/apple/vpp_token`, `DELETE /mdm/apple/vpp_token` and `GET /vpp` endpoints and related functionality.
+- Added new `GET /software/app_store_apps` and `POST /software/app_store_apps` endpoints and associated functionality.
+- Added the associated VPP apps to the `GET /software/titles` and `GET /software/titles/:id` endpoints.
+- Added the associated VPP apps to the `GET /hosts/:id/software` and `GET /device/:token/software` endpoints.
+- Added support to delete a VPP app from a team in `DELETE /software/titles/:software_title_id/available_for_install`.
+- Added `exclude_software` query parameter to "Get host by identifier" API.
+- Added ability to add/remove/disable apps with VPP in the Fleet UI.
+- Added a warning banner to the UI if the uploaded VPP token is about to expire/has expired.
+- Added UI updates for VPP feature on host software and my device pages.
+- Added global activity support for VPP-related activities.
+- Added UI features for managing VPP apps for iPadOS and iOS hosts.
+- Updated profile activities to include iOS and iPadOS.
+- Updated Fleet UI to show OS version compliance on host details page.
+- Added support for "No teams" on all software pages including adding software installers.
+- Added DB migration to support VPP software features.
+- Added DB migration to migrate older team configurations to the new version that includes both installers and App Store apps.
+- Linux lock/unlock scripts now make use of pam_nologin to keep AD users locked out.
+- Installed software list now includes Linux .deb packages that are 'on hold'.
+- Added a special-case to properly name the Notion .exe Windows installer the same as how it will be reported by osquery post-install.
+- Increased threshold to renew Apple SCEP certificates for MDM enrollments to 180 days.
+
+### Vulnerability Management
+
+- Fixed CVEs identified as 'Rejected' in NVD not matching against software.
+- Fixed false negative vulnerabilities with IntelliJ IDEA CE and PyCharm CE installed via Homebrew.
+
+### Bug fixes and improvements
+
+- Dropped support for MySQL 5.7 and raised minimum required to MySQL 8.0.36.
+- Updated software pre-install to use new GitOps format for query.
+- Updated UI tooltips for pending OS settings.
+- Added a migration to migrate older team configurations to the new version that includes both installers and App Store apps.
+- Fixed a styling issue in the controls > OS settings > disk encryption table.
+- Fixed a bug in `fleetctl preview` that was causing it to fail if Docker was installed without support for the deprecated `docker-compose` CLI.
+- Fixed an issue where the app-wide warning banners were not showing on the initial page load.
+- Fixed a bug where the hosts page would sometimes allow excess pagination.
+- Fixed a bug where software install results could not be retrieved for deleted hosts in the activity feed.
+- Fixed path that was incorrect for the download software installer package endpoint `GET /software/titles/:software_title_id/package`.
+- Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set.
+- Fixed the "Available for install" filter in the host's software page so that installers that were requested to be installed on the host (regardless of installation status) also show up in the list.
+- Fixed a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly processed by 4.54.0.
+- Fixed a bug in `fleetctl preview` that was causing it to fail if Docker was installed without support for the deprecated `docker-compose` CLI.
+- Fixed a bug where software install results could not be retrieved for deleted hosts in the activity feed.
+- Fixed a bug where a software installer (a package or a VPP app) that has been installed on a host still shows up as "Available for install" and can still be requested to be installed after the host is transferred to a different team without that installer (or after the installer is deleted).
+- Fixed the "Available for install" filter in the host's software page so that installers that were requested to be installed on the host (regardless of installation status) also show up in the list.
+
+## Fleet 4.54.1 (Jul 24, 2024)
+
+### Bug fixes
+- Fixed a startup bug by performing an early restart of orbit if an agent options setting has changed.
+- Implemented a small refactor of orbit subsystems.
+- Removed the `--version` flag from the `fleetctl package` command. The version of the package can now be controlled by the `--orbit-channel` flag.
+- Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set .
+- In `fleetctl package` command, removed the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
+- Fixed a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly processed by 4.54.0.
+- Re-enabled cached logins after windows Unlock.
+
+
+
+## Ready to upgrade?
+
+Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.55.0.
+
+
+
+
+
+
+
diff --git a/articles/fleet-4.56.0.md b/articles/fleet-4.56.0.md
new file mode 100644
index 0000000000..14089ad9d6
--- /dev/null
+++ b/articles/fleet-4.56.0.md
@@ -0,0 +1,153 @@
+# Fleet 4.56.0 | Enhanced MDM migration, Exact CVE Search, and Self-Service VPP Apps.
+
+
+
+Fleet 4.56.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.56.0) or continue reading to get the highlights.
+For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs.
+
+## Highlights
+* Improved end-user MDM migration
+* Enforce minimum OS version for MDM enrollment
+* Exact match CVE search
+* Software vulnerabilities severity filter
+* Self-service VPP apps
+* Multiple ABM and VPP support
+
+
+### Improved end-user MDM migration
+
+Fleet has improved the end-user MDM migration workflow on macOS by enabling the migration of hosts manually enrolled in a third-party MDM over to Fleet MDM using the Fleet Desktop application. Previously, this capability was limited to hosts enrolled through Apple's Automated Device Enrollment (ADE), but with this update, manually enrolled hosts can now be seamlessly migrated to Fleet MDM. This feature is specifically available for macOS Sonoma devices (macOS 14 or greater). It makes the migration process more flexible and accessible for organizations looking to centralize their MDM management under Fleet. This enhancement simplifies the transition to Fleet MDM for a broader range of macOS devices, ensuring that all hosts can be managed consistently and securely.
+
+
+### Enforce minimum OS version for MDM enrollment
+
+Fleet now enforces a minimum operating system (OS) requirement for macOS devices before they can be enrolled into Fleet's MDM. This feature ensures that only devices running a specified minimum macOS version can be enrolled, helping organizations maintain a consistent security and compliance baseline across their fleet. By setting a minimum OS requirement, Fleet prevents older, potentially less secure macOS versions from being managed under its MDM, thereby reducing vulnerabilities and ensuring all enrolled devices meet the organization's standards. This update enhances Fleet's ability to enforce security policies from the outset, ensuring that all devices in the fleet are up-to-date and capable of supporting the latest security and management features.
+
+
+### Exact match CVE search
+
+Fleet has enhanced its CVE (Common Vulnerabilities and Exposures) search functionality by introducing exact match searching, allowing users to quickly and accurately find specific vulnerabilities across their fleet. This improvement ensures that security teams can pinpoint the exact CVE they are investigating without sifting through irrelevant results, streamlining the vulnerability management process. Additionally, Fleet provides better context in cases where no results are found, helping users understand why a particular CVE might not be present in their environment. This update improves the overall user experience in vulnerability management, making it easier to maintain security and compliance across all managed devices.
+
+
+### Software vulnerabilities severity filter
+
+Fleet has introduced improved filtering capabilities for vulnerable software, allowing users to filter vulnerabilities by severity level. This enhancement enables security teams to prioritize their response efforts by focusing on the most critical vulnerabilities, ensuring that the highest-risk issues are promptly addressed. By providing a straightforward and efficient way to filter vulnerable software based on severity, Fleet helps organizations streamline their vulnerability management processes, reducing the risk of security incidents. This update aligns with Fleet's commitment to providing powerful tools that enhance the efficiency and effectiveness of security operations across all managed devices.
+
+
+### Self-Service Apple App Store apps
+
+Fleet enables organizations to assign and install Apple App Store apps purchased through the Volume Purchase Program (VPP) directly via Self-Service using Fleet Desktop. This new feature allows IT administrators to make VPP-purchased apps available to end users seamlessly and flexibly. By integrating VPP app distribution into the Fleet Desktop Self-Service portal, organizations can streamline the deployment of essential software across their macOS devices, ensuring that users have easy access to the tools they need while maintaining control over software distribution. This update enhances the overall user experience and operational efficiency, empowering end users to install approved applications with minimal IT intervention.
+
+
+### Multiple Apple Business Manager and VPP support
+
+Fleet now enables administrators to add and manage multiple Apple Business Manager (ABM) and Volume Purchase Program (VPP) tokens within a single Fleet instance. This feature is designed for both Managed Service Providers (MSPs) and large enterprises, allowing them to create separate automatic enrollment and App Store app workflows for different clients or divisions, each with their own ABM and VPP tokens. Whether you’re managing devices for multiple customers or supporting large organizations with distinct divisions, this update simplifies the process of handling macOS, iOS, and iPadOS devices. With support for multiple ABM and VPP connections, Fleet streamlines software and device management across varied environments, providing a scalable solution for both MSPs and enterprises looking to centralize control while maintaining flexibility for different user groups.
+
+
+## Changes
+
+**NOTE:** Beginning with Fleet v4.55.0, Fleet no longer supports MySQL 5.7 because it has reached [end of life](https://mattermost.com/blog/mysql-5-7-reached-eol-upgrade-to-mysql-8-x-today/#:~:text=In%20October%202023%2C%20MySQL%205.7,to%20upgrade%20to%20MySQL%208.). The minimum version supported is MySQL 8.0.36.
+
+## Fleet 4.56.0 (Sep 7, 2024)
+
+### Endpoint operations
+
+- Added index to `query_results` DB table to speed up finding last query timestamp for a given query and host.
+- Added a link in the UI to the error message when a CSR can't be downloaded due to missing private key.
+- Added a disabled overlay to the Other Workflows modal on the policy page.
+- Improved performance of live queries to accommodate for higher volumes when utilizing zero-trust workflows.
+- Improved `fleetctl` gitops error message when trying to change team name to a team that already exists.
+
+### Device management
+
+- Added server support for multiple VPP tokens.
+- Added new endpoints and updated existing endpoints for managing multiple Apple Business Manager tokens.
+- Added support for S3 to store MDM bootstrap packages (uses the same bucket configuration as for software installers).
+- Added support to UI for self service VPP software.
+- Added backend and gitops support for self service VPP.
+- Added ability for MDM migrations if the host is manually enrolled to a 3rd party MDM.
+- Added an offline screen to the macOS MDM migration flow.
+- Added new ABM page to Fleet UI.
+- Added new VPP page to the fleet UI
+- Added support to track the Apple Business Manager "terms expired" API error per token, as well as a global flag that gets set as soon as one token has its terms expired.
+- Updated the instructions on "My device" for MDM migrations on pre-Sonoma macOS hosts.
+- Updated to allow multiple teams to be assigned to the same VPP Token.
+- Updated process so that deleting installed software or VPP app now makes it available for re-installation.
+- Updated to enforce minimum OS version settings during Apple Automated Device Enrollment (ADE).
+- Updated ABM ingestion so that deleted iOS/iPadOS host will continue to report to Fleet as long as host is in Apple Business Manager (ABM).
+- Updated so that refetching an offline iOS/iPadOS host will not add new MDM commands to the queue if previous refetch has not completed yet.
+- Updated UI so that downloading a software installer package now shows the browser's built-in progress bar.
+- Updated relevant documentation to include references to multiple ABM and VPP tokens.
+- Consolidated Automatic Enrollment and VPP settings under the MDM settings integration page.
+- Cleared apps associated with a VPP token if it's moved off of a team.
+
+### Vulnerability management
+
+- Added ALAS bulletins as vulnerability source for Amazon Linux (instead of OVAL for Amazon Linux 2, and adds support for Amazon Linux 1, 2022, and 2023).
+- Added matching rules for July and August Microsoft 365 security updates (https://learn.microsoft.com/en-us/officeupdates/microsoft365-apps-security-updates).
+- Added the following filters to `/software/titles` and `/software/versions` API endpoints: `exploit: bool`, `min_cvss_score: float`, `max_cvss_score: float`.
+- Updated software titles/versions tables to allow for filtering by vulnerabilities including severity and known exploit.
+- Updated to use empty CVE description when the NVD CVE feed doesn't include description entries (instead of panicking).
+- Updated matching software that is not installed by Fleet so that it shows up as 'Available for install' on host details page.
+- Updated base images of `fleetdm/fleetctl`, `fleetdm/bomutils` and `fleetdm/wix` to fix critical vulnerabilities found by Trivy.
+- Updated vulnerability scanning to use `macos` SW target for CPEs of homebrew packages.
+- Updated vulnerability scanning to not ignore software with non-ASCII en dash and em dash characters.
+- Updated `GET /api/v1/fleet/vulnerabilities/{cve}` endpoint to add validation of CVE format, and a 204 response. The 204 response indicates that the vulnerability is known to Fleet but not present on any hosts.
+- Updated the UI to add new empty states for searching vulnerabilities: invalid CVE format searched, a known CVE serached but not present on hosts, not a known CVE searched, exploited vulnerability empty state, operating systems empty state, new icons.
+
+### Bug fixes and improvements
+
+- Added support for MySQL 8.4.2 LTS.
+- Updated Go to go1.22.6.
+- Updated Fleet server to now accept arguments via stdin. This is useful for passing secrets that you don't want to expose as env vars, in the command line, or in the config file.
+- Updated text for "Turn on MDM" banners in UI.
+- Updated ABM host tooltip copy on the manage host page to clarify when host vitals will be available to view.
+- Updated copy on auotmatic enrollment modal on my device page.
+- Updated host details activities tooltip and empty state copy to reflect recently added capabilities.
+- Updated Fleet Free so users see a Premium feature message when clicking to add software.
+- Updated usage reporting to report statistics on new AI features, maintenance window, and `fleetd`.
+- Fixed bug where configuration profile was still showing the old label name after the name was updated.
+- Fixed a bug when a cached prepared statement gets deleted in the MySQL server itself without Fleet knowing.
+- Fixed a bug where the wrong API path was used to download a software installer.
+- Fixed the failing_host_count so it is never 0. This count is normally updated once an hour during cleanups_then_aggregation cron job.
+- Fixed CVE-2024-4030 in Vulncheck feed incorrectly targeting non-Windows hosts.
+- Fixed a bug where the "Self-service" filter for the list of software and the list of host's software did not take App Store apps into account.
+- Fixed a bug where the "My device" page in Fleet Desktop did not show the self-service software tab when App Store apps were available as self-install.
+- Fixed a bug where a software installer (a package or a VPP app) that has been installed on a host still shows up as "Available for install" and can still be requested to be installed after the host is transferred to a different team without that installer (or after the installer is deleted).
+- Fixed the "Available for install" filter in the host's software page so that installers that were requested to be installed on the host (regardless of installation status) also show up in the list.
+- Fixed UI popup messages bleeding off viewport in some cases.
+- Fixed an issue with the scheduling of cron jobs at startup if the job has never run, which caused it to be delayed.
+- Fixed UI to display the label names in case-insensitive alphabetical order.
+
+## Fleet 4.55.2 (Sep 05, 2024)
+
+### Bug fixes
+
+* Removed validation of APNS certificate from server startup. This was no longer necessary because we now allow for APNS certificates to be renewed in the UI.
+* Fixed logic to properly catch and log APNs errors.
+
+## Fleet 4.55.1 (Aug 14, 2024)
+
+### Bug fixes
+
+* Added a disabled overlay to the Other Workflows modal on the policy page.
+* Updated text for "Turn on MDM" banners in UI.
+* Fixed a bug when a cached prepared statement got deleted in the MySQL server itself without Fleet knowing.
+* Continued with an empty CVE description when the NVD CVE feed didn't include description entries (instead of panicking).
+* Scheduled maintenance events are now scheduled over calendar events marked "Free" (not busy) in Google Calendar.
+* Fixed a bug where the wrong API path was used to download a software installer.
+* Improved fleetctl gitops error message when trying to change team name to a team that already exists.
+* Updated ABM (Apple Business Manager) host tooltip copy on the manage host page to clarify when host vitals will be available to view.
+* Added index to query_results DB table to speed up finding the last query timestamp for a given query and host.
+* Displayed the label names in case-insensitive alphabetical order in the fleet UI.
+
+## Ready to upgrade?
+
+Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.56.0.
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/Fleet-desktop.md b/articles/fleet-desktop.md
similarity index 84%
rename from docs/Using Fleet/Fleet-desktop.md
rename to articles/fleet-desktop.md
index b4696943db..c0b1b0e574 100644
--- a/docs/Using Fleet/Fleet-desktop.md
+++ b/articles/fleet-desktop.md
@@ -1,12 +1,7 @@
# Fleet Desktop
-- [Installing Fleet Desktop](#installing-fleet-desktop)
-- [Upgrading Fleet Desktop](#upgrading-fleet-desktop)
-- [Custom Transparency Link](#custom-transparency-link)
-- [Securing Fleet Desktop](#securing-fleet-desktop)
-Fleet Desktop is a menu bar icon available on macOS, Windows, and Linux.
+Fleet Desktop is a menu bar icon available on macOS, Windows, and Linux that gives your end users visibility into the security posture of their machine. This unlocks two key benefits:
-At its core, Fleet Desktop gives your end users visibility into the security posture of their machine. This unlocks two key benefits:
* Self-remediation: end users can see which policies they are failing and resolution steps, reducing the need for IT and security teams to intervene
* Scope transparency: end users can see what the Fleet agent can do on their machines, eliminating ambiguity between end users and their IT and security teams
@@ -16,10 +11,10 @@ At its core, Fleet Desktop gives your end users visibility into the security pos
-## Installing Fleet Desktop
+## Install Fleet Desktop
For information on how to install Fleet Desktop, visit: [Adding Hosts](https://fleetdm.com/docs/using-fleet/adding-hosts#fleet-desktop).
-## Upgrading Fleet Desktop
+## Upgrade Fleet Desktop
Once installed, Fleet Desktop will be automatically updated via Fleetd. To learn more, visit: [Self-managed agent updates](https://fleetdm.com/docs/deploying/fleetctl-agent-updates#self-managed-agent-updates).
## Custom transparency link
@@ -32,7 +27,7 @@ On the settings page, go to "Organization Settings" and select "Fleet Desktop."
For information on how to set the custom transparency link via a YAML configuration file, see the [configuration files](https://fleetdm.com/docs/configuration/fleet-server-configuration#fleet-desktop-settings) documentation.
-## Securing Fleet Desktop
+## Secure Fleet Desktop
Requests sent by Fleet Desktop and the web page that opens when clicking on the "My Device" tray item use a [Random (Version 4) UUID](https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4) token to uniquely identify each host.
@@ -57,7 +52,9 @@ As a consequence, Fleet Desktop will issue a new token if the current token is:
This change is imperceptible to users, as clicking on the "My device" tray item always uses a valid token. If a user visits an address with an expired token, they will get a message instructing them to click on the tray item again.
-
-
+
+
+
+
+
-
diff --git a/articles/fleet-in-your-calendar-introducing-maintenance-windows.md b/articles/fleet-in-your-calendar-introducing-maintenance-windows.md
index 534b993835..79ef934847 100644
--- a/articles/fleet-in-your-calendar-introducing-maintenance-windows.md
+++ b/articles/fleet-in-your-calendar-introducing-maintenance-windows.md
@@ -21,7 +21,7 @@ Fleet provides AI-generated explanations directly in the calendar events, detail
## _Maintenance windows_ include:
* **Personalized scheduling:** Updates are timed based on individual calendar events, so interventions happen when they are least intrusive.
-* **Automatic rescheduling:** If a scheduled update becomes impractical—due to changes in your calendar, for example—Fleet automatically finds a new appropriate time.
+* **Rescheduling flexibility:** If a scheduled update becomes impractical for any reason, users have the option to manually move the maintenance window to a more suitable time. We suggest rescheduling within one week to ensure timely updates.
* **Enhanced compliance:** With auto-scheduled maintenance windows, compliance with security protocols is maintained effortlessly, ensuring all devices are up to date without manual intervention.
_Maintenance windows_ is a direct response to common challenges faced in workplace productivity, particularly unplanned disruptions from essential updates. Fleet aims to support smoother, more efficient work environments by incorporating user feedback and addressing these long-standing issues.
diff --git a/docs/Using Fleet/Usage-statistics.md b/articles/fleet-usage-statistics.md
similarity index 88%
rename from docs/Using Fleet/Usage-statistics.md
rename to articles/fleet-usage-statistics.md
index 28413ee174..1db24222de 100644
--- a/docs/Using Fleet/Usage-statistics.md
+++ b/articles/fleet-usage-statistics.md
@@ -1,7 +1,9 @@
-# Usage statistics
+# Fleet usage statistics
Fleet Device Management Inc. periodically collects information about your instance.
+> To disable usage statistics, [see here](#disable-usage-statistics).
+
## What is included in usage statistics in Fleet?
Below is the JSON payload that is sent to Fleet Device Management Inc:
@@ -34,6 +36,11 @@ Below is the JSON payload that is sent to Fleet Device Management Inc:
"numHostSoftwareInstalledPaths": 999,
"numSoftwareCPEs": 999,
"numSoftwareCVEs": 999,
+ "numHostsNotResponding": 9,
+ "aiFeaturesDisabled": true,
+ "maintenanceWindowsEnabled": true,
+ "maintenanceWindowsConfigured": true,
+ "numHostsFleetDesktopEnabled": 999,
"hostsEnrolledByOperatingSystem": {
"darwin": [
{
@@ -101,8 +108,7 @@ Below is the JSON payload that is sent to Fleet Device Management Inc:
]
},
...
- ],
- "numHostsNotResponding": 9
+ ]
}
```
@@ -134,6 +140,9 @@ To disable usage statistics:
3. Uncheck the "Enable usage statistics" checkbox and then select "Update settings."
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/fleetctl-CLI.md b/articles/fleetctl.md
similarity index 92%
rename from docs/Using Fleet/fleetctl-CLI.md
rename to articles/fleetctl.md
index 32b4c6724c..caa234f845 100644
--- a/docs/Using Fleet/fleetctl-CLI.md
+++ b/articles/fleetctl.md
@@ -1,6 +1,6 @@
-# fleetctl CLI
+# fleetctl
-fleetctl (pronounced "Fleet control") is a CLI tool for managing Fleet from the command line. fleetctl enables a GitOps workflow with Fleet.
+fleetctl (pronounced "Fleet control") is a command line interface (CLI) tool for managing Fleet from the command line. fleetctl enables a GitOps workflow with Fleet.
fleetctl also provides a quick way to work with all the data exposed by Fleet without having to use the Fleet UI or work directly with the Fleet API.
@@ -32,6 +32,8 @@ npm install -g fleetctl@latest
Much of the functionality available in the Fleet UI is also available in `fleetctl`. You can run queries, add and remove users, generate Fleet's agent (fleetd) to add new hosts, get information about existing hosts, and more!
+> Note: Unless a logging infrastructure is configured on your Fleet server, osquery-related logs will be stored locally on each device. Read more [here](https://fleetdm.com/guides/log-destinations)
+
To see the available commands you can run:
```sh
@@ -197,6 +199,9 @@ This will generate a `tar.gz` file with:
- A file containing a set of all the errors that happened in the server during the interval of time defined by the [logging_error_retention_period](https://fleetdm.com/docs/deploying/configuration#logging-error-retention-period) configuration.
- Files containing database-specific information.
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/update-agents.md b/articles/fleetd-updates.md
similarity index 95%
rename from docs/Using Fleet/update-agents.md
rename to articles/fleetd-updates.md
index 93b61c0052..d5693eba93 100644
--- a/docs/Using Fleet/update-agents.md
+++ b/articles/fleetd-updates.md
@@ -1,7 +1,6 @@
-# Self-managed agent updates
+# Fleetd updates
-The fleetd agent will periodically check the public Fleet update repository and update Orbit, Fleet Desktop, and/or osquery
-if it detects a later version.
+The fleetd agent will periodically check the public Fleet update repository and update Orbit, Fleet Desktop, and/or osquery if it detects a later version.
To override this behavior, users can set a channel for each component or disable updates altogether. Visit [Adding Hosts](https://fleetdm.com/docs/using-fleet/adding-hosts#fleet-desktop) to learn more.
Alternatively, users with a Fleet Premium subscription can self-manage an update server.
@@ -160,6 +159,9 @@ fleetctl updates rotate targets
After the key(s) have been rotated, publish the repository in the same fashion as any other update.
-
+
+
+
+
+
-
diff --git a/articles/install-vpp-apps-on-macos-using-fleet.md b/articles/install-vpp-apps-on-macos-using-fleet.md
new file mode 100644
index 0000000000..d5f0da0137
--- /dev/null
+++ b/articles/install-vpp-apps-on-macos-using-fleet.md
@@ -0,0 +1,114 @@
+# Install App Store apps (VPP) on macOS, iOS, and iPadOS using Fleet
+
+
+
+
+Fleet Premium supports the ability to add Apple App Store applications to your software library using the Volume Purchasing Program (VPP) and then install those apps on macOS, iOS, or iPadOS hosts. This guide will walk you through using this feature to add apps from your Apple Business Manager account to Fleet and install those apps on your hosts.
+
+The Volume Purchasing Program is an Apple initiative that allows organizations to purchase and distribute apps and books in bulk. This program is particularly beneficial for organizations that need to deploy multiple apps to many devices. Key benefits of VPP include:
+* **Bulk purchasing**: Purchase multiple licenses for an app in one transaction, often with volume discounts.
+* **Centralized management**: Manage and distribute purchased apps from a central location.
+* **Licensing flexibility**: Reassign app licenses as needed, ensuring efficient use of resources.
+* **Streamlined deployment**: Use Fleet to automate the installation and configuration of purchased apps on enrolled devices.
+* **Self-Service (macOS only)**: Allow users to assign licenses to their own devices as needed.
+
+By integrating VPP with Fleet, organizations can seamlessly add apps to their software library and deploy them across macOS, iOS, and iPadOS hosts, ensuring that all devices have the necessary applications installed efficiently and effectively.
+
+## Prerequisites
+* **MDM features**: to use the VPP integration, you must first enable MDM features in Fleet. See the [MDM setup guide](https://fleetdm.com/docs/using-fleet/mdm-setup) for instructions on enabling MDM features.
+* **Teams**: Apps can only be added to a specific Team. You can manage teams by selecting your avatar in the top navigation and then **Settings > Teams**. (Note: Apps can also be added to the 'No Team' team, which contains hosts not assigned to any other team.) You can control which team uses which VPP token by assigning teams to the VPP token. Each token may have multiple teams assigned to it, but each team may be assigned to only 1 token.
+
+> As of Fleet 4.55.0, there is a [known issue](https://github.com/fleetdm/fleet/issues/20686) that uninstalled or deleted VPP apps will continue to show a status of `installed`.
+
+## Accessing the VPP configuration
+
+1. **Navigate to the MDM integration settings page**: Click your avatar on the far right of the main navigation menu, and then **Settings > Integrations > "Mobile device management (MDM)"**
+
+2. **Add your VPP token**: Scroll to the "Volume Purchasing Program (VPP)" section. Click "Add VPP", and then click "Add VPP" again on the following page. Follow the directions on the modal to get your VPP token from Apple Business Manager, and then click the "Upload" button at the bottom to upload it to Fleet.
+
+3. **Edit the team assignment for the new token**: Find the token in the table of VPP tokens. Click the "Actions" dropdown, and then click "Edit teams". Use the picker to select which team(s) this VPP token should be assigned to.
+
+## Purchasing apps
+
+To add apps to Fleet, you must first purchase them through Apple Business Manager, even if they are free. This ensures that all apps are appropriately licensed and available for distribution via the Volume Purchasing Program (VPP). For detailed instructions on selecting and buying content, please refer to Apple’s documentation on [purchasing apps through Apple Business Manager](https://support.apple.com/guide/apple-business-manager/select-and-buy-content-axmc21817890/web).
+
+## Add an app to Fleet
+
+1. **Navigate to the Software page**: Click on the "Software" tab in the main navigation menu.
+
+2. **Select your team**: Click on the "All teams" dropdown in the top left of the page and select your desired team.
+
+3. **Open the "Add software" modal**: Click on the "Add software" button in the top right of the page.
+
+4. **View your available apps**: Click on the "App Store (VPP)" tab in the "Add software" modal. The modal will list the apps that you have purchased through VPP but still need to add to Fleet.
+
+5. **Add an app**: Select an app from the list. You may optionally check the "Self-Service" box at the bottom left of the modal if you wish for the software to be available for user-initiated installs. Finally, click the "Add software" button in the bottom right of the modal. The app should appear in the software list for the selected team.
+
+## Remove an app from Fleet
+
+1. **Navigate to the Software page**: Click "Software" in the main navigation menu.
+
+2. **Find the app you want to remove**: Search for the app using the search bar in the top right corner of the table.
+
+3. **Access the app's details page**: Click on the app's name in the table.
+
+4. **Remove the app**: Click on the "Actions" dropdown on the right side of the page. Click "Delete," then click "Delete" on the confirmation modal. Deleting an app will not uninstall the app from the hosts on which it was previously installed.
+
+## Installing apps on macOS, iOS, and iPadOS hosts
+
+1. **Add the host to the relevant team.**
+
+2. **Go to the host's detail page**: Click the "Hosts" tab in the main navigation menu. Filter the hosts by the team, and click the host's name to see its details page.
+
+3. **Find the app**: Click the "Software" tab on the host details page. Search for the software you added in the software table's search bar. Instead of searching, you can also filter software by clicking the **All software** dropdown and selecting **Available for install.**
+
+4. **Install the app**: Click the "Actions" dropdown on the far right of the app's entry in the
+ table. Click "Install" to trigger an install. This action will send an MDM command to the host
+ instructing it to install the app. If the host is offline, the upcoming install will show up in
+ the **Details** -> **Activity** -> **Upcoming** tab of this page. After the app is installed and
+ the host details are refetched, the app will show up as **Installed** in the **Software** tab.
+
+## Installing apps on macOS using self-service
+
+1. **Open Fleet from the host**: On the host that will be installing an application through self-service, click on the Fleet Desktop tray icon, then click **My Device**. This will open the browser to the device's page on Fleet.
+
+2. **Navigate to the self-service tab**: Click on the **Self-Service** tab under the device's details.
+
+3. **Locate the app and click install**: Scroll through the list of software to find the app you would like to install, then click the **Install** button underneath it.
+
+## Renewing an expired or expiring VPP token
+
+When one of your uploaded VPP tokens has expired or is within 30 days of expiring, you will see a warning
+banner at the top of page reminding you to renew your token. You can do this with the following steps:
+
+1. **Navigate to the MDM integration settings page**: Click your avatar on the far right of the main navigation menu, and then **Settings > Integrations > "Mobile device management (MDM)"** Scroll to the "Volume Purchasing Program (VPP)" section, and click "Edit".
+
+2. **Renew the token**: Find the VPP token that you want to renew in the table. Token status is indicated in the "Renew date" column: tokens less than 30 days from expiring will have a yellow indicator, and expired tokens will have a red indicator. Click the "Actions" dropdown for the token and then click "Renew". Follow the instructions in the modal to download a new token from Apple Business Manager and then upload the new token to Fleet.
+
+## Deleting a VPP token
+
+To remove VPP tokens from Fleet:
+
+1. **Navigate to the MDM integration settings page**: Click your avatar on the far right of the main navigation menu, and then **Settings > Integrations > "Mobile device management (MDM)"** Scroll to the "Volume Purchasing Program (VPP)" section, and click "Edit".
+
+2. **Delete the token**: Find the VPP token that you want to delete in the table. Click the "Actions" dropdown for that token, and then click "Delete". Click "Delete" in the confirmation modal to finish deleting the token.
+
+## Managing apps with GitOps
+
+To manage App Store apps using Fleet's best practice GitOps, check out the `software` key in the GitOps reference documentation [here](https://fleetdm.com/docs/using-fleet/gitops#software).
+
+## REST API
+
+Fleet also provides a REST API for managing apps programmatically. You can add, install, and delete apps via this API and manage your organization’s VPP tokens. Learn more about Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api).
+
+## Conclusion
+
+This feature extends Fleet's capabilities for managing macOS, iOS, and iPadOS hosts. Whether you manage your hosts' software via uploaded installers or via the App Store VPP integration, Fleet provides you with the tools you need to manage your hosts effectively.
+
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/Log-destinations.md b/articles/log-destinations.md
similarity index 92%
rename from docs/Using Fleet/Log-destinations.md
rename to articles/log-destinations.md
index fb153f2589..ddec0c9b0c 100644
--- a/docs/Using Fleet/Log-destinations.md
+++ b/articles/log-destinations.md
@@ -1,19 +1,5 @@
# Log destinations
-- [Log destinations](#log-destinations)
- - [Amazon Kinesis Data Firehose](#amazon-kinesis-data-firehose)
- - [Snowflake](#snowflake)
- - [Splunk](#splunk)
- - [Amazon Kinesis Data Streams](#amazon-kinesis-data-streams)
- - [AWS Lambda](#aws-lambda)
- - [Google Cloud Pub/Sub](#google-cloud-pubsub)
- - [Apache Kafka](#apache-kafka)
- - [Stdout](#stdout)
- - [Filesystem](#filesystem)
- - [Sending logs outside of Fleet](#sending-logs-outside-of-fleet)
-
-This document provides a list of the supported log destinations in Fleet.
-
Log destinations can be used in Fleet to log:
- Osquery [status logs](https://osquery.readthedocs.io/en/stable/deployment/logging/#status-logs).
@@ -23,11 +9,27 @@ Log destinations can be used in Fleet to log:
To configure each log destination, you must set the correct logging configuration options in Fleet.
+
Check out the reference documentation for:
- [Osquery status logging configuration options](https://fleetdm.com/docs/deploying/configuration#osquery-status-log-plugin).
- [Osquery result logging configuration options](https://fleetdm.com/docs/deploying/configuration#osquery-result-log-plugin).
- [Activity audit logging configuration options](https://fleetdm.com/docs/deploying/configuration#activity_audit_log_plugin).
+This guide provides a list of the supported log destinations in Fleet.
+
+### In this guide:
+
+- [Amazon Kinesis Data Firehose](#amazon-kinesis-data-firehose)
+- [Snowflake](#snowflake)
+- [Splunk](#splunk)
+- [Amazon Kinesis Data Streams](#amazon-kinesis-data-streams)
+- [AWS Lambda](#aws-lambda)
+- [Google Cloud Pub/Sub](#google-cloud-pubsub)
+- [Apache Kafka](#apache-kafka)
+- [Stdout](#stdout)
+- [Filesystem](#filesystem)
+- [Sending logs outside of Fleet](#sending-logs-outside-of-fleet)
+
## Amazon Kinesis Data Firehose
Logs are written to [Amazon Kinesis Data Firehose (Firehose)](https://aws.amazon.com/kinesis/data-firehose/).
@@ -145,6 +147,9 @@ See the [osquery logging documentation](https://osquery.readthedocs.io/en/stable
If `--logger_plugin=tls` is used with osquery clients, the following configuration can be applied on the Fleet server for handling the incoming logs.
-
+
+
+
+
+
-
diff --git a/articles/macos-mdm-setup.md b/articles/macos-mdm-setup.md
new file mode 100644
index 0000000000..bc91ee6a72
--- /dev/null
+++ b/articles/macos-mdm-setup.md
@@ -0,0 +1,65 @@
+# macOS MDM setup
+
+To turn on macOS, iOS, and iPadOS MDM features, follow the instructions on this page to connect Fleet to Apple Push Notification service (APNs).
+
+To use automatic enrollment (aka zero-touch) features on macOS, iOS, and iPadOS, follow instructions to connect Fleet with Apple Business Manager (ABM).
+
+To turn on Windows MDM features, head to this [Windows MDM setup article](https://fleetdm.com/guides/windows-mdm-setup).
+
+## Apple Push Notification service (APNs)
+
+Apple uses APNs to authenticate and manage interactions between Fleet and hosts.
+
+To connect Fleet to APNs or renew APNs, head to the **Settings > Integrations > Mobile device management (MDM)** page.
+
+> Apple requires that APNs certificates are renewed annually.
+> - If your certificate expires, you will have to turn MDM off and back on for all macOS hosts.
+> - Be sure to use the same Apple ID from year-to-year. If you don't, you will have to turn MDM off and back on for all macOS hosts.
+
+## Apple Business Manager (ABM)
+
+> Available in Fleet Premium
+
+To connect Fleet to ABM, you have to add an ABM token to Fleet. To add an ABM token:
+
+1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page.
+2. Under "Automatic enrollment", click "Add ABM", and then click "Add ABM" again on the next page. Follow the instructions in the modal and upload an ABM token to Fleet.
+
+When one of your uploaded ABM tokens has expired or is within 30 days of expiring, you will see a warning
+banner at the top of page reminding you to renew your token.
+
+To renew an ABM token:
+
+1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page.
+2. Under "Automatic enrollment", click "Edit", and then find the token that you want to renew. Token status is indicated in the "Renew date" column: tokens less than 30 days from expiring will have a yellow indicator, and expired tokens will have a red indicator. Click the "Actions" dropdown for the token and then click "Renew". Follow the instructions in the modal to download a new token from Apple Business Manager and then upload the new token to Fleet.
+
+After connecting Fleet to ABM, set Fleet to be the MDM for all Macs:
+
+1. Log in to [Apple Business Manager](https://business.apple.com)
+2. Click your profile icon in the bottom left
+3. Click **Preferences**
+4. Click **MDM Server Assignment** and click **Edit** next to **Default Server Assignment**.
+5. Switch **Mac**, **iPhone**, and **iPad** to Fleet.
+
+macOS, iOS, and iPadOS hosts listed in ABM and associated to a Fleet instance with MDM enabled will sync to Fleet and appear in the Hosts view with the **MDM status** label set to "Pending".
+
+Hosts that automatically enroll will be assigned to a default team. You can configure the default team for macOS, iOS, and iPadOS hosts by:
+
+1. Navigating to the **Settings > Integrations > Mobile device management (MDM)** page and clicking "Edit" under "Automatic enrollment".
+2. Click on the "Actions" dropdown for the ABM token you want to update, and then click "Edit teams".
+3. Use the dropdowns in the modal to select the default team for each type of host, and click "Save" to save your selections.
+
+If no default team is set for a host platform (macOS, iOS, or iPadOS), then newly enrolled hosts of that platform will be placed in "No team".
+
+> A host can be transferred to a new (not default) team before it enrolls. In the Fleet UI, you can do this under **Settings** > **Teams**.
+
+### Simple Certificate Enrollment Protocol (SCEP)
+
+Fleet uses SCEP certificates (1 year expiry) to authenticate the requests hosts make to Fleet. Fleet renews each host's SCEP certificates automatically every 180 days.
+
+
+
+
+
+
+
diff --git a/docs/Using Fleet/MDM-macOS-setup-experience.md b/articles/macos-setup-experience.md
similarity index 91%
rename from docs/Using Fleet/MDM-macOS-setup-experience.md
rename to articles/macos-setup-experience.md
index 31dac0a131..6d7f9cc2ef 100644
--- a/docs/Using Fleet/MDM-macOS-setup-experience.md
+++ b/articles/macos-setup-experience.md
@@ -2,7 +2,7 @@
_Available in Fleet Premium_
-In Fleet, you can customize the out-of-the-box macOS setup experience for your end users:
+In Fleet, you can customize the out-of-the-box macOS Setup Assistant with Remote Management and Automated Device Enrollment (ADE) for end users:
* Require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new Mac.
@@ -12,7 +12,7 @@ In Fleet, you can customize the out-of-the-box macOS setup experience for your e
In addition to the customization above, Fleet automatically installs the fleetd agent during out-of-the-box macOS setup. This agent is responsible for reporting host vitals to Fleet and presenting Fleet Desktop to the end user.
-macOS setup features require connecting Fleet to Apple Business Manager (ABM). Learn how [here](./mdm-setup.md#apple-business-manager-abm).
+macOS setup features require connecting Fleet to Apple Business Manager (ABM). Learn how [here](https://fleetdm.com/guides/macos-mdm-setup#apple-business-manager-abm).
## End user authentication and EULA
@@ -20,7 +20,7 @@ Using Fleet, you can require end users to authenticate with your identity provid
### End user authentication
-To require end user authentication, first [configure single sign-on (SSO)](../Deploy/single-sign-on-sso.md). Next, enable end user authentication by heading to to **Controls > Setup experience End user authentication** or use [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
+To require end user authentication, first [configure single sign-on (SSO)](https://fleetdm.com/docs/deploy/single-sign-on-sso). Next, enable end user authentication by heading to to **Controls > Setup experience End user authentication** or use [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
If you've already configured SSO in Fleet, create a new SAML app in your IdP. In your new app, use `https:///api/v1/fleet/mdm/sso/callback` for the SSO URL.
@@ -155,13 +155,15 @@ Testing requires a test Mac that is present in your Apple Business Manager (ABM)
2. In Fleet, navigate to the Hosts page and find your Mac. Make sure that the host's **MDM status** is set to "Pending."
- > New Macs purchased through Apple Business Manager appear in Fleet with MDM status set to "Pending." Learn more about these hosts [here](./mdm-setup.md#pending-hosts).
+ > New Macs purchased through Apple Business Manager appear in Fleet with MDM status set to "Pending." Learn more about these hosts [here](https://fleetdm.com/guides/macos-mdm-setup#apple-business-manager-abm).
3. Transfer this host to the "Workstations (canary)" team by selecting the checkbox to the left of the host and selecting **Transfer** at the top of the table. In the modal, choose the Workstations (canary) team and select **Transfer**.
4. Boot up your test Mac and complete the custom out-of-the-box setup experience.
-
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/MDM-commands.md b/articles/mdm-commands.md
similarity index 70%
rename from docs/Using Fleet/MDM-commands.md
rename to articles/mdm-commands.md
index c541c7799d..b1e5918c5e 100644
--- a/docs/Using Fleet/MDM-commands.md
+++ b/articles/mdm-commands.md
@@ -1,4 +1,4 @@
-# Commands
+# MDM commands
In Fleet you can run MDM commands to take action on your macOS, iOS, iPadOS, and Windows hosts, like restarting the host, remotely.
@@ -83,19 +83,11 @@ You can view a list of the 1,000 latest commands:
1. Run `fleetctl get mdm-commands`
2. View the list of latest commands, most recent first, along with the timestamp, targeted hostname, command type, execution status and command ID.
-The command ID can be used to view command results as documented in [step 4 of the previous section](#step-4-view-the-commands-results).
+The command ID can be used to view command results as documented in [step 4 of the previous section](#step-4-view-the-commands-results).
-The possible statuses for macOS, iOS, and iPadOS hosts are the following:
-
-* Pending: the command has yet to run on the host. The host will run the command the next time it comes online.
-* NotNow: the host responded with "NotNow" status via the MDM protocol: the host received the command, but couldn’t execute it. The host will try to run the command the next time it comes online.
-* Acknowledged: the host responded with "Acknowledged" status via the MDM protocol: the host processed the command successfully.
-* Error: the host responded with "Error" status via the MDM protocol: an error occurred. Run the `fleetctl get mdm-command-results --id=
-
+
+
+
+
+
-
diff --git a/articles/mdm-migration.md b/articles/mdm-migration.md
new file mode 100644
index 0000000000..b28f6febd0
--- /dev/null
+++ b/articles/mdm-migration.md
@@ -0,0 +1,123 @@
+# MDM migration
+
+This guide provides instructions for migrating devices from your current MDM solution to Fleet.
+
+> For seamless MDM migration, [view this guide](https://fleetdm.com/guides/seamless-mdm-migration).
+
+## Requirements
+
+- A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet)
+- Fleet is connected to Apple Push Notification service (APNs) and Apple Business Manager (ABM). [See macOS MDM setup](https://fleetdm.com/guides/macos-mdm-setup)
+
+## Migrate hosts
+
+To migrate hosts, we will do the following steps:
+
+1. Enroll hosts to Fleet
+2. Assign hosts in Apple Business Manager (ABM) to Fleet
+3. Choose migration workflow and migrate hosts
+
+### Step 1: enroll hosts to Fleet
+
+1. First, enroll your hosts to Fleet by installing Fleet's agent (fleetd). Learn how [here](https://fleetdm.com/guides/enroll-hosts).
+2. Ensure your end users have access to an admin account on their Mac. End users won't be able to migrate on their own if they have a standard account.
+
+### Step 2: assign hosts in Apple Business Manager (ABM) to Fleet
+
+1. In ABM, unassign your hosts from your current MDM solution by selecting **Devices** and then selecting **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Unassign from the current MDM**, and select **Continue**.
+
+2. Assign these hosts to Fleet: select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Assign to the following MDM:**, select your Fleet server in the dropdown, and select **Continue**.
+
+### Step 3: choose migration workflow and migrate hosts
+
+There are two migration workflows in Fleet: default and end user.
+
+The default migration workflow requires that the IT admin unenrolls hosts from the old MDM solution before the end user can complete migration. This will result in a gap in MDM coverage until the end user completes migration.
+
+The end user migration workflow allows the user to kick off migration by unenrolling from the old MDM solution on their own. Once the user is unenrolled, they're prompted to turn on MDM features in Fleet, reducing the gap in MDM coverage.
+
+#### Default workflow
+
+End user experience:
+
+- After a host is unenrolled from your current MDM solution, the end user will be prompted with Apple's **Remote Management** full-screen popup if the host is assigned to Fleet in ABM.
+
+- If the host is not assigned to Fleet in ABM (manual enrollment), the end user will be given the option to download the MDM enrollment profile on their **My device page**.
+
+
+
+Configuration:
+
+- To kick off the default workflow, unenroll the hosts to be migrated in your current MDM solution. MacOS does not allow a host to be connected to multiple MDM solutions at once.
+
+#### End user workflow
+
+> Available in Fleet Premium
+
+End user experience:
+
+- To watch an animation of the end user experience during the migration workflow, head to **Settings > Integrations > Mobile device management (MDM)** in the Fleet UI, and scroll down to the **End user migration workflow** section.
+
+Configuration:
+
+- In Fleet, you can configure the end user workflow using the Fleet UI, Fleet API, or Fleet's GitOps workflow.
+
+- After configuring the end user workflow, instruct your end users to select the Fleet icon in their menu bar, select **Migrate to Fleet** and follow the on-screen instructions to migrate to Fleet.
+
+- Fleet UI:
+1. Select the avatar on the right side of the top navigation and select **Settings > Integrations > Mobile device management (MDM)**.
+2. Scroll down to the **End user migration workflow** section and select the toggle to enable the workflow.
+3. Under **Mode**, choose a mode, enter the webhook URL for your automation tool (e.g., Tines) under **Webhook URL**, and select **Save**.
+4. During the end user migration workflow, an end user's device will have its selected system theme (light or dark) applied. If your logo is not easy to see on both light and dark backgrounds, you can optionally set a logo for each theme:
+Head to **Settings** > **Organization settings** > **Organization info**, add URLs to your logos in the **Organization avatar URL (for dark backgrounds)** and **Organization avatar URL (for light backgrounds)** fields, and select **Save**.
+- Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#mdm-macos-migration)
+- GitOps:
+ - To manage macOS MDM migration configuration using Fleet's best practice GitOps, check out the `macos_migration` key in the [GitOps reference documentation](https://fleetdm.com/docs/configuration/yaml-files#macos-migration).
+ - To manage your organization's logo for dark and light backgrounds using Fleet's best practice GitOps, check out the `org_info` key in the [GitOps reference documentation](https://fleetdm.com/docs/configuration/yaml-files#org-info).
+
+## Check migration progress
+
+To see a report of which hosts have successfully migrated to Fleet, have MDM features off, or are still enrolled to your old MDM solution head to the **Dashboard** page by clicking the icon on the left side of the top navigation bar.
+
+Then, scroll down to the **Mobile device management (MDM)** section of the Dashboard. You'll see a breakdown of which hosts have successfully migrated to Fleet, which have MDM features disabled, and which are still enrolled in the previous MDM solution.
+
+## FileVault recovery keys
+
+_Available in Fleet Premium_
+
+When migrating from a previous MDM, end users must restart or log out of their device to escrow FileVault keys to Fleet. The **My device** page in Fleet Desktop will present users with instructions on how to reset their key.
+
+To start, enforce FileVault disk encryption and escrow recovery keys in Fleet. Learn how [here](https://fleetdm.com/guides/enforce-disk-encryption).
+
+After turning on disk encryption in Fleet, share [these guided instructions](#how-to-turn-on-disk-encryption) with your end users.
+
+### How to turn on disk encryption
+
+1. Select the Fleet icon in your menu bar and select **My device**.
+
+
+
+2. On your **My device** page, follow the disk encryption instructions in the yellow banner.
+ - If you don’t see the yellow banner, select the purple **Refetch** button at the top of the page.
+ - If you still don't see the yellow banner after a couple minutes or if the **My device** page presents you with an error, please contact your IT administrator.
+
+
+
+## Activation Lock
+
+In Fleet, the [Activation Lock](https://support.apple.com/en-us/HT208987) feature is disabled by default for automatically enrolled (ADE) hosts.
+
+In 2024, Apple added the ability to manage activation lock in Apple Business Manager (ABM). For devices that are owned by the business and available in ABM, you can [turn off activation lock remotely](https://support.apple.com/en-ca/guide/apple-business-manager/axm812df1dd8/web).
+
+If a device is not available in ABM and has Activation Lock enabled, we recommend asking the end user to follow these instructions to disable Activation Lock before migrating the device to Fleet: https://support.apple.com/en-us/HT208987.
+
+If the Activation Lock is enabled, you will need the Activation Lock bypass code to wipe and reuse the Mac successfully.
+
+However, Activation Lock bypass codes can only be retrieved from the Mac up to 30 days after the device is enrolled. This means that when migrating from your old MDM solution, it’s likely that you’ll be unable to retrieve the Activation Lock bypass code.
+
+
+
+
+
+
+
diff --git a/articles/osquery-evented-tables-overview.md b/articles/osquery-evented-tables-overview.md
index 883f0bc8ab..f1316a85f1 100644
--- a/articles/osquery-evented-tables-overview.md
+++ b/articles/osquery-evented-tables-overview.md
@@ -121,7 +121,7 @@ On macOS, there are two utilities that enable osquery process auditing: [OpenBSM
To use the `es_process_events` tables, use the flag `--disable_endpointsecurity=false`. See the [EndpointSecurity instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-endpointsecurity) for more information. To use `process_events` and `socket_events` with OpenBSM, see the [OpenBSM instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-openbsm).
#### Windows
-Currently, osquery does not support process auditing for Windows. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to build process auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/7732).
+Fleet supports auditing process events on Windows via the `process_etw_events` table. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to add file auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/20946).
### YARA scanning
[YARA](https://virustotal.github.io/yara/) is a malware research and detection tool available on Linux and macOS that allows users to create descriptions of malware families based on patterns of text or binary code. Each potential piece of malware is matched against a YARA rule and triggers if the specified conditions are met.
diff --git a/docs/Using Fleet/Osquery-process.md b/articles/osquery-watchdog.md
similarity index 90%
rename from docs/Using Fleet/Osquery-process.md
rename to articles/osquery-watchdog.md
index 68efcd81da..5fb0bd980b 100644
--- a/docs/Using Fleet/Osquery-process.md
+++ b/articles/osquery-watchdog.md
@@ -1,4 +1,4 @@
-# Osquery children processes
+# Osquery watchdog
Osquery will run a watcher process to keep track of any child process and any managed extensions. What follows is a description of what happens during the watcher REPL and under what circumstances the child process and/or managed extensions are terminated.
@@ -25,6 +25,9 @@ If the managed extension is `Non-existent` (either because it was `Non-existent`
Lastly, we check the state of the watcher process itself. If it is deemed unhealthy because of resource contention, then the osquery process is shut down.
-
+
+
+
+
+
-
\ No newline at end of file
diff --git a/docs/Using Fleet/Puppet-module.md b/articles/puppet-module.md
similarity index 96%
rename from docs/Using Fleet/Puppet-module.md
rename to articles/puppet-module.md
index 30db545834..bf6a442bc1 100644
--- a/docs/Using Fleet/Puppet-module.md
+++ b/articles/puppet-module.md
@@ -151,7 +151,9 @@ if $err != '' {
The above example includes the XML payload for the `EnableRemoteDesktop` MDM command. Learn more about creating the payload for other custom commands [here](./MDM-commands.md).
-
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/Fleet-UI.md b/articles/queries.md
similarity index 73%
rename from docs/Using Fleet/Fleet-UI.md
rename to articles/queries.md
index 3ccd10d5e4..0379bca004 100644
--- a/docs/Using Fleet/Fleet-UI.md
+++ b/articles/queries.md
@@ -1,19 +1,25 @@
-# Fleet UI
-- [Creating a query](#create-a-query)
-- [Running a query](#run-a-query)
-- [Scheduling a query](#schedule-a-query)
-- [Update agent options](#update-agent-options)
+# Queries
+
+Queries in Fleet allow you to ask questions to help you manage, monitor, and identify threats on your devices. This guide will walk you through how to create, schedule, and run a query.
+
+> Note: Unless a logging infrastructure is configured on your Fleet server, osquery-related logs will be stored locally on each device. Read more [here](https://fleetdm.com/guides/log-destinations)
+
+> New users may find it helpful to start with Fleet's policies. You can find policies and queries from the community in Fleet's [query library](https://fleetdm.com/queries). To learn more about policies, see [What are Fleet policies?](https://fleetdm.com/securing/what-are-fleet-policies) and [Understanding the intricacies of Fleet policies](https://fleetdm.com/guides/understanding-the-intricacies-of-fleet-policies).
+
+### In this guide:
+
+- [Create a query](#create-a-query)
+- [Run a query](#run-a-query)
+- [Schedule a query](#schedule-a-query)
+
+
## Create a query
-Queries in Fleet allow you to ask a multitude of questions to help you manage, monitor, and identify threats on your devices.
-
-If you're unsure of what to ask, head to Fleet's [query library](https://fleetdm.com/queries). There you'll find common queries that have been tested by members of our community.
-
How to create a query:
1. In the top navigation, select **Queries**.
@@ -63,16 +69,10 @@ By default, queries that run on a schedule will only target platforms compatible
> Note: When viewing a specific [team](https://fleetdm.com/docs/using-fleet/segment-hosts) in Fleet Premium, only queries that belong to the selected team will be listed. When configuring query automations for all hosts, only global queries will be listed.
-## Update agent options
-
-
-
-> This content was relocated on 31st August 2023.
-
-See "[Agent configuration](https://fleetdm.com/docs/configuration/agent-configuration)" to learn how to simultaneously update agent options from the Fleet UI or fleetctl command line tool.
-
-
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/manage-access.md b/articles/role-based-access.md
similarity index 97%
rename from docs/Using Fleet/manage-access.md
rename to articles/role-based-access.md
index b472b33539..95fc712c52 100644
--- a/docs/Using Fleet/manage-access.md
+++ b/articles/role-based-access.md
@@ -1,4 +1,4 @@
-# Manage access
+# Role-based access
Users have different abilities depending on the access level they have.
@@ -83,7 +83,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
| View Apple business manager (BM) information | | | | ✅ | |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | |
| View disk encryption key for macOS and Windows hosts | ✅ | ✅ | ✅ | ✅ | |
-| Edit OS updates for macOS and Windows hosts | | | ✅ | ✅ | ✅ |
+| Edit OS updates for macOS, Windows, iOS, and iPadOS hosts | | | ✅ | ✅ | ✅ |
| Create, edit, resend and delete configuration profiles for macOS and Windows hosts | | | ✅ | ✅ | ✅ |
| Execute MDM commands on macOS and Windows hosts\** | | | ✅ | ✅ | |
| View results of MDM commands executed on macOS and Windows hosts\** | ✅ | ✅ | ✅ | ✅ | |
@@ -154,7 +154,7 @@ Users with access to multiple teams can be assigned different roles for each tea
| Edit agent options | | | | ✅ | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ | |
| View disk encryption key for macOS hosts | ✅ | ✅ | ✅ | ✅ | |
-| Edit OS updates for macOS and Windows hosts | | | ✅ | ✅ | ✅ |
+| Edit OS updates for macOS, Windows, iOS, and iPadOS hosts | | | ✅ | ✅ | ✅ |
| Create, edit, resend and delete configuration profiles for macOS and Windows hosts | | | ✅ | ✅ | ✅ |
| Execute MDM commands on macOS and Windows hosts* | | | ✅ | ✅ | |
| View results of MDM commands executed on macOS and Windows hosts* | ✅ | ✅ | ✅ | ✅ | |
@@ -175,6 +175,9 @@ Users with access to multiple teams can be assigned different roles for each tea
\** Team-level users only see global query results for hosts on teams where they have access.
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/Scripts.md b/articles/scripts.md
similarity index 88%
rename from docs/Using Fleet/Scripts.md
rename to articles/scripts.md
index 7adb2c057d..754fdf4da9 100644
--- a/docs/Using Fleet/Scripts.md
+++ b/articles/scripts.md
@@ -19,7 +19,7 @@ If you don't use MDM features, to enable scripts, we'll deploy a fleetd agent wi
2. Deploy fleetd to your hosts. If your hosts already have fleetd installed, you can deploy the new fleetd on-top of the old installation.
-Learn more about generating a fleetd agent and deploying it [here](./enroll-hosts.md).
+Learn more about generating a fleetd agent and deploying it [here](https://fleetdm.com/guides/enroll-hosts).
## Execute a script
@@ -45,7 +45,9 @@ fleetctl CLI:
fleetctl run-script --script-path=/path/to/script --host=hostname
```
-
-
+
+
+
+
+
-
diff --git a/articles/seamless-mdm-migration.md b/articles/seamless-mdm-migration.md
new file mode 100644
index 0000000000..9abf3c516c
--- /dev/null
+++ b/articles/seamless-mdm-migration.md
@@ -0,0 +1,133 @@
+# Seamless MDM migrations to Fleet
+
+
+
+Migrating macOS devices between Mobile Device Management (MDM) solutions is often fraught with challenges, including potential gaps in device management, user disruption, and compliance issues. Traditional MDM migrations typically require end-user interaction and leave devices unmanaged for a period, leading to problems like Wi-Fi disconnections due to certificate profile removal and incomplete migrations. These challenges can force organizations to stay with outdated MDM solutions that no longer meet their needs. But there’s a better way.
+
+Seamless MDM migrations are now possible, allowing organizations to transition their macOS devices to Fleet without any downtime or end-user involvement. By leveraging Fleet, you can ensure that your devices remain fully managed and compliant throughout the migration process. This means no more gaps in management, no user disruptions, and a smoother path to a more modern and effective MDM solution.
+
+This guide will walk you through the entire process of migrating your MDM deployment to Fleet. You’ll start by understanding the specific requirements for a seamless migration, followed by configuring Fleet with the necessary certificates and database records. The guide will then take you through the process of installing Fleet’s agent (`fleetd`) on your devices, updating DNS records to redirect devices to the Fleet server, and finally, decommissioning your old MDM server.
+
+Throughout the guide, you’ll find practical advice and best practices to ensure a smooth transition with minimal risk. By the end, you’ll be equipped with the knowledge and tools to execute a seamless MDM migration to Fleet, ensuring that your organization’s devices are securely managed without the typical headaches associated with a traditional MDM switch.
+
+## Requirements
+
+Note: Deployments that do not meet these seamless migration requirements can still migrate with the [standard MDM migration process](https://fleetdm.com/docs/using-fleet/mdm-migration-guide).
+
+* Customer controls the DNS used in the MDM server enrollment (eg. devices are enrolled to `*.customerowneddomain.com`, not `*.mdmvendor.com`).
+* Customer has access to the Apple Push Notification Service (APNS) certificate/key and SCEP certificate/key, or access to the MDM server database to extract these values.
+
+These requirements are easily met in self-hosted open-source MDM solutions and may be met with commercial solutions when the customer is self-hosting or otherwise controls the DNS.
+
+Seamless migration may still be possible with control of DNS along with a copy of the original Certificate Signing Request (CSR) for the APNS certificate. If you are in this situation, please reach out to the Fleet team.
+
+### Why?
+
+Apple allows changing most values in profiles delivered by MDM, but the `ServerURL`, `CheckinURL`, and `PushTopic` cannot be changed without re-enrollment (and user actions). Control of DNS and the certificates allows the MDM to be swapped out without changing these.
+
+## High-level process
+
+1. Configure Fleet with the APNS & SCEP certificates/keys, path redirects, and SCEP renewal.
+2. Import database records letting Fleet know about the devices to be migrated.
+3. Configure controls (profiles, updates, etc.) in Fleet.
+4. Install `fleetd` on the devices (through the existing MDM).
+5. Update DNS records to point devices to the Fleet server.
+6. Decommission the old server.
+
+It is recommended to follow the entire process on a staging/test MDM instance and devices, then repeat for the production instance and devices.
+
+[](https://mermaid.live/edit#pako:eNpVUctuwjAQ_BVrT62URIaEvFRxqNKeSivBrZiDiTeJpdhGxqFQBN9eA23VXvY1o9lZ7RFqIxBKCMOQaSddjyV5xMZYJEq2ljtpNNNXtOnNR91x68jLnOntsPbwpiOK128LUuFO1sg0IUqoupeo3XJWzcitXDGNWjD9i5EwJHMzOBRkfSDV64I8rO2U3HlChHuuNj1GtVH3YTg1vfBTpm95-bSXWyd1Sy7qC7Q7tKu_wufzmTQ9orsY9mn5fIk_TAhAoVVcCn_z8WKXgetQIYPSlwIbPvSOAdMnT-WDM4uDrqF0dsAAho3gDivJ_eUKyob3Wz_dcP1uzL8eyiPsoRzTOBrHySim2SQtaBbAAco4S6NxTmmeZcUoLiZJfArg8ypAo5SOKC3iNM-LNMmTJAAU0hk7u32pNrqRrXdmzdB23xtPX3Gkloc)
+
+[](https://mermaid.live/edit#pako:eNpVUcFuwjAM_ZXIu2xSW7XQdaWakCYxTmOT4DayQ0jcNqJJUEgZDMG3L6Vs2g5JbL9n-9k5AjcCoYAwDKl20jVYkKfSoSVKVpY5aTTVF7BszCevmXXkZU71tl15eFMTxfjbgkxwJzlSTYgSijcStVvOJjPSmx9UoxZUm0Z4ePm8l1sndUU6xgLtDq1n_CaS8_lMeurfaBiSuWkdCrI6kMnrgjyu7JjcekKEe6Y2DUbcqLswHJcNousE-2c57e6fLhCAQquYFH7kYyeXgqtRIYXCm42sakch6AHB7Hrmt9NhJWu2eI2vGF9X1rR-okvWzXQ6pUD1yVdnrTOLg-ZQONtiAO1GMIcTyfyyFBR9Gdgw_W7MPx-KI-yhSPI8GgzTJE2T-GGU5UkABygGeRz5k8SDJL8fpGmcnQL4ulSIo8zH49Ewy_NRluZpGgAK6Yyd9R_LjS5l5aV5xVV9bXn6BriRpdY)
+
+### 1. Configure Fleet
+
+The Fleet server must be configured with the APNS & SCEP certificates/keys copied from the existing server. This is done via manual modification of the Fleet database and configurations. The Fleet team will perform this configuration on Fleet Cloud instances and can advise how to do it on self-hosted Fleet instances.
+
+In most cases, the paths (portion of the URL after the domain name) used in the enrollment profile `ServerURL`, `CheckInURL` and SCEP URL will differ from those used by Fleet. The Fleet Server load balancer must be configured to redirect the MDM client via HTTP 3xx redirects.
+
+[Apple's documentation](https://developer.apple.com/documentation/devicemanagement/implementing_device_management/sending_mdm_commands_to_a_device?language=objc) states:
+
+> MDM follows HTTP 3xx redirections without user interaction. However, it doesn’t save the URL given by HTTP 301 (Moved Permanently) redirections. Each transaction begins at the URL the MDM payload specifies.
+
+Therefore, redirects must remain as long as migrated devices are enrolled.
+
+For a typical MicroMDM to Fleet migration, the following redirects are used:
+
+| From (MicroMDM path) | To (Fleet path) |
+| -------------------- | --------------- |
+| /mdm/checkin | /mdm/apple/mdm |
+| /mdm/connect | /mdm/apple/mdm |
+| /scep | /mdm/apple/scep |
+
+SCEP certificate renewals need special handling for migrated devices. This is configured (by, or with guidance from the Fleet team) in the server using the [`FLEET_SILENT_MIGRATION_ENROLLMENT_PROFILE` environment variable](https://github.com/fleetdm/fleet/pull/20063). When configured, migrated devices receive an enrollment profile with matching keys when SCEP renewal comes due (migrated devices reject the typical profile Fleet sends because it includes the new server URL).
+
+### 2. Import database records
+
+The Fleet server is made aware of the devices that will be migrated by inserting records into the database. The Fleet team will perform this operation in Fleet Cloud and can advise for self-hosted instances.
+
+For MicroMDM, a [migration script](https://github.com/fleetdm/fleet/pull/18151) has been made that will generate the necessary SQL statements from the MicroMDM database.
+
+For other MDM solutions, please work with the Fleet team to generate the appropriate records.
+
+### 3. Configure controls
+
+Next, configure the controls that will be applied to migrated devices. Use the Teams features in Fleet Premium to apply different configurations to different devices.
+
+In particular,
+
+* [Configuration profiles](https://fleetdm.com/docs/using-fleet/mdm-custom-os-settings#custom-os-settings)
+* [OS updates](https://fleetdm.com/docs/using-fleet/mdm-os-updates)
+* [Disk encryption](https://fleetdm.com/docs/using-fleet/mdm-disk-encryption)
+
+When the device checks in after migration, Fleet will send the full set of configuration profiles configured for that device's team. Any profiles with identifiers matching existing profiles on the device will be updated in place.
+
+Fleet will not send commands to remove profiles that have not been configured in Fleet. Either remove these profiles before migration in the existing MDM before migration or use `fleetctl` or the Fleet API to send an MDM command to remove any undesired profiles.
+
+OS update configurations will apply automatically after the device is migrated.
+
+As of Fleet 4.55, disk encryption keys will automatically be re-escrowed after migration the next time the user logs into their device.
+
+### 4. Install `fleetd`
+
+Install `fleetd` on the devices to migrate. Devices with `fleetd` installed will begin to show up in the Fleet UI (with profiles in a "Pending" state).
+
+Generate `.pkg` packages following the [standard enrollment documentation](https://fleetdm.com/docs/using-fleet/enroll-hosts). Install the package using the existing MDM or any other management tool.
+
+Devices are automatically assigned to Teams in Fleet based on the package they are provided, so be sure to distribute packages that assign devices to teams with the relevant configurations.
+
+### 5. Update DNS
+
+Devices are now communicating with the Fleet server via the `fleetd` agent. They have not yet migrated MDM servers.
+
+Ensure the Fleet server load balancer can terminate HTTPS using the existing server hostname. This typically involves issuing a certificate [with AWS ACM](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html). In Fleet Cloud, the Fleet team will ask the customer team to update a DNS record for verification so that AWS can issue the certificate.
+
+Now the customer updates DNS to point the existing domain to the Fleet server load balancer. This typically involves setting a `CNAME` record with the hostname of the load balancer (eg. `mdm.example.com -> fleet-cloud-alb-1723349272.us-east-2.elb.amazonaws.com`).
+
+Devices will begin checking in with the Fleet server and receiving new configurations.
+
+### 6. Decommission the old server
+
+At this point, the migration is complete. The old server can be decommissioned.
+
+Keep a database backup of the old server on hand in case it is ever needed for reference or recovery.
+
+## Gradual migration
+
+In the process described, when we update DNS all of the devices are migrated immediately. To minimize risk, it is often desired to gradually migrate devices.
+
+Fleet has created a [migration proxy](https://github.com/fleetdm/fleet/tree/main/tools/mdm/migration/mdmproxy) that can be used to gradually migrate specific devices and/or a percentage of devices. This allows a staged migration with progressively more devices migrated.
+
+## Conclusion
+
+Seamless MDM migrations on macOS are not just possible but are a significant step forward in maintaining a secure and compliant environment without disrupting end users. By following this guide, you can transition from your existing MDM solution to Fleet smoothly, keeping your devices managed and secure throughout the process. If you encounter any challenges, the Fleet team is ready to assist you, ensuring your migration is successful.
+
+For organizations ready to take control of their MDM strategy, this seamless migration process is an opportunity to upgrade to a modern, flexible, and secure management solution. We encourage you to reach out for support or further explore the robust features Fleet offers to enhance your device management capabilities.
+
+
+
+
+
+
+
+
diff --git a/articles/software-self-service.md b/articles/software-self-service.md
new file mode 100644
index 0000000000..f82a27f8c8
--- /dev/null
+++ b/articles/software-self-service.md
@@ -0,0 +1,80 @@
+# Software self-service
+
+
+
+Fleet’s self-service software feature empowers end users by allowing them to independently install approved software packages from a curated list through the Fleet Desktop “My device” page. This not only reduces the administrative burden on IT teams but also enhances user productivity and satisfaction. In this guide, we will walk you through the process of uploading, editing, and managing self-service software packages in Fleet, enabling seamless software distribution and management.
+
+## Prerequisites
+
+* Fleet Premium is required for software self-service.
+
+> Software packages can be added to a specific team or to the "No team" category. The "No team" category is the default assignment for hosts that are not part of any specific team.
+
+## Step-by-Step Instructions
+
+### Adding a self-service software package
+
+1. **Navigate to the Software page**: Click “Software” in the main navigation menu.
+2. **Select a team**: Click the dropdown in the upper left corner of the page and click on the team to which you want to add the software package.
+3. **Open the “Add software” modal**: Click the “Add software” button in the upper right corner of the page.
+4. **Select a software package to upload**: Click “Choose file” in the “Add software” modal and select a software package from your computer.
+5. **Advanced options**: If desired, click “Advanced options” to add a pre-install condition or post-install script to your software package.
+ * **Pre-install condition**: This is an osquery query that results in true. For example, you might require a specific software title to exist before installing additional extensions.
+ * **Post-install script**: This might be used to apply a license key, perform configuration tasks, or execute cleanup tasks after the software installation.
+6. **Make the software package self-service**: Check the “Self-service” checkbox to mark the software package as self-service.
+7. **Finish the upload**: Click the “Add software” button to finish the upload process.
+
+### Editing a self-service software package
+
+1. **Navigate to the software details page for the software package**: Click “Software” in the main navigation menu.
+2. **Select a team**: Click the dropdown in the upper left corner of the page and click on the team to which you added the software package.
+3. **Filter by self-service**: To make it easier to find your software package, click on the dropdown to the left of the search bar and select “Self-service”. This will filter the results in the table to only show self-service software packages. If you still don’t see your software package, you can page through the results or search for your software package’s name in the search bar.
+4. **Open the details page**: Click on the software package’s name.
+5. **Open the actions dropdown**: Click on the “Actions” dropdown on the far right of the page. From here, you can download the software package, delete the software package, or click “Advanced options” to see the options you configured when adding the software package.
+
+### Downloading a self-service software package
+
+1. **Navigate to the software details page for the software package**: Click “Software” in the main navigation menu.
+2. **Select a team**: Click the dropdown in the upper left corner of the page and click on the team to which you added the software package.
+3. **Filter by self-service**: Click on the dropdown to the left of the search bar and select “Self-service” and page through the results or search for your software package’s name in the search bar.
+4. **Download the software package**:
+* **Option 1**: Click on the down-arrow next to the software package name in the list of self-service software packages to start an immediate download.
+* **Option 2**: Click on the software package’s name to open the details page. Click on the “Actions” dropdown on the far right of the page, and then click on “Download” to download the software package to your computer.
+
+### Deleting a self-service software package
+
+1. **Navigate to the software details page for the software package**: Click “Software” in the main navigation menu.
+2. **Select a team**: Click the dropdown in the upper left corner of the page and click on the team to which you added the software package.
+3. **Filter by self-service**: Click on the dropdown to the left of the search bar and select “Self-service” and page through the results or search for your software package’s name in the search bar.
+4. **Open the details page**: Click on the software package’s name.
+5. **Open the actions dropdown**: Click on the “Actions” dropdown on the far right of the page.
+6. **Delete the software package**: Click on “Delete” to remove the software package from Fleet. Confirm the deletion if prompted.
+
+### Installing self-service software packages
+
+To install the self-service software package on the host:
+
+1. **Navigate to the “Self-service” tab**: Click on the Fleet Desktop icon in the OS menu bar. Click “Self-service”. This will point your default web browser to the list of self-service software packages in the “My device” page.
+2. **Install the self-service software package**: Click the “Install” button for the software package you want to install.
+
+### Using the REST API for self-service software packages
+
+Fleet provides a REST API for managing software packages, including self-service software packages. Learn more about Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api#software).
+
+### Managing self-service software packages with GitOps
+
+To manage self-service software packages using Fleet's best practice GitOps, check out the `software` key in the [GitOps reference documentation](https://fleetdm.com/docs/using-fleet/gitops#software).
+
+> Note: with GitOps enabled, software packages uploaded using the web UI will not persist.
+
+## Conclusion
+
+Fleet’s self-service software feature not only simplifies software management for IT administrators but also empowers end users by giving them access to necessary software on demand. This feature ensures that your hosts remain secure while improving overall user experience. For further information and advanced management techniques, refer to Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api#software) and [GitOps](https://fleetdm.com/docs/using-fleet/gitops#software) documentation.
+
+
+
+
+
+
+
+
diff --git a/docs/01-Using-Fleet/standard-query-library/README.md b/articles/standard-query-library.md
similarity index 87%
rename from docs/01-Using-Fleet/standard-query-library/README.md
rename to articles/standard-query-library.md
index fc3f3bfdc1..7bbdd52bf2 100644
--- a/docs/01-Using-Fleet/standard-query-library/README.md
+++ b/articles/standard-query-library.md
@@ -47,4 +47,9 @@ Listed below are great resources that contain additional queries.
- Osquery (https://github.com/osquery/osquery/tree/master/packs)
- Palantir osquery configuration (https://github.com/palantir/osquery-configuration/tree/master/Fleet)
-
+
+
+
+
+
+
diff --git a/articles/tales-from-fleet-security-securing-the-startup.md b/articles/tales-from-fleet-security-securing-the-startup.md
index 09b0ac4fd6..6a8423dfc9 100644
--- a/articles/tales-from-fleet-security-securing-the-startup.md
+++ b/articles/tales-from-fleet-security-securing-the-startup.md
@@ -43,7 +43,7 @@ I thought using Apple’s Automated Device Enrollment (or Device Enrollment Prog
Technically, I was not wrong, but there are non-technical challenges.
-1. The requirements to establish a DEP account vary by country. In the US, for example, it requires a [DUNS](https://en.wikipedia.org/wiki/Data_Universal_Numbering_System) number. Getting a DUNS number is simple for US companies, but what is not easy is to fulfill similar requirements in every country where you would like to use DEP. We could not register for DEP in Canada. We have people in many other countries with a similar situation.
+1. The requirements to establish a ADE account vary by country. In the US, for example, it requires a [DUNS](https://en.wikipedia.org/wiki/Data_Universal_Numbering_System) number. Getting a DUNS number is simple for US companies, but what is not easy is to fulfill similar requirements in every country where you would like to use ADE. We could not register for ADE in Canada. We have people in many other countries with a similar situation.
2. The delays for obtaining hardware are very long. When planning endpoint deployment strategies, we must consider this, as supply chain issues will not disappear soon.
3. The benchmarks made by the Center for Internet Security (CIS) are excellent but are incredibly long (700+ pages) and written for experts. We wanted to be transparent about why we configured company devices a certain way and explain it so everyone could understand without Googling for hours.
@@ -55,9 +55,9 @@ Google should offer more granularity than on/off for third-party cookies, such a
## Solutions
-### DEP in other countries
+### ADE in other countries
-First, we enrolled in DEP in the US. Once we had our customer numbers and Mobile Device Management (MDM) system linked up, we were ready to buy laptops in the US that would get configured out of the box. Then, we found a workaround for Canada. If you add Apple’s Reseller ID to [Apple Business Manager](https://business.apple.com/), you can order computers over the phone and have them linked to your business account. The Reseller ID part is critical. I learned that the hard way, by receiving a laptop ordered like this to find it not part of DEP. Fortunately, it was easy for me to [add it to DEP manually](https://support.apple.com/en-ca/guide/apple-configurator/welcome/ios).
+First, we enrolled in ADE in the US. Once we had our customer numbers and Mobile Device Management (MDM) system linked up, we were ready to buy laptops in the US that would get configured out of the box. Then, we found a workaround for Canada. If you add Apple’s Reseller ID to [Apple Business Manager](https://business.apple.com/), you can order computers over the phone and have them linked to your business account. The Reseller ID part is critical. I learned that the hard way, by receiving a laptop ordered like this to find it not part of ADE. Fortunately, it was easy for me to [add it to ADE manually](https://support.apple.com/en-ca/guide/apple-configurator/welcome/ios).
We will keep trying the same approach in every country where we need Macs, though we know it will not be possible everywhere. We will either obtain equipment from a nearby country or rely on manual MDM enrollment by end-users for those countries.
@@ -76,7 +76,7 @@ Using the [CIS Benchmark for macOS 12](https://www.cisecurity.org/benchmark/appl
### Effort
-Implementing our own security baseline, configuring our MDM and DEP required a couple of days of effort, mostly because I insisted on reviewing all of the CIS Benchmark to be certain I didn’t miss something important. Having everything published in our handbook required additional effort, but if you were to use our baseline, you could get started very quickly. The main thing that will slow you down is getting onboarded to DEP, and receiving your first laptop ordered!
+Implementing our own security baseline, configuring our MDM and ADE required a couple of days of effort, mostly because I insisted on reviewing all of the CIS Benchmark to be certain I didn’t miss something important. Having everything published in our handbook required additional effort, but if you were to use our baseline, you could get started very quickly. The main thing that will slow you down is getting onboarded to ADE, and receiving your first laptop ordered!
## What's next?
diff --git a/articles/tales-from-fleet-security-soc2.md b/articles/tales-from-fleet-security-soc2.md
index c5b6d8aaaa..641583270a 100644
--- a/articles/tales-from-fleet-security-soc2.md
+++ b/articles/tales-from-fleet-security-soc2.md
@@ -43,7 +43,7 @@ One of the essential things about SOC 2 is having the right security policies. T
Writing policies from scratch can seem daunting. Many compliance automation products have templates you can use to get started, but there are excellent free and open resources online.
-As you can see, our policies are in our [handbook](https://fleetdm.com/handbook/business-operations/security-policies#information-security-policy-and-acceptable-use-policy), and we created most of them using this [free set of templates](https://github.com/JupiterOne/security-policy-templates) published by JupiterOne under Creative Commons licensing.
+As you can see, our policies are in our [handbook](https://fleetdm.com/handbook/digital-experience/security-policies#information-security-policy-and-acceptable-use-policy), and we created most of them using this [free set of templates](https://github.com/JupiterOne/security-policy-templates) published by JupiterOne under Creative Commons licensing.
We kept our policies as basic as possible to make sure everything in them is valuable and achievable. Having policies that state you must do the impossible is a surefire way of getting in trouble! The templates we used contained many processes and procedures as well. We used the policies and will eventually document more of our procedures in our handbook.
diff --git a/docs/Using Fleet/segment-hosts.md b/articles/teams.md
similarity index 87%
rename from docs/Using Fleet/segment-hosts.md
rename to articles/teams.md
index 548bb1c4cd..ea688bc08b 100644
--- a/docs/Using Fleet/segment-hosts.md
+++ b/articles/teams.md
@@ -1,8 +1,8 @@
-# Segment hosts
+# Teams
_Available in Fleet Premium_
-In Fleet, you can group hosts together in a "team" in Fleet. This way, you can apply queries, policies, scripts, and more that are tailored to the hosts' risk/compliance needs.
+In Fleet, you can group hosts together in a "team" in Fleet. This way, you can apply queries, policies, scripts, and more that are tailored to a host's risk/compliance needs.
A host can only belong to one team.
@@ -30,10 +30,13 @@ You can add hosts to a new team in Fleet by either enrolling the host with a tea
## Advanced
-You can automatically enroll hosts to a specific team in Fleet by installing a fleetd with a team enroll secret. Learn more [here](./enroll-hosts.md#enroll-host-to-a-specific-team).
+You can automatically enroll hosts to a specific team in Fleet by installing a fleetd with a team enroll secret. Learn more [here](https://fleetdm.com/guides/enroll-hosts#enroll-host-to-a-specific-team).
Changing the host's enroll secret after enrollment will not cause the host to be transferred to a different team.
-
+
+
+
+
+
-
diff --git a/docs/Using Fleet/Vulnerability-Processing.md b/articles/vulnerability-processing.md
similarity index 54%
rename from docs/Using Fleet/Vulnerability-Processing.md
rename to articles/vulnerability-processing.md
index 2e675e5735..610bd51347 100644
--- a/docs/Using Fleet/Vulnerability-Processing.md
+++ b/articles/vulnerability-processing.md
@@ -1,7 +1,5 @@
# Vulnerability processing
-## Introduction
-
Vulnerability processing in Fleet detects vulnerabilities (CVEs) for the software installed on your hosts.
To see what software is covered, check out the [Coverage section](#coverage).
@@ -16,18 +14,27 @@ To see what software is covered, check out the [Coverage section](#coverage).
Fleet detects vulnerabilities for these software types:
-| Type | macOS | Windows | Linux |
-| ------------------- | ------------------------------------------ | ------------------------------------------------ | ---------------- |
-| Apps | ✅ | ✅ | ❌ |
-| Browser plugins | Chrome extensions, Firefox extensions | Chrome extensions, Firefox extensions | ❌ |
-| Packages | Python, Homebrew | Python, Atom, Chocolatey | Packages defined in the [OVAL definitions](https://github.com/fleetdm/nvd/blob/master/oval_sources.json), except for vulnerabilities involving configuration files. Supported distributions:
Ubuntu
RHEL based distros (Red Hat, CentOS, Fedora, and Amazon Linux)
|
-| IDE extensions | VS Code extensions | VS Code extensions | VS Code extensions |
+| Type | macOS | Windows | Linux |
+| ------------------- | ------------------------------------------ | ------------------------------------------------ |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Apps | ✅ | ✅ | ❌ |
+| Browser plugins | Chrome extensions, Firefox extensions | Chrome extensions, Firefox extensions | ❌ |
+| Packages | Python, Homebrew | Python, Atom, Chocolatey |
For Ubuntu, Debian, RHEL (including CentOS), and Fedora: packages defined in the [OVAL definitions](https://github.com/fleetdm/nvd/blob/master/oval_sources.json), except for vulnerabilities involving configuration files.
For Amazon Linux, packages maintained by Amazon by checking [ALAS advisories](https://alas.aws.amazon.com/).
|
+| IDE extensions | VS Code extensions | VS Code extensions | VS Code extensions |
As of right now, only app names with all ASCII characters are supported. Apps with names featuring non-ASCII characters, such as Cyrillic, will not generate matches.
For Ubuntu Linux, kernel vulnerabilities with known variants (ie. `-generic`) are detected using OVAL. Custom kernels (unknown variants) are detected using NVD.
-### Advanced configuration
+## Sources
+
+Fleet combines multiple sources to get accurate and up-to-date CVE information:
+- [National Vulnerability Database](https://nvd.nist.gov/developers/vulnerabilities) CVE feeds
+- [VulnCheck](https://vulncheck.com/) CVE feeds
+- [Mac Office release notes](https://learn.microsoft.com/en-us/officeupdates/release-notes-office-for-mac) for Office for Mac
+- [Microsoft MSRC Security Bulletins](https://msrc.microsoft.com/update-guide) for Windows OS vulnerabilities
+- [OVAL definitions](https://github.com/fleetdm/nvd/blob/master/oval_sources.json) for Linux software
+
+## Advanced configuration
Fleet runs vulnerability downloading and processing via internal scheduled cron job. This internal mechanism is very useful
for frictionless deployments and is well suited for most use cases. However, in larger deployments,
@@ -63,6 +70,9 @@ command.
fleet vuln_processing
```
-
+
+
+
+
+
-
diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md
index 1fb36f3bf6..87188e11ee 100644
--- a/articles/windows-mdm-setup.md
+++ b/articles/windows-mdm-setup.md
@@ -10,7 +10,7 @@ To use automatic enrollment (aka zero-touch) features on Windows, follow instruc
### Step 1: Generate your certificate and key
-Fleet uses a certificate and key pair to authenticate and manage interactions between Fleet and Windows host.
+Fleet uses a certificate and key pair to authenticate and manage interactions between the Fleet server and a Windows host.
How to generate a certificate and key:
diff --git a/assets/images/iPadOS-install-profile.png b/assets/images/iPadOS-install-profile.png
new file mode 100644
index 0000000000..c398038517
Binary files /dev/null and b/assets/images/iPadOS-install-profile.png differ
diff --git a/assets/images/iPadOS-profile-downloaded.png b/assets/images/iPadOS-profile-downloaded.png
new file mode 100644
index 0000000000..f21b12615f
Binary files /dev/null and b/assets/images/iPadOS-profile-downloaded.png differ
diff --git a/assets/images/ios-install-profile.png b/assets/images/ios-install-profile.png
new file mode 100644
index 0000000000..8266c55201
Binary files /dev/null and b/assets/images/ios-install-profile.png differ
diff --git a/assets/images/ios-profile-downloaded.png b/assets/images/ios-profile-downloaded.png
new file mode 100644
index 0000000000..7941a65815
Binary files /dev/null and b/assets/images/ios-profile-downloaded.png differ
diff --git a/changes/13157-fv-escrow b/changes/13157-fv-escrow
deleted file mode 100644
index e6804a05ec..0000000000
--- a/changes/13157-fv-escrow
+++ /dev/null
@@ -1 +0,0 @@
-* `fleetd` now uses Escrow Buddy to rotate FileVault keys. Internal API endpoints documented in the API for contributors have been modified and/or removed.
diff --git a/changes/16866-ade-force-filevault b/changes/16866-ade-force-filevault
deleted file mode 100644
index 4486357bf5..0000000000
--- a/changes/16866-ade-force-filevault
+++ /dev/null
@@ -1,2 +0,0 @@
-- Adds enforcement of FileVault during the MacOS Setup Assistant process for hosts that are enrolled
-into teams (or no team) with disk encryption turned on.
\ No newline at end of file
diff --git a/changes/17249-mysql-8 b/changes/17249-mysql-8
deleted file mode 100644
index b3948968cf..0000000000
--- a/changes/17249-mysql-8
+++ /dev/null
@@ -1,2 +0,0 @@
-* Drop support for MySQL 5.7
-* Minimum requirements raised to MySQL 8.0
diff --git a/changes/17558-validation-errs b/changes/17558-validation-errs
new file mode 100644
index 0000000000..115c9bf14e
--- /dev/null
+++ b/changes/17558-validation-errs
@@ -0,0 +1,2 @@
+- Adds validation of Setup Assistant profiles on profile upload, giving users immediate feedback on
+the validity of the profile.
\ No newline at end of file
diff --git a/changes/1845-linux-arm64 b/changes/1845-linux-arm64
deleted file mode 100644
index 6ebb53ff63..0000000000
--- a/changes/1845-linux-arm64
+++ /dev/null
@@ -1,2 +0,0 @@
-* Added support for generating fleetd packages for Linux ARM64
-* fleetctl: New `fleetctl package` --arch flag
diff --git a/changes/18897-shoe-zeroes b/changes/18897-shoe-zeroes
new file mode 100644
index 0000000000..7faddd522d
--- /dev/null
+++ b/changes/18897-shoe-zeroes
@@ -0,0 +1 @@
+Added "0 items" description on empty software tables for UI consistency
diff --git a/changes/18913-ignore-rejected-cves b/changes/18913-ignore-rejected-cves
deleted file mode 100644
index 1fabe60f9f..0000000000
--- a/changes/18913-ignore-rejected-cves
+++ /dev/null
@@ -1 +0,0 @@
-CVEs identified as 'Rejected' in NVD will no longer match against software
\ No newline at end of file
diff --git a/changes/19280-maintenance-window-descriptions b/changes/19280-maintenance-window-descriptions
deleted file mode 100644
index 90848dcffe..0000000000
--- a/changes/19280-maintenance-window-descriptions
+++ /dev/null
@@ -1 +0,0 @@
-Maintenance window descriptions are now updated regularly to match the failing policy description/resolution.
diff --git a/changes/19352-calendar-real-time b/changes/19352-calendar-real-time
deleted file mode 100644
index d96cf1fa11..0000000000
--- a/changes/19352-calendar-real-time
+++ /dev/null
@@ -1,3 +0,0 @@
-- In maintenance windows using Google Calendar, calendar event is now recreated within 30 seconds if deleted or moved to the past.
- - Fleet server watches for potential changes for up to 1 week after original event time. If event is moved forward more than 1 week, then after 1 week Fleet server will check for event changes once every 30 minutes.
- - These near real-time updates may add additional load to the Google Calendar API, so it is recommended to use API usage alerts or other monitoring methods.
diff --git a/changes/19442-ubuntu-python-packages b/changes/19442-ubuntu-python-packages
new file mode 100644
index 0000000000..0be7e95616
--- /dev/null
+++ b/changes/19442-ubuntu-python-packages
@@ -0,0 +1 @@
+- Addressing Ubuntu python package false positive vulnerabilities by removing duplicate entries for ubuntu python packages installed by dpkg and renaming remaining pip installed packages to match OVAL definitions.
\ No newline at end of file
diff --git a/changes/19447-ios-ipados-software b/changes/19447-ios-ipados-software
deleted file mode 100644
index 26acad5131..0000000000
--- a/changes/19447-ios-ipados-software
+++ /dev/null
@@ -1,3 +0,0 @@
-- iOS and iPadOS device details refetch can now be triggered with the existing `POST /api/latest/fleet/hosts/:id/refetch` endpoint.
-- iOS and iPadOS user-installed apps can be viewed in Fleet
-- iOS and iPadOS apps can be installed using Apple's VPP (Volume Purchase Program)
diff --git a/changes/19550-software-no-teams b/changes/19550-software-no-teams
deleted file mode 100644
index 933665cd22..0000000000
--- a/changes/19550-software-no-teams
+++ /dev/null
@@ -1 +0,0 @@
-- adds support for No teams on all software pages including adding software installers
\ No newline at end of file
diff --git a/changes/19551-policy-software-automations b/changes/19551-policy-software-automations
new file mode 100644
index 0000000000..4b88cb4c1f
--- /dev/null
+++ b/changes/19551-policy-software-automations
@@ -0,0 +1 @@
+* Implement features allowing automatic installation of software on hosts that fail policies.
diff --git a/changes/19646-ui-profiles-pending-tooltip b/changes/19646-ui-profiles-pending-tooltip
deleted file mode 100644
index 824ba143c9..0000000000
--- a/changes/19646-ui-profiles-pending-tooltip
+++ /dev/null
@@ -1 +0,0 @@
-- Updated UI tooltips for pending OS settings.
diff --git a/changes/19684-renew-scep-180 b/changes/19684-renew-scep-180
deleted file mode 100644
index 131c08ff51..0000000000
--- a/changes/19684-renew-scep-180
+++ /dev/null
@@ -1 +0,0 @@
-* Increase threshold to renew Apple SCEP certificates for MDM enrollments to 180 days.
diff --git a/changes/19808-prof b/changes/19808-prof
new file mode 100644
index 0000000000..71d19f8c4b
--- /dev/null
+++ b/changes/19808-prof
@@ -0,0 +1 @@
+* Fixed bugs on enrollment profiles when the organization name contains invalid XML characters.
diff --git a/changes/19853-homebrew-intellij b/changes/19853-homebrew-intellij
deleted file mode 100644
index 713d4ae142..0000000000
--- a/changes/19853-homebrew-intellij
+++ /dev/null
@@ -1 +0,0 @@
-Fixed false negative vulnerabilities with IntelliJ IDEA CE and PyCharm CE installed via Homebrew.
diff --git a/changes/19864-vpp-token-crud b/changes/19864-vpp-token-crud
deleted file mode 100644
index ee4a92e80f..0000000000
--- a/changes/19864-vpp-token-crud
+++ /dev/null
@@ -1,2 +0,0 @@
-- Adds the functionality for the `POST /mdm/apple/vpp_token`, `DELETE /mdm/apple/vpp_token` and
-`GET /vpp` endpoints.
\ No newline at end of file
diff --git a/changes/19865-db-schema b/changes/19865-db-schema
deleted file mode 100644
index ede5f90ed0..0000000000
--- a/changes/19865-db-schema
+++ /dev/null
@@ -1 +0,0 @@
-- Adds DB updates to support the VPP software feature.
\ No newline at end of file
diff --git a/changes/19867-get-avail-apps b/changes/19867-get-avail-apps
deleted file mode 100644
index 4ace068f95..0000000000
--- a/changes/19867-get-avail-apps
+++ /dev/null
@@ -1 +0,0 @@
-- Adds functionality for the `GET /software/app_store_apps` and `POST /software/app_store_apps` endpoints.
\ No newline at end of file
diff --git a/changes/19868-vpp-install-command b/changes/19868-vpp-install-command
deleted file mode 100644
index 337b5d5010..0000000000
--- a/changes/19868-vpp-install-command
+++ /dev/null
@@ -1 +0,0 @@
-- Adds functionality for installing App Store apps to the VPP feature.
\ No newline at end of file
diff --git a/changes/19870-vpp-activities-backend b/changes/19870-vpp-activities-backend
deleted file mode 100644
index 115f92e1fd..0000000000
--- a/changes/19870-vpp-activities-backend
+++ /dev/null
@@ -1 +0,0 @@
-- Adds global activity support for VPP related activities.
\ No newline at end of file
diff --git a/changes/19871-gitops-vpp-config b/changes/19871-gitops-vpp-config
deleted file mode 100644
index e9a02e0fa7..0000000000
--- a/changes/19871-gitops-vpp-config
+++ /dev/null
@@ -1 +0,0 @@
-* Add support for VPP to gitops config
diff --git a/changes/19880-include-vpp-apps-in-software-titles-endpoints b/changes/19880-include-vpp-apps-in-software-titles-endpoints
deleted file mode 100644
index 9503cdef99..0000000000
--- a/changes/19880-include-vpp-apps-in-software-titles-endpoints
+++ /dev/null
@@ -1,2 +0,0 @@
-* Added the associated VPP apps to the `GET /software/titles` and `GET /software/titles/:id` endpoints.
-* Added the associated VPP apps to the `GET /hosts/:id/software` and `GET /device/:token/software` endpoints.
diff --git a/changes/20042-remove-package-version b/changes/20042-remove-package-version
deleted file mode 100644
index a4a5801417..0000000000
--- a/changes/20042-remove-package-version
+++ /dev/null
@@ -1 +0,0 @@
-In `fleetctl package` command, removed the `--version` flag. The version of the package can be controlled by `--orbit-channel` flag.
diff --git a/changes/20100-os-version-compliance b/changes/20100-os-version-compliance
deleted file mode 100644
index f14334f97f..0000000000
--- a/changes/20100-os-version-compliance
+++ /dev/null
@@ -1 +0,0 @@
-- Fleet UI: Show OS version compliance on Host Details page
diff --git a/changes/20271-deleted-host-software-installs b/changes/20271-deleted-host-software-installs
deleted file mode 100644
index 674b8a823f..0000000000
--- a/changes/20271-deleted-host-software-installs
+++ /dev/null
@@ -1 +0,0 @@
-- Fig bug where software install results could not be retrieved for deleted hosts in the activity feed
diff --git a/changes/20278-vpp-batch-api b/changes/20278-vpp-batch-api
deleted file mode 100644
index e5cbbf7eca..0000000000
--- a/changes/20278-vpp-batch-api
+++ /dev/null
@@ -1 +0,0 @@
-- GitOps supports VPP app associations
diff --git a/changes/20320-uninstall-packages b/changes/20320-uninstall-packages
new file mode 100644
index 0000000000..89ab892841
--- /dev/null
+++ b/changes/20320-uninstall-packages
@@ -0,0 +1 @@
+* Implement the ability to use Fleet to uninstall packages from hosts.
\ No newline at end of file
diff --git a/changes/20370-linux-nologin b/changes/20370-linux-nologin
deleted file mode 100644
index 236418c963..0000000000
--- a/changes/20370-linux-nologin
+++ /dev/null
@@ -1 +0,0 @@
-- Linux lock/unlock scripts now make use of pam_nologin to keep AD users locked out
diff --git a/changes/20397-do-not-set-last_enrolled_at-when-enrolling-orbit b/changes/20397-do-not-set-last_enrolled_at-when-enrolling-orbit
deleted file mode 100644
index c8f305c4d1..0000000000
--- a/changes/20397-do-not-set-last_enrolled_at-when-enrolling-orbit
+++ /dev/null
@@ -1 +0,0 @@
-* Fixed a bug that set `last_enrolled_at` during orbit re-enrollment, which caused osquery enroll failures when `FLEET_OSQUERY_ENROLL_COOLDOWN` is set .
diff --git a/changes/20404-edit-software b/changes/20404-edit-software
new file mode 100644
index 0000000000..ec65b392b4
--- /dev/null
+++ b/changes/20404-edit-software
@@ -0,0 +1 @@
+* Software installer packages, self-service flag, scripts, pre-install query, and self-service availability can now be edited in-place rather than needing to be deleted and re-added.
diff --git a/changes/20440-Notion-exe-installer-name b/changes/20440-Notion-exe-installer-name
deleted file mode 100644
index bc3996cc5d..0000000000
--- a/changes/20440-Notion-exe-installer-name
+++ /dev/null
@@ -1 +0,0 @@
-* Added a special-case to properly name the Notion .exe Windows installer the same as how it will be reported by osquery post-install.
diff --git a/changes/20467-vpp-ipadios-ui b/changes/20467-vpp-ipadios-ui
deleted file mode 100644
index 2cc84e31cd..0000000000
--- a/changes/20467-vpp-ipadios-ui
+++ /dev/null
@@ -1 +0,0 @@
-* Add UI features for managing Apple VPP apps for iPadOS and iOS hosts
\ No newline at end of file
diff --git a/changes/20469-backend-ios-ipados-os-updates b/changes/20469-backend-ios-ipados-os-updates
deleted file mode 100644
index 075cca4876..0000000000
--- a/changes/20469-backend-ios-ipados-os-updates
+++ /dev/null
@@ -1 +0,0 @@
-* Adding OS updates support to iOS/iPadOS devices.
diff --git a/changes/20515-delete-vpp-app b/changes/20515-delete-vpp-app
deleted file mode 100644
index 49599edf94..0000000000
--- a/changes/20515-delete-vpp-app
+++ /dev/null
@@ -1,2 +0,0 @@
-* Added support to delete a VPP app from a team in `DELETE /software/titles/:software_title_id/available_for_install`.
-* Fixed path that was incorrect for the download software installer package endpoint `GET /software/titles/:software_title_id/package`.
diff --git a/changes/20535-sw-table-loading b/changes/20535-sw-table-loading
new file mode 100644
index 0000000000..d144ce782c
--- /dev/null
+++ b/changes/20535-sw-table-loading
@@ -0,0 +1 @@
+* Improve loading state for DataTables when no data is present yet
\ No newline at end of file
diff --git a/changes/20575-fix-profile-activities-to-include-ios-ipados b/changes/20575-fix-profile-activities-to-include-ios-ipados
deleted file mode 100644
index bf089bf489..0000000000
--- a/changes/20575-fix-profile-activities-to-include-ios-ipados
+++ /dev/null
@@ -1 +0,0 @@
-- Update profile activities to include iOS and iPadOS
diff --git a/changes/20604-hosts-page-pagination b/changes/20604-hosts-page-pagination
deleted file mode 100644
index c1f68d5f94..0000000000
--- a/changes/20604-hosts-page-pagination
+++ /dev/null
@@ -1 +0,0 @@
-* Fix a bug where hosts page would sometimes allow excess pagination
\ No newline at end of file
diff --git a/changes/20618-nil-tz-not-handled b/changes/20618-nil-tz-not-handled
deleted file mode 100644
index cbb5d0bd99..0000000000
--- a/changes/20618-nil-tz-not-handled
+++ /dev/null
@@ -1,2 +0,0 @@
-* Fix a bug where Fleet google calendar events generated by Fleet <= 4.53.0 were not correctly
- processed by 4.54.0
\ No newline at end of file
diff --git a/changes/20683-less-columns-smaller-width b/changes/20683-less-columns-smaller-width
new file mode 100644
index 0000000000..c2e03aedde
--- /dev/null
+++ b/changes/20683-less-columns-smaller-width
@@ -0,0 +1 @@
+- UI cleanup: Host details about section condenses information into fewer columns at smaller widths
diff --git a/changes/20747-gitops-software-query b/changes/20747-gitops-software-query
deleted file mode 100644
index 100efc17f3..0000000000
--- a/changes/20747-gitops-software-query
+++ /dev/null
@@ -1 +0,0 @@
-- Use new gitops format for software pre install query
diff --git a/changes/20751-detect-held-linux-packages-as-installed b/changes/20751-detect-held-linux-packages-as-installed
deleted file mode 100644
index 6aa524ce80..0000000000
--- a/changes/20751-detect-held-linux-packages-as-installed
+++ /dev/null
@@ -1 +0,0 @@
-Linux .deb packages 'on hold' are now included in the installed software list.
diff --git a/changes/20757-profiles-batch-activity b/changes/20757-profiles-batch-activity
new file mode 100644
index 0000000000..6b110b87c7
--- /dev/null
+++ b/changes/20757-profiles-batch-activity
@@ -0,0 +1 @@
+API endpoint `/api/v1/fleet/mdm/profiles/batch` will now not log an activity for profile types that did not change in the database (Apple configuration profiles, Windows configuration profiles, or Apple declarations).
diff --git a/changes/20764-fix-cron-with-duplicate-host-uuid-windows-mdm b/changes/20764-fix-cron-with-duplicate-host-uuid-windows-mdm
new file mode 100644
index 0000000000..df19c08bc8
--- /dev/null
+++ b/changes/20764-fix-cron-with-duplicate-host-uuid-windows-mdm
@@ -0,0 +1 @@
+* Fixed an issue where cron profiles delivery fails if a Windows VM is enrolled twice with the same `host_uuid` / `mdm_device_id`.
diff --git a/changes/20828-better-appid-error b/changes/20828-better-appid-error
new file mode 100644
index 0000000000..540c8fcbfa
--- /dev/null
+++ b/changes/20828-better-appid-error
@@ -0,0 +1 @@
+- Improve clarity of gitops VPP app ID type errors
diff --git a/changes/20846-vuln-virtual-box b/changes/20846-vuln-virtual-box
new file mode 100644
index 0000000000..225dd0be22
--- /dev/null
+++ b/changes/20846-vuln-virtual-box
@@ -0,0 +1 @@
+- resolved an issue where virtual box for macOS wasn't matching against the vm_virtualbox NVD product name
\ No newline at end of file
diff --git a/changes/20865-fix-chrome-icon b/changes/20865-fix-chrome-icon
new file mode 100644
index 0000000000..9ac53c39cc
--- /dev/null
+++ b/changes/20865-fix-chrome-icon
@@ -0,0 +1 @@
+- show proper software icon for chrome packages
diff --git a/changes/20868-turn-off-mdm b/changes/20868-turn-off-mdm
new file mode 100644
index 0000000000..bfcd35d315
--- /dev/null
+++ b/changes/20868-turn-off-mdm
@@ -0,0 +1 @@
+- Improves the UX of turning off MDM on an offline host (endpoint doesn't error anymore)
\ No newline at end of file
diff --git a/changes/20895-policy-software-install-gitops b/changes/20895-policy-software-install-gitops
new file mode 100644
index 0000000000..774f6a4bfe
--- /dev/null
+++ b/changes/20895-policy-software-install-gitops
@@ -0,0 +1 @@
+* Added support for configuring policy installers via GitOps.
diff --git a/changes/20959-query-host-flow-fix-observer b/changes/20959-query-host-flow-fix-observer
new file mode 100644
index 0000000000..f1db67c3e9
--- /dev/null
+++ b/changes/20959-query-host-flow-fix-observer
@@ -0,0 +1 @@
+- Fix UI flow for observers to easily query hosts from the host details page
diff --git a/changes/21019-ota-enrollment b/changes/21019-ota-enrollment
new file mode 100644
index 0000000000..b43db060a7
--- /dev/null
+++ b/changes/21019-ota-enrollment
@@ -0,0 +1 @@
+* Implement protocol support for OTA enrollment and automatic team assignment for hosts.
diff --git a/changes/21264-fix-reserved-team-names b/changes/21264-fix-reserved-team-names
new file mode 100644
index 0000000000..6363b81869
--- /dev/null
+++ b/changes/21264-fix-reserved-team-names
@@ -0,0 +1,2 @@
+- Prevents teams with the name "All teams" or "No team" from being created (these are reserved team
+ names in Fleet).
\ No newline at end of file
diff --git a/changes/21315-vpp-premium-license b/changes/21315-vpp-premium-license
new file mode 100644
index 0000000000..2fd081703e
--- /dev/null
+++ b/changes/21315-vpp-premium-license
@@ -0,0 +1 @@
+- Verify user has premium license before uploading VPP tokens
diff --git a/changes/21402-improve-windows-mdm-enabled-error-message b/changes/21402-improve-windows-mdm-enabled-error-message
new file mode 100644
index 0000000000..36dc6082f6
--- /dev/null
+++ b/changes/21402-improve-windows-mdm-enabled-error-message
@@ -0,0 +1 @@
+- Improve gitops error message about enabling windows MDM
diff --git a/changes/21404-minio-false-positive b/changes/21404-minio-false-positive
new file mode 100644
index 0000000000..57b4245e45
--- /dev/null
+++ b/changes/21404-minio-false-positive
@@ -0,0 +1 @@
+- resolved issue where minio was reporting false positive vulnerabilities due to a mismatch in version strings
\ No newline at end of file
diff --git a/changes/21412-remove-node-key-from-server-logs b/changes/21412-remove-node-key-from-server-logs
new file mode 100644
index 0000000000..c6555bd5bc
--- /dev/null
+++ b/changes/21412-remove-node-key-from-server-logs
@@ -0,0 +1 @@
+* Removed invalid node keys from server logs.
diff --git a/changes/21428-policy-automatic-install-software b/changes/21428-policy-automatic-install-software
new file mode 100644
index 0000000000..e61dc2a9ea
--- /dev/null
+++ b/changes/21428-policy-automatic-install-software
@@ -0,0 +1 @@
+* Added automatic installation of software packages using policy automations.
diff --git a/changes/21428-prevent-install-when-already-pending b/changes/21428-prevent-install-when-already-pending
new file mode 100644
index 0000000000..d01006d6f9
--- /dev/null
+++ b/changes/21428-prevent-install-when-already-pending
@@ -0,0 +1 @@
+* Added validation to `POST /api/_version_/fleet/hosts/{host_id}/software/install/{software_title_id}` to prevent installing on a host that already has a pending installation for that software title.
diff --git a/changes/21462-host-vulnerability-filter b/changes/21462-host-vulnerability-filter
new file mode 100644
index 0000000000..e55fb8c836
--- /dev/null
+++ b/changes/21462-host-vulnerability-filter
@@ -0,0 +1 @@
+- fixed issue where the vulnerability filter was returning software not vulnerable for the currently selected host
\ No newline at end of file
diff --git a/changes/21467-policies-for-no-team b/changes/21467-policies-for-no-team
new file mode 100644
index 0000000000..4613cd39ed
--- /dev/null
+++ b/changes/21467-policies-for-no-team
@@ -0,0 +1 @@
+* Added support for policies in "No team" that run on hosts that belong to "No team".
diff --git a/changes/21468-no-teams-policies b/changes/21468-no-teams-policies
new file mode 100644
index 0000000000..d11adda1b8
--- /dev/null
+++ b/changes/21468-no-teams-policies
@@ -0,0 +1 @@
+* Enable 'No teams' funcitonality for the policies page and associated workflows.
\ No newline at end of file
diff --git a/changes/21557-ota-profile-endpoint b/changes/21557-ota-profile-endpoint
new file mode 100644
index 0000000000..4acf2bbcf5
--- /dev/null
+++ b/changes/21557-ota-profile-endpoint
@@ -0,0 +1 @@
+- Adds an endpoint for getting an OTA MDM profile for enrolling iOS and iPadOS hosts.
\ No newline at end of file
diff --git a/changes/21559-add-end-user-enrolment-page b/changes/21559-add-end-user-enrolment-page
new file mode 100644
index 0000000000..427f1c5beb
--- /dev/null
+++ b/changes/21559-add-end-user-enrolment-page
@@ -0,0 +1 @@
+- add feature for end users to enroll their device into fleet mdm
diff --git a/changes/21612-edit-software-gitops b/changes/21612-edit-software-gitops
new file mode 100644
index 0000000000..9a157286d4
--- /dev/null
+++ b/changes/21612-edit-software-gitops
@@ -0,0 +1 @@
+* Reset install counts and cancel pending installs/uninstalls when GitOps installer updates change package contents
diff --git a/changes/21683-apns-cert-validation-on-start b/changes/21683-apns-cert-validation-on-start
new file mode 100644
index 0000000000..9f17143599
--- /dev/null
+++ b/changes/21683-apns-cert-validation-on-start
@@ -0,0 +1,2 @@
+- Removed validation of APNS certificate from server startup. This was no longer necessary because
+ we now allow for APNS certificates to be renewed in the UI.
diff --git a/changes/21779-git-false-negative b/changes/21779-git-false-negative
new file mode 100644
index 0000000000..080dfe1a4e
--- /dev/null
+++ b/changes/21779-git-false-negative
@@ -0,0 +1 @@
+- fixed a false negative vulnerability for git
\ No newline at end of file
diff --git a/changes/21796-fix-vpp-self-service-checkbox b/changes/21796-fix-vpp-self-service-checkbox
new file mode 100644
index 0000000000..6ec7e46db9
--- /dev/null
+++ b/changes/21796-fix-vpp-self-service-checkbox
@@ -0,0 +1 @@
+- Fixed self-service checkbox appearing when iOS or iPadOS app is selected.
diff --git a/changes/21813-email-err b/changes/21813-email-err
new file mode 100644
index 0000000000..a9d25ecc21
--- /dev/null
+++ b/changes/21813-email-err
@@ -0,0 +1,2 @@
+- Fixed regression: we now check if the email used to get a signed CSR is invalid (i.e. is an email
+ from a free email provider).
\ No newline at end of file
diff --git a/changes/21866-startup-expired-abm-cert b/changes/21866-startup-expired-abm-cert
new file mode 100644
index 0000000000..f9e74bb641
--- /dev/null
+++ b/changes/21866-startup-expired-abm-cert
@@ -0,0 +1,2 @@
+- Fixed issue where Fleet server could start when expired ABM cerfificate was provided as server
+ config options.
diff --git a/changes/21890-vpp-token-error b/changes/21890-vpp-token-error
new file mode 100644
index 0000000000..da03734eca
--- /dev/null
+++ b/changes/21890-vpp-token-error
@@ -0,0 +1 @@
+- Improve messaging for VPP token constraint errors
diff --git a/changes/21891-mdm-profile-fails b/changes/21891-mdm-profile-fails
new file mode 100644
index 0000000000..a01bac1649
--- /dev/null
+++ b/changes/21891-mdm-profile-fails
@@ -0,0 +1,2 @@
+- Fixes a bug where a profile wouldn't be removed from a host if it was deleted or if the host was
+ moved to another team before the profile was installed on the host.
\ No newline at end of file
diff --git a/changes/21976-update-macos-target-version-tooltip b/changes/21976-update-macos-target-version-tooltip
new file mode 100644
index 0000000000..5ae1a5ffdf
--- /dev/null
+++ b/changes/21976-update-macos-target-version-tooltip
@@ -0,0 +1 @@
+- update the macos target minimum version tooltip
diff --git a/changes/22069-gitops-async-software-batch b/changes/22069-gitops-async-software-batch
new file mode 100644
index 0000000000..35f0652fe2
--- /dev/null
+++ b/changes/22069-gitops-async-software-batch
@@ -0,0 +1 @@
+* Modified `POST /api/latest/fleet/software/batch` endpoint to be asynchronous and added a new endpoint `GET /api/latest/fleet/software/batch/{request_uuid}` to retrieve the result of the batch upload.
diff --git a/changes/22097-mdm-migration-guide b/changes/22097-mdm-migration-guide
new file mode 100644
index 0000000000..0177cf49b6
--- /dev/null
+++ b/changes/22097-mdm-migration-guide
@@ -0,0 +1 @@
+- Updates the guide for MDM migration to include the new UX in fleetd.
\ No newline at end of file
diff --git a/changes/22106-fix-software-package-name b/changes/22106-fix-software-package-name
new file mode 100644
index 0000000000..3cd87a328e
--- /dev/null
+++ b/changes/22106-fix-software-package-name
@@ -0,0 +1 @@
+- Fixed UI design bug where software package file name was not displayed as expected.
diff --git a/changes/22136-software-status-no-teams-hosts-page b/changes/22136-software-status-no-teams-hosts-page
new file mode 100644
index 0000000000..6ede268471
--- /dev/null
+++ b/changes/22136-software-status-no-teams-hosts-page
@@ -0,0 +1 @@
+* Support the software status filter for 'No teams' on the hosts page
\ No newline at end of file
diff --git a/changes/22158-scep b/changes/22158-scep
new file mode 100644
index 0000000000..ab75574680
--- /dev/null
+++ b/changes/22158-scep
@@ -0,0 +1 @@
+* Allow custom SCEP CA certificates with any kind of extendedKeyUsage attributes.
diff --git a/changes/7476-fix-ui-overflow-os-settings-table b/changes/7476-fix-ui-overflow-os-settings-table
new file mode 100644
index 0000000000..6c95925de8
--- /dev/null
+++ b/changes/7476-fix-ui-overflow-os-settings-table
@@ -0,0 +1 @@
+- fixes UI overflow issues with OS settings table data
diff --git a/changes/api-get-host-by-identifier-exclude-software b/changes/api-get-host-by-identifier-exclude-software
deleted file mode 100644
index aa2aa5404a..0000000000
--- a/changes/api-get-host-by-identifier-exclude-software
+++ /dev/null
@@ -1 +0,0 @@
-- add exclude_software query paramter to "Get host by identifier" API
\ No newline at end of file
diff --git a/changes/apns-errors b/changes/apns-errors
new file mode 100644
index 0000000000..6de48617a1
--- /dev/null
+++ b/changes/apns-errors
@@ -0,0 +1 @@
+* Fixed logic to properly catch and log APNs errors.
diff --git a/changes/hosts-can-access-any-software b/changes/hosts-can-access-any-software
new file mode 100644
index 0000000000..0fbcae035a
--- /dev/null
+++ b/changes/hosts-can-access-any-software
@@ -0,0 +1 @@
+- Hosts can no longer access installers that aren't directly assigned to it
diff --git a/changes/issue-19691-add-vpp-token-expiry-banner b/changes/issue-19691-add-vpp-token-expiry-banner
deleted file mode 100644
index d4f14c98c6..0000000000
--- a/changes/issue-19691-add-vpp-token-expiry-banner
+++ /dev/null
@@ -1 +0,0 @@
-- add a warning banner to the UI if the uploaded VPP token is about to expire/has expired.
diff --git a/changes/issue-19866-add-remove-disable-vpp-in-ui b/changes/issue-19866-add-remove-disable-vpp-in-ui
deleted file mode 100644
index 09000dbff2..0000000000
--- a/changes/issue-19866-add-remove-disable-vpp-in-ui
+++ /dev/null
@@ -1 +0,0 @@
-- add ability to add/remove/disable vpp in the fleet UI.
diff --git a/changes/issue-19869-vpp-ui-on-software-pages b/changes/issue-19869-vpp-ui-on-software-pages
deleted file mode 100644
index 74f71d41c9..0000000000
--- a/changes/issue-19869-vpp-ui-on-software-pages
+++ /dev/null
@@ -1 +0,0 @@
-- add UI to support the apple vpp feature on the software pages.
diff --git a/changes/issue-20612-ui-updates-host-software-device-user-pages-for-vpp b/changes/issue-20612-ui-updates-host-software-device-user-pages-for-vpp
deleted file mode 100644
index 01e6073b2d..0000000000
--- a/changes/issue-20612-ui-updates-host-software-device-user-pages-for-vpp
+++ /dev/null
@@ -1 +0,0 @@
-- add UI updates for VPP feature on host software and my device pages.
diff --git a/changes/issue-20784-fix-app-wide-banner-showing b/changes/issue-20784-fix-app-wide-banner-showing
deleted file mode 100644
index 9720e4b20b..0000000000
--- a/changes/issue-20784-fix-app-wide-banner-showing
+++ /dev/null
@@ -1 +0,0 @@
-- fix an issue where the app-wide warning banners were not showing on the initial page load
diff --git a/changes/update-go1.23.1 b/changes/update-go1.23.1
new file mode 100644
index 0000000000..22a59cdc40
--- /dev/null
+++ b/changes/update-go1.23.1
@@ -0,0 +1 @@
+* Updated Go to go1.23.1
diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml
index 6921cfc2f0..adc22108c2 100644
--- a/charts/fleet/Chart.yaml
+++ b/charts/fleet/Chart.yaml
@@ -8,7 +8,7 @@ version: v6.2.0
home: https://github.com/fleetdm/fleet
sources:
- https://github.com/fleetdm/fleet.git
-appVersion: v4.54.1
+appVersion: v4.56.0
dependencies:
- name: mysql
condition: mysql.enabled
diff --git a/charts/fleet/templates/_helpers.tpl b/charts/fleet/templates/_helpers.tpl
index f6c41ef3fb..fd0cfffbf9 100644
--- a/charts/fleet/templates/_helpers.tpl
+++ b/charts/fleet/templates/_helpers.tpl
@@ -23,6 +23,11 @@ If release name contains chart name it will be used as a full name.
{{- end }}
{{- end }}
+{{- define "fleet.servicename" -}}
+{{- $fullName := include "fleet.fullname" . -}}
+{{- printf "%s-service" $fullName }}
+{{- end }}
+
{{/*
Create chart name and version as used by the chart label.
*/}}
diff --git a/charts/fleet/templates/ingress.yaml b/charts/fleet/templates/ingress.yaml
index 0a7326d995..1a3e758a29 100644
--- a/charts/fleet/templates/ingress.yaml
+++ b/charts/fleet/templates/ingress.yaml
@@ -1,5 +1,6 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "fleet.fullname" . -}}
+{{- $serviceName := include "fleet.servicename" . -}}
{{- $svcPort := ternary .Values.fleet.listenPort .Values.fleet.servicePort (eq .Values.fleet.servicePort nil) -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
@@ -49,11 +50,11 @@ spec:
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
- name: {{ $fullName }}
+ name: {{ $serviceName }}
port:
number: {{ $svcPort }}
{{- else }}
- serviceName: {{ $fullName }}
+ serviceName: {{ $serviceName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
diff --git a/charts/fleet/templates/job-migration.yaml b/charts/fleet/templates/job-migration.yaml
index 8226e3d29a..135dac9a63 100644
--- a/charts/fleet/templates/job-migration.yaml
+++ b/charts/fleet/templates/job-migration.yaml
@@ -18,6 +18,7 @@ metadata:
"helm.sh/hook-delete-policy": hook-succeeded
{{- end }}
spec:
+ ttlSecondsAfterFinished: 100
template:
metadata:
{{- with .Values.podAnnotations }}
diff --git a/charts/fleet/templates/service.yaml b/charts/fleet/templates/service.yaml
index 1a22e48fc0..fa1c13a94d 100644
--- a/charts/fleet/templates/service.yaml
+++ b/charts/fleet/templates/service.yaml
@@ -1,3 +1,4 @@
+{{- $serviceName := include "fleet.servicename" . -}}
apiVersion: v1
kind: Service
metadata:
@@ -6,7 +7,7 @@ metadata:
chart: fleet
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
- name: fleet
+ name: {{ $serviceName }}
namespace: {{ .Release.Namespace }}
spec:
{{- if .Values.gke.ingress.useGKEIngress }}
diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml
index 0f8e570d09..040a539a83 100644
--- a/charts/fleet/values.yaml
+++ b/charts/fleet/values.yaml
@@ -3,7 +3,7 @@
hostName: fleet.localhost
replicas: 3 # The number of Fleet instances to deploy
imageRepository: fleetdm/fleet
-imageTag: v4.54.1 # Version of Fleet to deploy
+imageTag: v4.56.0 # Version of Fleet to deploy
podAnnotations: {} # Additional annotations to add to the Fleet pod
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
resources:
@@ -212,7 +212,7 @@ environments:
# The following environment variable is required if you are using
# Fleet's macOS MDM features. In a production environment, it is recommended that
# you store this private key in a secret and use envsFrom to reference the secret below.
- # To more information: https://fleetdm.com/docs/using-fleet/fleet-server-configuration#server-private-key
+ # For more information, check out the docs: https://fleetdm.com/docs/configuration/fleet-server-configuration#server-private-key
FLEET_SERVER_PRIVATE_KEY: ""
## Section: Environment Variables from Secrets/CMs
diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go
index 0cdcd66277..3b42a53567 100644
--- a/cmd/fleet/cron.go
+++ b/cmd/fleet/cron.go
@@ -11,6 +11,7 @@ import (
"strings"
"time"
+ eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
eewebhooks "github.com/fleetdm/fleet/v4/ee/server/webhooks"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/config"
@@ -28,6 +29,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/customcve"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/goval_dictionary"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/macoffice"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd"
@@ -37,7 +39,6 @@ import (
"github.com/fleetdm/fleet/v4/server/worker"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
- "github.com/google/uuid"
"github.com/hashicorp/go-multierror"
)
@@ -159,6 +160,7 @@ func scanVulnerabilities(
nvdVulns := checkNVDVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "")
ovalVulns := checkOvalVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "")
+ govalDictVulns := checkGovalDictionaryVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "")
macOfficeVulns := checkMacOfficeVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "")
customVulns := checkCustomVulnerabilities(ctx, ds, logger, config, vulnAutomationEnabled != "")
@@ -173,6 +175,7 @@ func scanVulnerabilities(
vulns = append(vulns, nvdVulns...)
vulns = append(vulns, ovalVulns...)
vulns = append(vulns, macOfficeVulns...)
+ vulns = append(vulns, govalDictVulns...)
vulns = append(vulns, customVulns...)
meta, err := ds.ListCVEs(ctx, config.RecentVulnerabilityMaxAge)
@@ -345,6 +348,11 @@ func checkOvalVulnerabilities(
for _, version := range versions.OSVersions {
start := time.Now()
r, err := oval.Analyze(ctx, ds, version, vulnPath, collectVulns)
+ if err != nil && errors.Is(err, oval.ErrUnsupportedPlatform) {
+ level.Debug(logger).Log("msg", "oval-analysis-unsupported", "platform", version.Name)
+ continue
+ }
+
elapsed := time.Since(start)
level.Debug(logger).Log(
"msg", "oval-analysis-done",
@@ -360,6 +368,57 @@ func checkOvalVulnerabilities(
return results
}
+func checkGovalDictionaryVulnerabilities(
+ ctx context.Context,
+ ds fleet.Datastore,
+ logger kitlog.Logger,
+ vulnPath string,
+ config *config.VulnerabilitiesConfig,
+ collectVulns bool,
+) []fleet.SoftwareVulnerability {
+ var results []fleet.SoftwareVulnerability
+
+ // Get Platforms
+ versions, err := ds.OSVersions(ctx, nil, nil, nil, nil)
+ if err != nil {
+ errHandler(ctx, logger, "listing platforms for goval_dictionary pulls", err)
+ return nil
+ }
+
+ if !config.DisableDataSync {
+ // Sync on disk goval_dictionary sqlite with current OS Versions.
+ downloaded, err := goval_dictionary.Refresh(versions, vulnPath, logger)
+ if err != nil {
+ errHandler(ctx, logger, "updating goval_dictionary databases", err)
+ }
+ for _, d := range downloaded {
+ level.Debug(logger).Log("goval_dictionary-sync-downloaded", d)
+ }
+ }
+
+ // Analyze all supported os versions using the synced goval_dictionary definitions.
+ for _, version := range versions.OSVersions {
+ start := time.Now()
+ r, err := goval_dictionary.Analyze(ctx, ds, version, vulnPath, collectVulns, logger)
+ if err != nil && errors.Is(err, goval_dictionary.ErrUnsupportedPlatform) {
+ level.Debug(logger).Log("msg", "goval_dictionary-analysis-unsupported", "platform", version.Name)
+ continue
+ }
+ elapsed := time.Since(start)
+ level.Debug(logger).Log(
+ "msg", "goval_dictionary-analysis-done",
+ "platform", version.Name,
+ "elapsed", elapsed,
+ "found new", len(r))
+ results = append(results, r...)
+ if err != nil {
+ errHandler(ctx, logger, "analyzing goval_dictionary definitions", err)
+ }
+ }
+
+ return results
+}
+
func checkNVDVulnerabilities(
ctx context.Context,
ds fleet.Datastore,
@@ -625,7 +684,11 @@ func newWorkerIntegrationsSchedule(
Log: logger,
Commander: commander,
}
- w.Register(jira, zendesk, macosSetupAsst, appleMDM)
+ dbMigrate := &worker.DBMigration{
+ Datastore: ds,
+ Log: logger,
+ }
+ w.Register(jira, zendesk, macosSetupAsst, appleMDM, dbMigrate)
// 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
@@ -735,6 +798,7 @@ func newCleanupsAndAggregationSchedule(
config *config.FleetConfig,
commander *apple_mdm.MDMAppleCommander,
softwareInstallStore fleet.SoftwareInstallerStore,
+ bootstrapPackageStore fleet.MDMBootstrapPackageStore,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronCleanupsThenAggregation)
@@ -882,7 +946,19 @@ func newCleanupsAndAggregationSchedule(
return ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, appConfig.ActivityExpirySettings.ActivityExpiryWindow)
}),
schedule.WithJob("cleanup_unused_software_installers", func(ctx context.Context) error {
- return ds.CleanupUnusedSoftwareInstallers(ctx, softwareInstallStore)
+ // remove only those unused created more than a minute ago to avoid a
+ // race where we delete those created after the mysql query to get those
+ // in use.
+ return ds.CleanupUnusedSoftwareInstallers(ctx, softwareInstallStore, time.Now().Add(-time.Minute))
+ }),
+ schedule.WithJob("cleanup_unused_bootstrap_packages", func(ctx context.Context) error {
+ // remove only those unused created more than a minute ago to avoid a
+ // race where we delete those created after the mysql query to get those
+ // in use.
+ return ds.CleanupUnusedBootstrapPackages(ctx, bootstrapPackageStore, time.Now().Add(-time.Minute))
+ }),
+ schedule.WithJob("cleanup_host_mdm_commands", func(ctx context.Context) error {
+ return ds.CleanupHostMDMCommands(ctx)
}),
)
@@ -936,7 +1012,6 @@ func verifyDiskEncryptionKeys(
logger kitlog.Logger,
ds fleet.Datastore,
) error {
-
appCfg, err := ds.AppConfig(ctx)
if err != nil {
logger.Log("err", "unable to get app config", "details", err)
@@ -1050,31 +1125,59 @@ func newAppleMDMDEPProfileAssigner(
) (*schedule.Schedule, error) {
const name = string(fleet.CronAppleMDMDEPProfileAssigner)
logger = kitlog.With(logger, "cron", name, "component", "nanodep-syncer")
- var fleetSyncer *apple_mdm.DEPService
s := schedule.New(
ctx, name, instanceID, periodicity, ds, ds,
schedule.WithLogger(logger),
- schedule.WithJob("dep_syncer", func(ctx context.Context) error {
- appCfg, err := ds.AppConfig(ctx)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "retrieving app config")
- }
-
- if !appCfg.MDM.AppleBMEnabledAndConfigured {
- return nil
- }
-
- if fleetSyncer == nil {
- fleetSyncer = apple_mdm.NewDEPService(ds, depStorage, logger)
- }
-
- return fleetSyncer.RunAssigner(ctx)
- }),
+ schedule.WithJob("dep_syncer", appleMDMDEPSyncerJob(ds, depStorage, logger)),
)
return s, nil
}
+func appleMDMDEPSyncerJob(
+ ds fleet.Datastore,
+ depStorage *mysql.NanoDEPStorage,
+ logger kitlog.Logger,
+) func(context.Context) error {
+ var fleetSyncer *apple_mdm.DEPService
+ return func(ctx context.Context) error {
+ appCfg, err := ds.AppConfig(ctx)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "retrieving app config")
+ }
+
+ if !appCfg.MDM.AppleBMEnabledAndConfigured {
+ return nil
+ }
+
+ // As part of the DB migration of the single ABM token to the multi-ABM
+ // token world (where the token was migrated from mdm_config_assets to
+ // abm_tokens), we need to complete migration of the existing token as
+ // during the DB migration we didn't have the organization name, apple id
+ // and renewal date.
+ incompleteToken, err := ds.GetABMTokenByOrgName(ctx, "")
+ if err != nil && !fleet.IsNotFound(err) {
+ return ctxerr.Wrap(ctx, err, "retrieving migrated ABM token")
+ }
+ if incompleteToken != nil {
+ logger.Log("msg", "migrated ABM token found, updating its metadata")
+ if err := apple_mdm.SetABMTokenMetadata(ctx, incompleteToken, depStorage, ds, logger); err != nil {
+ return ctxerr.Wrap(ctx, err, "updating migrated ABM token metadata")
+ }
+ if err := ds.SaveABMToken(ctx, incompleteToken); err != nil {
+ return ctxerr.Wrap(ctx, err, "saving updated migrated ABM token")
+ }
+ logger.Log("msg", "completed migration of existing ABM token")
+ }
+
+ if fleetSyncer == nil {
+ fleetSyncer = apple_mdm.NewDEPService(ds, depStorage, logger)
+ }
+
+ return fleetSyncer.RunAssigner(ctx)
+ }
+}
+
func newMDMProfileManager(
ctx context.Context,
instanceID string,
@@ -1115,17 +1218,16 @@ func newMDMAPNsPusher(
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) (*schedule.Schedule, error) {
-
const name = string(fleet.CronAppleMDMAPNsPusher)
- var interval = 1 * time.Minute
+ interval := 1 * time.Minute
if intervalEnv := os.Getenv("FLEET_DEV_CUSTOM_APNS_PUSHER_INTERVAL"); intervalEnv != "" {
var err error
interval, err = time.ParseDuration(intervalEnv)
if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "invalid duration provided in env var FLEET_DEV_CUSTOM_APNS_PUSHER_INTERVAL")
+ level.Warn(logger).Log("msg", "invalid duration provided for FLEET_DEV_CUSTOM_APNS_PUSHER_INTERVAL, using default interval")
+ interval = 1 * time.Minute
}
-
}
logger = kitlog.With(logger, "cron", name)
@@ -1285,37 +1387,34 @@ func newIPhoneIPadRefetcher(
ctx, name, instanceID, periodicity, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("cron_iphone_ipad_refetcher", func(ctx context.Context) error {
- appCfg, err := ds.AppConfig(ctx)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "fetching app config")
- }
-
- if !appCfg.MDM.EnabledAndConfigured {
- level.Debug(logger).Log("msg", "apple mdm is not configured, skipping run")
- return nil
- }
-
- start := time.Now()
- uuids, err := ds.ListIOSAndIPadOSToRefetch(ctx, 1*time.Hour)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "list ios and ipad devices to refetch")
- }
- if len(uuids) == 0 {
- return nil
- }
- logger.Log("msg", "sending commands to refetch", "count", len(uuids), "lookup-duration", time.Since(start))
- commandUUID := fleet.RefetchCommandUUIDPrefix + uuid.NewString()
- err = commander.InstalledApplicationList(ctx, uuids, fleet.RefetchAppsCommandUUIDPrefix+commandUUID)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "send InstalledApplicationList commands to ios and ipados devices")
- }
- // DeviceInformation is last because the refetch response clears the refetch_requested flag
- if err := commander.DeviceInformation(ctx, uuids, fleet.RefetchCommandUUIDPrefix+commandUUID); err != nil {
- return ctxerr.Wrap(ctx, err, "send DeviceInformation commands to ios and ipados devices")
- }
- return nil
+ return apple_mdm.IOSiPadOSRefetch(ctx, ds, commander, logger)
}),
)
return s, nil
}
+
+// cronUninstallSoftwareMigration will update uninstall scripts for software.
+// Once all customers are using on Fleet 4.57 or later, this job can be removed.
+func cronUninstallSoftwareMigration(
+ ctx context.Context,
+ instanceID string,
+ ds fleet.Datastore,
+ softwareInstallStore fleet.SoftwareInstallerStore,
+ logger kitlog.Logger,
+) (*schedule.Schedule, error) {
+ const (
+ name = string(fleet.CronUninstallSoftwareMigration)
+ defaultInterval = 24 * time.Hour
+ )
+ logger = kitlog.With(logger, "cron", name, "component", name)
+ s := schedule.New(
+ ctx, name, instanceID, defaultInterval, ds, ds,
+ schedule.WithLogger(logger),
+ schedule.WithRunOnce(true),
+ schedule.WithJob(name, func(ctx context.Context) error {
+ return eeservice.UninstallSoftwareMigration(ctx, ds, softwareInstallStore, logger)
+ }),
+ )
+ return s, nil
+}
diff --git a/cmd/fleet/cron_test.go b/cmd/fleet/cron_test.go
index dd31dd2bec..789b38c405 100644
--- a/cmd/fleet/cron_test.go
+++ b/cmd/fleet/cron_test.go
@@ -2,13 +2,24 @@ package main
import (
"context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
+ "github.com/fleetdm/fleet/v4/server/datastore/mysql"
+ "github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
+ "github.com/fleetdm/fleet/v4/server/test"
+ "github.com/go-kit/log"
kitlog "github.com/go-kit/log"
)
@@ -23,3 +34,93 @@ func TestNewMDMProfileManagerWithoutConfig(t *testing.T) {
require.NotNil(t, sch)
require.NoError(t, err)
}
+
+func TestMigrateABMTokenDuringDEPCronJob(t *testing.T) {
+ ctx := context.Background()
+ ds := mysql.CreateMySQLDS(t)
+
+ depStorage, err := ds.NewMDMAppleDEPStorage()
+ require.NoError(t, err)
+ // to avoid issues with syncer, use that constant as org name for now
+ const tokenOrgName = "fleet"
+
+ // insert an ABM token as if it had been migrated by the DB migration script
+ tok := mysql.SetTestABMAssets(t, ds, "")
+ // tok, err := ds.InsertABMToken(ctx, &fleet.ABMToken{EncryptedToken: abmToken, RenewAt: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)})
+ // require.NoError(t, err)
+ require.Empty(t, tok.OrganizationName)
+
+ // start a server that will mock the Apple DEP API
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ encoder := json.NewEncoder(w)
+ switch r.URL.Path {
+ case "/session":
+ _, _ = w.Write([]byte(`{"auth_session_token": "session123"}`))
+ case "/account":
+ _, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "admin123", "org_name": "%s"}`, tokenOrgName)))
+ case "/profile":
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"})
+ require.NoError(t, err)
+ case "/server/devices":
+ err := encoder.Encode(godep.DeviceResponse{Devices: nil})
+ require.NoError(t, err)
+ case "/devices/sync":
+ err := encoder.Encode(godep.DeviceResponse{Devices: nil})
+ require.NoError(t, err)
+ default:
+ t.Errorf("unexpected request to %s", r.URL.Path)
+ }
+ }))
+ t.Cleanup(srv.Close)
+
+ err = depStorage.StoreConfig(ctx, tokenOrgName, &nanodep_client.Config{BaseURL: srv.URL})
+ require.NoError(t, err)
+ err = depStorage.StoreConfig(ctx, apple_mdm.UnsavedABMTokenOrgName, &nanodep_client.Config{BaseURL: srv.URL})
+ require.NoError(t, err)
+
+ logger := log.NewNopLogger()
+ syncFn := appleMDMDEPSyncerJob(ds, depStorage, logger)
+ err = syncFn(ctx)
+ require.NoError(t, err)
+
+ // token has been updated with its org name/apple id
+ tok, err = ds.GetABMTokenByOrgName(ctx, tokenOrgName)
+ require.NoError(t, err)
+ require.Equal(t, tokenOrgName, tok.OrganizationName)
+ require.Equal(t, "admin123", tok.AppleID)
+ require.Nil(t, tok.MacOSDefaultTeamID)
+ require.Nil(t, tok.IOSDefaultTeamID)
+ require.Nil(t, tok.IPadOSDefaultTeamID)
+
+ // empty-name token does not exist anymore
+ _, err = ds.GetABMTokenByOrgName(ctx, "")
+ require.Error(t, err)
+ var nfe fleet.NotFoundError
+ require.ErrorAs(t, err, &nfe)
+
+ // the default profile was created
+ defProf, err := ds.GetMDMAppleEnrollmentProfileByType(ctx, fleet.MDMAppleEnrollmentTypeAutomatic)
+ require.NoError(t, err)
+ require.NotNil(t, defProf)
+ require.NotEmpty(t, defProf.Token)
+
+ // no profile UUID was assigned for no-team (because there are no hosts right now)
+ _, _, err = ds.GetMDMAppleDefaultSetupAssistant(ctx, nil, "")
+ require.Error(t, err)
+ require.ErrorAs(t, err, &nfe)
+
+ // no teams, so no team-specific custom setup assistants
+ teams, err := ds.ListTeams(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Empty(t, teams)
+
+ // no no-team custom setup assistant
+ _, err = ds.GetMDMAppleSetupAssistant(ctx, nil)
+ require.ErrorIs(t, err, sql.ErrNoRows)
+
+ // no host got created
+ hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{})
+ require.NoError(t, err)
+ require.Empty(t, hosts)
+}
diff --git a/cmd/fleet/main.go b/cmd/fleet/main.go
index eff64d3562..b39cbc2ee9 100644
--- a/cmd/fleet/main.go
+++ b/cmd/fleet/main.go
@@ -2,11 +2,14 @@ package main
import (
"fmt"
+ "io"
"math/rand"
"os"
"time"
+ "github.com/briandowns/spinner"
"github.com/fleetdm/fleet/v4/server/config"
+ "github.com/fleetdm/fleet/v4/server/shellquote"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
_ "github.com/go-sql-driver/mysql"
@@ -29,6 +32,35 @@ func main() {
rootCmd.AddCommand(createConfigDumpCmd(configManager))
rootCmd.AddCommand(createVersionCmd(configManager))
+ // See if the program is being piped data on stdin.
+ fi, err := os.Stdin.Stat()
+ if err != nil {
+ initFatal(err, "getting stdin stats")
+ }
+ if fi.Mode()&os.ModeNamedPipe != 0 {
+ _, _ = fmt.Fprintln(os.Stderr, "Reading additional arguments from stdin...")
+ // See charsets at https://godoc.org/github.com/briandowns/spinner#pkg-variables
+ s := spinner.New(spinner.CharSets[24], 200*time.Millisecond)
+ s.Writer = os.Stderr
+ s.Start()
+
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ initFatal(err, "reading from stdin")
+ }
+
+ // Split the string into arguments like a shell would.
+ extraArgs, err := shellquote.Split(string(data))
+ if err != nil {
+ initFatal(err, "splitting arguments from stdin")
+ }
+
+ // Add the new args to the existing args
+ os.Args = append(os.Args, extraArgs...)
+
+ s.Stop()
+ }
+
if err := rootCmd.Execute(); err != nil {
initFatal(err, "running root command")
}
diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go
index 5c8a3db664..eda0660a73 100644
--- a/cmd/fleet/serve.go
+++ b/cmd/fleet/serve.go
@@ -22,7 +22,6 @@ import (
"github.com/e-dard/netbug"
"github.com/fleetdm/fleet/v4/ee/server/licensing"
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
- "github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server"
configpkg "github.com/fleetdm/fleet/v4/server/config"
@@ -50,6 +49,7 @@ import (
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/async"
+ "github.com/fleetdm/fleet/v4/server/service/redis_key_value"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
"github.com/fleetdm/fleet/v4/server/sso"
@@ -75,7 +75,10 @@ import (
var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$")
-const softwareInstallerUploadTimeout = 4 * time.Minute
+const (
+ softwareInstallerUploadTimeout = 4 * time.Minute
+ liveQueryMemCacheDuration = 1 * time.Second
+)
type initializer interface {
// Initialize is used to populate a datastore with
@@ -125,6 +128,10 @@ the way that the Fleet server works.
logger := initLogger(config)
+ if dev {
+ createTestBucketForInstallers(&config, logger)
+ }
+
// Init tracing
if config.Logging.TracingEnabled {
ctx := context.Background()
@@ -346,7 +353,7 @@ the way that the Fleet server works.
resultStore := pubsub.NewRedisQueryResults(redisPool, config.Redis.DuplicateResults,
log.With(logger, "component", "query-results"),
)
- liveQueryStore := live_query.NewRedisLiveQuery(redisPool)
+ liveQueryStore := live_query.NewRedisLiveQuery(redisPool, logger, liveQueryMemCacheDuration)
ssoSessionStore := sso.NewSessionStore(redisPool)
// Set common configuration for all logging.
@@ -493,97 +500,6 @@ the way that the Fleet server works.
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
}
- // validate Apple APNs/SCEP config
- if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() {
- if !config.MDM.IsAppleAPNsSet() {
- initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), "validate Apple MDM")
- } else if !config.MDM.IsAppleSCEPSet() {
- initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), "validate Apple MDM")
- }
-
- if len(config.Server.PrivateKey) == 0 {
- initFatal(errors.New("inserting APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
- }
-
- apnsCert, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs()
- if err != nil {
- initFatal(err, "validate Apple APNs certificate and key")
- }
-
- _, appleSCEPCertPEM, appleSCEPKeyPEM, err := config.MDM.AppleSCEP()
- if err != nil {
- initFatal(err, "validate Apple SCEP certificate and key")
- }
-
- const (
- apnsConnectionTimeout = 10 * time.Second
- apnsConnectionURL = "https://api.sandbox.push.apple.com"
- )
-
- // check that the Apple APNs certificate is valid to connect to the API
- ctx, cancel := context.WithTimeout(context.Background(), apnsConnectionTimeout)
- if err := certificate.ValidateClientAuthTLSConnection(ctx, apnsCert, apnsConnectionURL); err != nil {
- initFatal(err, "validate authentication with Apple APNs certificate")
- }
- cancel()
-
- err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{
- {Name: fleet.MDMAssetAPNSCert, Value: apnsCertPEM},
- {Name: fleet.MDMAssetAPNSKey, Value: apnsKeyPEM},
- {Name: fleet.MDMAssetCACert, Value: appleSCEPCertPEM},
- {Name: fleet.MDMAssetCAKey, Value: appleSCEPKeyPEM},
- })
- if err != nil {
- // duplicate key errors mean that we already
- // have a value for those keys in the
- // database, fail to initalize on other
- // cases.
- if !mysql.IsDuplicate(err) {
- initFatal(err, "inserting MDM APNs and SCEP assets")
- }
-
- level.Warn(logger).Log("msg", "Your server already has stored SCEP and APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.")
- }
- }
-
- // validate Apple BM config
- if config.MDM.IsAppleBMSet() {
- if !license.IsPremium() {
- initFatal(errors.New("Apple Business Manager configuration is only available in Fleet Premium"), "validate Apple BM")
- }
-
- if len(config.Server.PrivateKey) == 0 {
- initFatal(errors.New("inserting MDM ABM assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
- }
-
- appleBM, err := config.MDM.AppleBM()
- if err != nil {
- initFatal(err, "validate Apple BM token, certificate and key")
- }
-
- err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{
- {Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM},
- {Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM},
- {Name: fleet.MDMAssetABMToken, Value: appleBM.EncryptedToken},
- })
- if err != nil {
- // duplicate key errors mean that we already
- // have a value for those keys in the
- // database, fail to initalize on other
- // cases.
- if !mysql.IsDuplicate(err) {
- initFatal(err, "inserting MDM ABM assets")
- }
-
- level.Warn(logger).Log("msg", "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.")
- }
- }
-
- appCfg, err := ds.AppConfig(context.Background())
- if err != nil {
- initFatal(err, "loading app config")
- }
-
checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) {
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names)
if err != nil {
@@ -595,6 +511,131 @@ the way that the Fleet server works.
return true, nil
}
+ // reconcile Apple Business Manager configuration environment variables with the database
+ if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() {
+ if len(config.Server.PrivateKey) == 0 {
+ initFatal(errors.New("inserting MDM APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
+
+ // first we'll check if the APNs and SCEP assets are already in the database and
+ // only insert config values if they're not already present in the database
+ toInsert := make(map[fleet.MDMAssetName]struct{}, 4)
+
+ // check DB for APNs assets
+ found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetAPNSCert, fleet.MDMAssetAPNSKey})
+ switch {
+ case err != nil:
+ initFatal(err, "reading APNs assets from database")
+ case !found:
+ toInsert[fleet.MDMAssetAPNSCert] = struct{}{}
+ toInsert[fleet.MDMAssetAPNSKey] = struct{}{}
+ default:
+ level.Warn(logger).Log("msg", "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.")
+ }
+
+ // check DB for SCEP assets
+ found, err = checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey})
+ switch {
+ case err != nil:
+ initFatal(err, "reading SCEP assets from database")
+ case !found:
+ toInsert[fleet.MDMAssetCACert] = struct{}{}
+ toInsert[fleet.MDMAssetCAKey] = struct{}{}
+ default:
+ level.Warn(logger).Log("msg", "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.")
+ }
+
+ if len(toInsert) > 0 {
+ if !config.MDM.IsAppleAPNsSet() {
+ initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), "validate Apple MDM")
+ } else if !config.MDM.IsAppleSCEPSet() {
+ initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), "validate Apple MDM")
+ }
+
+ // parse the APNs and SCEP assets from the config
+ _, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs()
+ if err != nil {
+ initFatal(err, "parse Apple APNs certificate and key from config")
+ }
+ _, appleSCEPCertPEM, appleSCEPKeyPEM, err := config.MDM.AppleSCEP()
+ if err != nil {
+ initFatal(err, "load Apple SCEP certificate and key from config")
+ }
+
+ var args []fleet.MDMConfigAsset
+ for name := range toInsert {
+ switch name {
+ case fleet.MDMAssetAPNSCert:
+ args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsCertPEM})
+ case fleet.MDMAssetAPNSKey:
+ args = append(args, fleet.MDMConfigAsset{Name: name, Value: apnsKeyPEM})
+ case fleet.MDMAssetCACert:
+ args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPCertPEM})
+ case fleet.MDMAssetCAKey:
+ args = append(args, fleet.MDMConfigAsset{Name: name, Value: appleSCEPKeyPEM})
+ }
+ }
+
+ if err := ds.InsertMDMConfigAssets(context.Background(), args); err != nil {
+ if mysql.IsDuplicate(err) {
+ // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case
+ level.Debug(logger).Log("msg", "unexpected duplicate key error inserting MDM APNs and SCEP assets")
+ } else {
+ initFatal(err, "inserting MDM APNs and SCEP assets")
+ }
+ }
+ }
+ }
+
+ // reconcile Apple Business Manager configuration environment variables with the database
+ if config.MDM.IsAppleBMSet() {
+ if len(config.Server.PrivateKey) == 0 {
+ initFatal(errors.New("inserting MDM ABM assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
+
+ appleBM, err := config.MDM.AppleBM()
+ if err != nil {
+ initFatal(err, "parse Apple BM token, certificate and key from config")
+ }
+
+ toInsert := make([]fleet.MDMConfigAsset, 0, 2)
+
+ found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetABMKey, fleet.MDMAssetABMCert})
+ switch {
+ case err != nil:
+ initFatal(err, "reading ABM assets from database")
+ case !found:
+ toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM})
+ default:
+ level.Warn(logger).Log("msg", "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.")
+ }
+
+ if len(toInsert) > 0 {
+ err := ds.InsertMDMConfigAssets(context.Background(), toInsert)
+ switch {
+ case err != nil && mysql.IsDuplicate(err):
+ // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case
+ level.Debug(logger).Log("msg", "unexpected duplicate key error inserting ABM assets")
+ case err != nil:
+ initFatal(err, "inserting ABM assets")
+ default:
+ // insert the ABM token without any metdata; it'll be picked by the
+ // apple_mdm_dep_profile_assigner cron and backfilled
+ if _, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{
+ EncryptedToken: appleBM.EncryptedToken,
+ RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), // 2000-01-01 is our "zero value" for time
+ }); err != nil {
+ initFatal(err, "save ABM token")
+ }
+ }
+ }
+ }
+
+ appCfg, err := ds.AppConfig(context.Background())
+ if err != nil {
+ initFatal(err, "loading app config")
+ }
+
appCfg.MDM.EnabledAndConfigured = false
appCfg.MDM.AppleBMEnabledAndConfigured = false
if len(config.Server.PrivateKey) > 0 {
@@ -605,17 +646,32 @@ the way that the Fleet server works.
fleet.MDMAssetAPNSCert,
})
if err != nil {
- initFatal(err, "validating MDM assets from database")
+ initFatal(err, "loading MDM assets from database")
}
- appCfg.MDM.AppleBMEnabledAndConfigured, err = checkMDMAssets([]fleet.MDMAssetName{
+ var appleBMCerts bool
+ appleBMCerts, err = checkMDMAssets([]fleet.MDMAssetName{
fleet.MDMAssetABMCert,
fleet.MDMAssetABMKey,
- fleet.MDMAssetABMToken,
})
if err != nil {
- initFatal(err, "validating MDM ABM assets from database")
+ initFatal(err, "loading MDM ABM assets from database")
}
+ if appleBMCerts {
+ // the ABM certs are there, check if a token exists and if so, apple
+ // BM is enabled and configured.
+ count, err := ds.GetABMTokenCount(context.Background())
+ if err != nil {
+ initFatal(err, "loading MDM ABM token from database")
+ }
+ appCfg.MDM.AppleBMEnabledAndConfigured = count > 0
+ }
+ }
+ if appCfg.MDM.EnabledAndConfigured {
+ level.Info(logger).Log("msg", "Apple MDM enabled")
+ }
+ if appCfg.MDM.AppleBMEnabledAndConfigured {
+ level.Info(logger).Log("msg", "Apple Business Manager enabled")
}
// register the Microsoft MDM services
@@ -692,6 +748,7 @@ the way that the Fleet server works.
}
var softwareInstallStore fleet.SoftwareInstallerStore
+ var bootstrapPackageStore fleet.MDMBootstrapPackageStore
var distributedLock fleet.Lock
if license.IsPremium() {
profileMatcher := apple_mdm.NewProfileMatcher(redisPool)
@@ -705,6 +762,13 @@ the way that the Fleet server works.
}
softwareInstallStore = store
level.Info(logger).Log("msg", "using S3 software installer store", "bucket", config.S3.SoftwareInstallersBucket)
+
+ bstore, err := s3.NewBootstrapPackageStore(config.S3)
+ if err != nil {
+ initFatal(err, "initializing S3 bootstrap package store")
+ }
+ bootstrapPackageStore = bstore
+ level.Info(logger).Log("msg", "using S3 bootstrap package store", "bucket", config.S3.SoftwareInstallersBucket)
} else {
installerDir := os.TempDir()
if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" {
@@ -733,7 +797,9 @@ the way that the Fleet server works.
ssoSessionStore,
profileMatcher,
softwareInstallStore,
+ bootstrapPackageStore,
distributedLock,
+ redis_key_value.New(redisPool),
)
if err != nil {
initFatal(err, "initial Fleet Premium service")
@@ -773,6 +839,16 @@ the way that the Fleet server works.
}
}()
+ if softwareInstallStore != nil {
+ if err := cronSchedules.StartCronSchedule(
+ func() (fleet.CronSchedule, error) {
+ return cronUninstallSoftwareMigration(ctx, instanceID, ds, softwareInstallStore, logger)
+ },
+ ); err != nil {
+ initFatal(err, fmt.Sprintf("failed to register %s", fleet.CronUninstallSoftwareMigration))
+ }
+ }
+
if config.Server.FrequentCleanupsEnabled {
if err := cronSchedules.StartCronSchedule(
func() (fleet.CronSchedule, error) {
@@ -787,7 +863,7 @@ the way that the Fleet server works.
func() (fleet.CronSchedule, error) {
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
return newCleanupsAndAggregationSchedule(
- ctx, instanceID, ds, logger, redisWrapperDS, &config, commander, softwareInstallStore,
+ ctx, instanceID, ds, logger, redisWrapperDS, &config, commander, softwareInstallStore, bootstrapPackageStore,
)
},
); err != nil {
@@ -936,7 +1012,7 @@ the way that the Fleet server works.
KeyPrefix: "ratelimit::",
}
- var apiHandler, frontendHandler http.Handler
+ var apiHandler, frontendHandler, endUserEnrollOTAHandler http.Handler
{
frontendHandler = service.PrometheusMetricsHandler(
"get_frontend",
@@ -954,8 +1030,10 @@ the way that the Fleet server works.
if setupRequired {
apiHandler = service.WithSetup(svc, logger, apiHandler)
frontendHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix)
+ endUserEnrollOTAHandler = service.RedirectLoginToSetup(svc, logger, frontendHandler, config.Server.URLPrefix)
} else {
frontendHandler = service.RedirectSetupToLogin(svc, logger, frontendHandler, config.Server.URLPrefix)
+ endUserEnrollOTAHandler = service.ServeEndUserEnrollOTA(config.Server.URLPrefix, logger)
}
}
@@ -1090,6 +1168,7 @@ the way that the Fleet server works.
}
apiHandler.ServeHTTP(rw, req)
})
+ rootMux.Handle("/enroll", endUserEnrollOTAHandler)
rootMux.Handle("/", frontendHandler)
debugHandler := &debugMux{
@@ -1131,7 +1210,11 @@ the way that the Fleet server works.
rootMux = prefixMux
}
- liveQueryRestPeriod := 90 * time.Second // default (see #1798)
+ // NOTE(lucas): It seems we missed updating this value from 90s (see #1798) to 25s after we
+ // decided to make the synchronous live query API to take up to 25 seconds.
+ // Not changing this to not break any long running requests (like when uploading software
+ // packages via GitOps).
+ liveQueryRestPeriod := 90 * time.Second
if v := os.Getenv("FLEET_LIVE_QUERY_REST_PERIOD"); v != "" {
duration, err := time.ParseDuration(v)
if err != nil {
@@ -1374,3 +1457,18 @@ var _ push.Pusher = nopPusher{}
func (n nopPusher) Push(context.Context, []string) (map[string]*push.Response, error) {
return nil, nil
}
+
+func createTestBucketForInstallers(config *configpkg.FleetConfig, logger log.Logger) {
+ store, err := s3.NewSoftwareInstallerStore(config.S3)
+ if err != nil {
+ initFatal(err, "initializing S3 software installer store")
+ }
+ if err := store.CreateTestBucket(config.S3.SoftwareInstallersBucket); err != nil {
+ // Don't panic, allow devs to run Fleet without minio/S3 dependency.
+ level.Info(logger).Log(
+ "err", err,
+ "msg", "failed to create test bucket",
+ "name", config.S3.SoftwareInstallersBucket,
+ )
+ }
+}
diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go
index 41e7d3885a..e472566f3e 100644
--- a/cmd/fleet/serve_test.go
+++ b/cmd/fleet/serve_test.go
@@ -30,9 +30,9 @@ import (
"github.com/go-kit/log"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
+ "github.com/smallstep/pkcs7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "go.mozilla.org/pkcs7"
)
// safeStore is a wrapper around mock.Store to allow for concurrent calling to
@@ -116,6 +116,10 @@ func TestMaybeSendStatistics(t *testing.T) {
HostsEnrolledByOsqueryVersion: []fleet.HostsCountByOsqueryVersion{},
StoredErrors: []byte(`[]`),
Organization: "Fleet",
+ AIFeaturesDisabled: true,
+ MaintenanceWindowsEnabled: true,
+ MaintenanceWindowsConfigured: true,
+ NumHostsFleetDesktopEnabled: 1984,
}, true, nil
}
recorded := false
@@ -134,7 +138,7 @@ func TestMaybeSendStatistics(t *testing.T) {
require.NoError(t, err)
assert.True(t, recorded)
require.True(t, cleanedup)
- assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0}`, requestBody)
+ assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numSoftwareVersions":100,"numHostSoftwares":101,"numSoftwareTitles":102,"numHostSoftwareInstalledPaths":103,"numSoftwareCPEs":104,"numSoftwareCVEs":105,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"mdmMacOsEnabled":false,"hostExpiryEnabled":false,"mdmWindowsEnabled":false,"liveQueryDisabled":false,"numWeeklyActiveUsers":111,"numWeeklyPolicyViolationDaysActual":0,"numWeeklyPolicyViolationDaysPossible":0,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"hostsEnrolledByOrbitVersion":[],"hostsEnrolledByOsqueryVersion":[],"storedErrors":[],"numHostsNotResponding":0,"aiFeaturesDisabled":true,"maintenanceWindowsEnabled":true,"maintenanceWindowsConfigured":true,"numHostsFleetDesktopEnabled":1984}`, requestBody)
}
func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) {
@@ -294,6 +298,7 @@ func TestAutomationsSchedule(t *testing.T) {
}
func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) {
+ t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
diff --git a/cmd/fleetctl/apply.go b/cmd/fleetctl/apply.go
index 751c74709f..b201f5dc94 100644
--- a/cmd/fleetctl/apply.go
+++ b/cmd/fleetctl/apply.go
@@ -90,7 +90,7 @@ func applyCommand() *cli.Command {
opts.TeamForPolicies = policiesTeamName
}
baseDir := filepath.Dir(flFilename)
- _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, opts)
+ _, _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
if err != nil {
return err
}
diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go
index 3cd8eb03dd..f35b39dc84 100644
--- a/cmd/fleetctl/apply_test.go
+++ b/cmd/fleetctl/apply_test.go
@@ -23,9 +23,11 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
"github.com/fleetdm/fleet/v4/server/mock"
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
+ nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
@@ -167,12 +169,15 @@ func TestApplyTeamSpecs(t *testing.T) {
return nil
}
- ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
- return nil
+ ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
+ winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.NewActivityFunc = func(
@@ -586,6 +591,10 @@ func TestApplyAppConfig(t *testing.T) {
return userRoleSpecList, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
) error {
@@ -623,8 +632,9 @@ func TestApplyAppConfig(t *testing.T) {
return nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
@@ -640,6 +650,12 @@ func TestApplyAppConfig(t *testing.T) {
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
+ }
+ ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
+ return []*fleet.TeamSummary{{Name: "team1", ID: 1}}, nil
+ }
name := writeTmpYml(t, `---
apiVersion: v1
@@ -659,8 +675,8 @@ spec:
`)
newMDMSettings := fleet.MDM{
- AppleBMDefaultTeam: "team1",
- AppleBMTermsExpired: false,
+ DeprecatedAppleBMDefaultTeam: "team1",
+ AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("12.1.1"),
Deadline: optjson.SetString("2011-02-01"),
@@ -714,8 +730,8 @@ spec:
`)
newMDMSettings = fleet.MDM{
- AppleBMDefaultTeam: "team1",
- AppleBMTermsExpired: false,
+ DeprecatedAppleBMDefaultTeam: "team1",
+ AppleBMTermsExpired: false,
MacOSUpdates: fleet.AppleOSUpdateSettings{
MinimumVersion: optjson.SetString("12.1.1"),
Deadline: optjson.SetString("2011-02-01"),
@@ -1157,6 +1173,9 @@ func TestApplyAsGitOps(t *testing.T) {
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
+ // Mock Apple DEP API
+ depStorage := SetupMockDEPStorageAndMockDEPServer(t)
+
config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../server/service/testdata")
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
@@ -1164,6 +1183,7 @@ func TestApplyAsGitOps(t *testing.T) {
MDMStorage: enqueuer,
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
+ DEPStorage: depStorage,
})
gitOps := &fleet.User{
@@ -1240,11 +1260,14 @@ func TestApplyAsGitOps(t *testing.T) {
teamEnrollSecrets = secrets
return nil
}
- ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
- return nil
+ ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
+ winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
return nil, ¬FoundError{}
@@ -1258,7 +1281,7 @@ func TestApplyAsGitOps(t *testing.T) {
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, ¬FoundError{}
}
- ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
+ ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
return nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
@@ -1279,6 +1302,20 @@ func TestApplyAsGitOps(t *testing.T) {
return nil
}
+ ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
+ return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
+ }
+ ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
+ return 0, nil
+ }
+
+ ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
+ return []string{"foobar"}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{ID: 1}}, nil
+ }
+
// Apply global config.
name := writeTmpYml(t, `---
apiVersion: v1
@@ -1615,6 +1652,34 @@ spec:
assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query)
}
+func SetupMockDEPStorageAndMockDEPServer(t *testing.T) *nanodep_mock.Storage {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case strings.Contains(r.URL.Path, "/server/devices"):
+ _, err := w.Write([]byte("{}"))
+ require.NoError(t, err)
+ case strings.Contains(r.URL.Path, "/session"):
+ _, err := w.Write([]byte(`{"auth_session_token": "yoo"}`))
+ require.NoError(t, err)
+ case strings.Contains(r.URL.Path, "/profile"):
+ _, err := w.Write([]byte(`{"profile_uuid": "profile123"}`))
+ require.NoError(t, err)
+ }
+ }))
+ depStorage := &nanodep_mock.Storage{}
+ depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) {
+ return &nanodep_client.Config{
+ BaseURL: ts.URL,
+ }, nil
+ }
+ depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) {
+ return &nanodep_client.OAuth1Tokens{}, nil
+ }
+ t.Cleanup(func() { ts.Close() })
+
+ return depStorage
+}
+
func TestApplyEnrollSecrets(t *testing.T) {
_, ds := runServerWithMockedDS(t)
@@ -1868,7 +1933,8 @@ func TestApplyMacosSetup(t *testing.T) {
tier = fleet.TierPremium
}
license := &fleet.LicenseInfo{Tier: tier, Expiration: time.Now().Add(24 * time.Hour)}
- _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
+ depStorage := SetupMockDEPStorageAndMockDEPServer(t)
+ _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license, DEPStorage: depStorage})
tm1 := &fleet.Team{ID: 1, Name: "tm1"}
teamsByName := map[string]*fleet.Team{
@@ -2001,7 +2067,7 @@ func TestApplyMacosSetup(t *testing.T) {
}
return nil, ¬FoundError{}
}
- ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
+ ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
return nil
}
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
@@ -2010,6 +2076,21 @@ func TestApplyMacosSetup(t *testing.T) {
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return nil, nil
}
+
+ ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
+ return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil
+ }
+ ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
+ return 0, nil
+ }
+
+ ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
+ return []string{"foobar"}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{ID: 1}}, nil
+ }
+
return ds
}
@@ -2387,7 +2468,7 @@ spec:
t.Run(c.pkgName, func(t *testing.T) {
srv, pkgLen := serveMDMBootstrapPackage(t, filepath.Join("../../server/service/testdata/bootstrap-packages", c.pkgName), c.pkgName)
ds := setupServer(t, true)
- ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
+ ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
require.Equal(t, len(bp.Bytes), pkgLen)
return nil
}
@@ -2446,7 +2527,7 @@ spec:
defer srv.Close()
ds := setupServer(t, true)
- ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage) error {
+ ds.InsertMDMAppleBootstrapPackageFunc = func(ctx context.Context, bp *fleet.MDMAppleBootstrapPackage, pkgStore fleet.MDMBootstrapPackageStore) error {
mockStore.Lock()
defer mockStore.Unlock()
require.Equal(t, pkgName, bp.Name)
@@ -2599,6 +2680,7 @@ spec:
}
func TestApplySpecs(t *testing.T) {
+ t.Parallel()
// create a macos setup json file (content not important)
macSetupFile := writeTmpJSON(t, map[string]any{})
diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go
index db5a00c9b2..f39ff1cd55 100644
--- a/cmd/fleetctl/get_test.go
+++ b/cmd/fleetctl/get_test.go
@@ -2060,8 +2060,13 @@ func TestGetAppleBM(t *testing.T) {
assert.Contains(t, err.Error(), expected)
})
- t.Run("premium license", func(t *testing.T) {
- runServerWithMockedDS(t, &service.TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, DEPStorage: depStorage})
+ t.Run("premium license, single token", func(t *testing.T) {
+ _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, DEPStorage: depStorage})
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{
+ {ID: 1},
+ }, nil
+ }
out := runAppForTest(t, []string{"get", "mdm_apple_bm"})
assert.Contains(t, out, "Apple ID:")
@@ -2070,6 +2075,29 @@ func TestGetAppleBM(t *testing.T) {
assert.Contains(t, out, "Renew date:")
assert.Contains(t, out, "Default team:")
})
+
+ t.Run("premium license, no token", func(t *testing.T) {
+ _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, DEPStorage: depStorage})
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return nil, nil
+ }
+
+ out := runAppForTest(t, []string{"get", "mdm_apple_bm"})
+ assert.Contains(t, out, "No Apple Business Manager server token found.")
+ })
+
+ t.Run("premium license, multiple tokens", func(t *testing.T) {
+ _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, DEPStorage: depStorage})
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{
+ {ID: 1},
+ {ID: 2},
+ }, nil
+ }
+
+ _, err := runAppNoChecks([]string{"get", "mdm_apple_bm"})
+ assert.ErrorContains(t, err, "This API endpoint has been deprecated. Please use the new GET /abm_tokens API endpoint")
+ })
}
func TestGetCarves(t *testing.T) {
@@ -2269,11 +2297,14 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
}
return nil, fmt.Errorf("team not found: %s", name)
}
- ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
- return nil
+ ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
+ winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
return nil
diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go
index c56016eca3..a7db6be73d 100644
--- a/cmd/fleetctl/gitops.go
+++ b/cmd/fleetctl/gitops.go
@@ -77,12 +77,35 @@ func gitopsCommand() *cli.Command {
if appConfig.License == nil {
return errors.New("no license struct found in app config")
}
+ logf := func(format string, a ...interface{}) {
+ _, _ = fmt.Fprintf(c.App.Writer, format, a...)
+ }
- var appleBMDefaultTeam string
- var appleBMDefaultTeamFound bool
+ // We need to extract the controls from no-team.yml to be able to apply them when applying the global app config.
+ var (
+ noTeamControls spec.Controls
+ noTeamPresent bool
+ )
+ for _, flFilename := range flFilenames.Value() {
+ if filepath.Base(flFilename) == "no-team.yml" {
+ baseDir := filepath.Dir(flFilename)
+ config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, func(format string, a ...interface{}) {})
+ if err != nil {
+ return err
+ }
+ noTeamControls = config.Controls
+ noTeamPresent = true
+ break
+ }
+ }
+
+ var originalABMConfig []any
+ var originalVPPConfig []any
var teamNames []string
var firstFileMustBeGlobal *bool
var teamDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions
+ var abmTeams, vppTeams []string
+ var hasMissingABMTeam, hasMissingVPPTeam, usesLegacyABMConfig bool
if totalFilenames > 1 {
firstFileMustBeGlobal = ptr.Bool(true)
}
@@ -90,7 +113,7 @@ func gitopsCommand() *cli.Command {
secrets := make(map[string]struct{})
for _, flFilename := range flFilenames.Value() {
baseDir := filepath.Dir(flFilename)
- config, err := spec.GitOpsFromFile(flFilename, baseDir)
+ config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, logf)
if err != nil {
return err
}
@@ -106,16 +129,74 @@ func gitopsCommand() *cli.Command {
}
firstFileMustBeGlobal = ptr.Bool(false)
}
- if isGlobalConfig && totalFilenames > 1 {
- // Check if Apple BM default team already exists
- appleBMDefaultTeam, appleBMDefaultTeamFound, err = checkAppleBMDefaultTeam(config, fleetClient)
+
+ if isGlobalConfig {
+ if noTeamControls.Set() && config.Controls.Set() {
+ return errors.New("'controls' cannot be set on both global config and on no-team.yml")
+ }
+ if !noTeamControls.Defined && !config.Controls.Defined {
+ if appConfig.License.IsPremium() {
+ return errors.New("'controls' must be set on global config or no-team.yml")
+ }
+ return errors.New("'controls' must be set on global config")
+ }
+ if !config.Controls.Set() {
+ config.Controls = noTeamControls
+ }
+ }
+
+ // Special handling for tokens is required because they link to teams (by
+ // name.) Because teams can be created/deleted during the same gitops run, we
+ // grab some information to help us determine allowed/restricted actions and
+ // when to perform the associations.
+ if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) {
+ abmTeams, hasMissingABMTeam, usesLegacyABMConfig, err = checkABMTeamAssignments(config, fleetClient)
if err != nil {
return err
}
+
+ vppTeams, hasMissingVPPTeam, err = checkVPPTeamAssignments(config, fleetClient)
+ if err != nil {
+ return err
+ }
+
+ // if one of the teams assigned to an ABM token doesn't exist yet, we need to
+ // submit the configs without the ABM default team set. We'll set those
+ // separately later when the teams are already created.
+ if hasMissingABMTeam {
+ if mdm, ok := config.OrgSettings["mdm"]; ok {
+ if mdmMap, ok := mdm.(map[string]any); ok {
+ if appleBM, ok := mdmMap["apple_business_manager"]; ok {
+ if bmSettings, ok := appleBM.([]any); ok {
+ originalABMConfig = bmSettings
+ }
+ }
+
+ // If team is not found, we need to remove the AppleBMDefaultTeam from
+ // the global config, and then apply it after teams are processed
+ mdmMap["apple_business_manager"] = nil
+ mdmMap["apple_bm_default_team"] = ""
+ }
+ }
+ }
+
+ if hasMissingVPPTeam {
+ if mdm, ok := config.OrgSettings["mdm"]; ok {
+ if mdmMap, ok := mdm.(map[string]any); ok {
+ if vpp, ok := mdmMap["volume_purchasing_program"]; ok {
+ if vppSettings, ok := vpp.([]any); ok {
+ originalVPPConfig = vppSettings
+ }
+ }
+
+ // If team is not found, we need to remove the VPP config from
+ // the global config, and then apply it after teams are processed
+ mdmMap["volume_purchasing_program"] = nil
+ }
+ }
+ }
}
- logf := func(format string, a ...interface{}) {
- _, _ = fmt.Fprintf(c.App.Writer, format, a...)
- }
+
if flDryRun {
incomingSecrets := fleetClient.GetGitOpsSecrets(config)
for _, secret := range incomingSecrets {
@@ -125,6 +206,7 @@ func gitopsCommand() *cli.Command {
secrets[secret] = struct{}{}
}
}
+
assumptions, err := fleetClient.DoGitOps(c.Context, config, flFilename, logf, flDryRun, teamDryRunAssumptions, appConfig)
if err != nil {
return err
@@ -135,10 +217,15 @@ func gitopsCommand() *cli.Command {
teamDryRunAssumptions = assumptions
}
}
- if appleBMDefaultTeam != "" && !appleBMDefaultTeamFound {
- // If the Apple BM default team did not exist earlier, check again and apply it if needed
- err = applyAppleBMDefaultTeamIfNeeded(c, teamNames, appleBMDefaultTeam, flDryRun, fleetClient)
- if err != nil {
+
+ // if there were assignments to tokens, and some of the teams were missing at that time, submit a separate patch request to set them now.
+ if len(abmTeams) > 0 && hasMissingABMTeam {
+ if err = applyABMTokenAssignmentIfNeeded(c, teamNames, abmTeams, originalABMConfig, usesLegacyABMConfig, flDryRun, fleetClient); err != nil {
+ return err
+ }
+ }
+ if len(vppTeams) > 0 && hasMissingVPPTeam {
+ if err = applyVPPTokenAssignmentIfNeeded(c, teamNames, vppTeams, originalVPPConfig, flDryRun, fleetClient); err != nil {
return err
}
}
@@ -149,8 +236,14 @@ func gitopsCommand() *cli.Command {
}
for _, team := range teams {
if !slices.Contains(teamNames, team.Name) {
- if appleBMDefaultTeam == team.Name {
- return fmt.Errorf("apple_bm_default_team %s cannot be deleted", appleBMDefaultTeam)
+ if slices.Contains(abmTeams, team.Name) {
+ if usesLegacyABMConfig {
+ return fmt.Errorf("apple_bm_default_team %s cannot be deleted", team.Name)
+ }
+ return fmt.Errorf("apple_business_manager team %s cannot be deleted", team.Name)
+ }
+ if slices.Contains(vppTeams, team.Name) {
+ return fmt.Errorf("volume_purchasing_program team %s cannot be deleted", team.Name)
}
if flDryRun {
_, _ = fmt.Fprintf(c.App.Writer, "[!] would delete team %s\n", team.Name)
@@ -174,54 +267,194 @@ func gitopsCommand() *cli.Command {
}
}
-func checkAppleBMDefaultTeam(config *spec.GitOps, fleetClient *service.Client) (
- appleBMDefaultTeam string, appleBMDefaultTeamFound bool, err error,
+// checkABMTeamAssignments validates the spec, and finds if:
+//
+// 1. The user is using the legacy apple_bm_default_team config.
+// 2. All teams assigned to ABM tokens already exist.
+// 3. Performs validations according to the spec for both the new and the
+// deprecated key used for this setting.
+func checkABMTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
+ abmTeams []string, missingTeam bool, usesLegacyConfig bool, err error,
) {
if mdm, ok := config.OrgSettings["mdm"]; ok {
- if mdmMap, ok := mdm.(map[string]interface{}); ok {
- if appleBMDT, ok := mdmMap["apple_bm_default_team"]; ok {
- if appleBMDefaultTeam, ok = appleBMDT.(string); ok {
- teams, err := fleetClient.ListTeams("")
- if err != nil {
- return "", false, err
- }
- // Normalize AppleBMDefaultTeam for Unicode support
+ if mdmMap, ok := mdm.(map[string]any); ok {
+ appleBMDT, hasLegacyConfig := mdmMap["apple_bm_default_team"]
+ appleBM, hasNewConfig := mdmMap["apple_business_manager"]
+
+ if hasLegacyConfig && hasNewConfig {
+ return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
+ }
+
+ if !hasLegacyConfig && !hasNewConfig {
+ return nil, false, false, nil
+ }
+
+ teams, err := fleetClient.ListTeams("")
+ if err != nil {
+ return nil, false, false, err
+ }
+ teamNames := map[string]struct{}{}
+ for _, tm := range teams {
+ teamNames[tm.Name] = struct{}{}
+ }
+
+ if hasLegacyConfig {
+ if appleBMDefaultTeam, ok := appleBMDT.(string); ok {
+ // normalize for Unicode support
appleBMDefaultTeam = norm.NFC.String(appleBMDefaultTeam)
- for _, team := range teams {
- if team.Name == appleBMDefaultTeam {
- appleBMDefaultTeamFound = true
- break
- }
+ abmTeams = append(abmTeams, appleBMDefaultTeam)
+ usesLegacyConfig = true
+ if _, ok = teamNames[appleBMDefaultTeam]; !ok {
+ missingTeam = true
}
- if !appleBMDefaultTeamFound {
- // If team is not found, we need to remove the AppleBMDefaultTeam from the global config, and then apply it after teams are processed
- mdmMap["apple_bm_default_team"] = ""
+ }
+ }
+
+ if hasNewConfig {
+ if settingMap, ok := appleBM.([]any); ok {
+ for _, item := range settingMap {
+ if cfg, ok := item.(map[string]any); ok {
+ for _, teamConfigKey := range []string{"macos_team", "ios_team", "ipados_team"} {
+ if team, ok := cfg[teamConfigKey].(string); ok && team != "" {
+ // normalize for Unicode support
+ team = norm.NFC.String(team)
+ abmTeams = append(abmTeams, team)
+ if _, ok := teamNames[team]; !ok {
+ missingTeam = true
+ }
+ }
+ }
+ }
}
}
}
}
}
- return appleBMDefaultTeam, appleBMDefaultTeamFound, nil
+
+ return abmTeams, missingTeam, usesLegacyConfig, nil
}
-func applyAppleBMDefaultTeamIfNeeded(
- ctx *cli.Context, teamNames []string, appleBMDefaultTeam string, flDryRun bool, fleetClient *service.Client,
+func applyABMTokenAssignmentIfNeeded(
+ ctx *cli.Context,
+ teamNames []string,
+ abmTeamNames []string,
+ originalMDMConfig []any,
+ usesLegacyConfig bool,
+ flDryRun bool,
+ fleetClient *service.Client,
) error {
- if !slices.Contains(teamNames, appleBMDefaultTeam) {
- return fmt.Errorf("apple_bm_default_team %s not found in team configs", appleBMDefaultTeam)
+ if usesLegacyConfig && len(abmTeamNames) > 1 {
+ return errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
}
- appConfigUpdate := map[string]map[string]interface{}{
- "mdm": {
- "apple_bm_default_team": appleBMDefaultTeam,
- },
+
+ if usesLegacyConfig && len(abmTeamNames) == 0 {
+ return errors.New("using legacy config without any ABM teams defined")
}
- if flDryRun {
- _, _ = fmt.Fprintf(ctx.App.Writer, "[!] would apply apple_bm_default_team %s\n", appleBMDefaultTeam)
- } else {
- _, _ = fmt.Fprintf(ctx.App.Writer, "[+] applying apple_bm_default_team %s\n", appleBMDefaultTeam)
- if err := fleetClient.ApplyAppConfig(appConfigUpdate, fleet.ApplySpecOptions{}); err != nil {
- return fmt.Errorf("applying fleet config: %w", err)
+
+ var appConfigUpdate map[string]map[string]any
+ if usesLegacyConfig {
+ appleBMDefaultTeam := abmTeamNames[0]
+ if !slices.Contains(teamNames, appleBMDefaultTeam) {
+ return fmt.Errorf("apple_bm_default_team team %q not found in team configs", appleBMDefaultTeam)
}
+ appConfigUpdate = map[string]map[string]any{
+ "mdm": {
+ "apple_bm_default_team": appleBMDefaultTeam,
+ },
+ }
+ } else {
+ for _, abmTeam := range abmTeamNames {
+ if !slices.Contains(teamNames, abmTeam) {
+ return fmt.Errorf("apple_business_manager team %q not found in team configs", abmTeam)
+ }
+ }
+
+ appConfigUpdate = map[string]map[string]any{
+ "mdm": {
+ "apple_business_manager": originalMDMConfig,
+ },
+ }
+ }
+
+ if flDryRun {
+ _, _ = fmt.Fprint(ctx.App.Writer, "[!] would apply ABM teams\n")
+ return nil
+ }
+ _, _ = fmt.Fprintf(ctx.App.Writer, "[+] applying ABM teams\n")
+ if err := fleetClient.ApplyAppConfig(appConfigUpdate, fleet.ApplySpecOptions{}); err != nil {
+ return fmt.Errorf("applying fleet config: %w", err)
+ }
+ return nil
+}
+
+func checkVPPTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
+ vppTeams []string, missingTeam bool, err error,
+) {
+ if mdm, ok := config.OrgSettings["mdm"]; ok {
+ if mdmMap, ok := mdm.(map[string]any); ok {
+ teams, err := fleetClient.ListTeams("")
+ if err != nil {
+ return nil, false, err
+ }
+ teamNames := map[string]struct{}{}
+ for _, tm := range teams {
+ teamNames[tm.Name] = struct{}{}
+ }
+
+ if vpp, ok := mdmMap["volume_purchasing_program"]; ok {
+ if vppInterfaces, ok := vpp.([]any); ok {
+ for _, item := range vppInterfaces {
+ if itemMap, ok := item.(map[string]any); ok {
+ if teams, ok := itemMap["teams"].([]any); ok {
+ for _, team := range teams {
+ if teamStr, ok := team.(string); ok {
+ // normalize for Unicode support
+ normalizedTeam := norm.NFC.String(teamStr)
+ vppTeams = append(vppTeams, normalizedTeam)
+ if _, ok := teamNames[normalizedTeam]; !ok {
+ missingTeam = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return vppTeams, missingTeam, nil
+}
+
+func applyVPPTokenAssignmentIfNeeded(
+ ctx *cli.Context,
+ teamNames []string,
+ vppTeamNames []string,
+ originalVPPConfig []any,
+ flDryRun bool,
+ fleetClient *service.Client,
+) error {
+ var appConfigUpdate map[string]map[string]any
+ for _, vppTeam := range vppTeamNames {
+ if !fleet.IsReservedTeamName(vppTeam) && !slices.Contains(teamNames, vppTeam) {
+ return fmt.Errorf("volume_purchasing_program team %s not found in team configs", vppTeam)
+ }
+ }
+
+ appConfigUpdate = map[string]map[string]any{
+ "mdm": {
+ "volume_purchasing_program": originalVPPConfig,
+ },
+ }
+
+ if flDryRun {
+ _, _ = fmt.Fprint(ctx.App.Writer, "[!] would apply volume_purchasing_program teams\n")
+ return nil
+ }
+ _, _ = fmt.Fprintf(ctx.App.Writer, "[+] applying volume_purchasing_program teams\n")
+ if err := fleetClient.ApplyAppConfig(appConfigUpdate, fleet.ApplySpecOptions{}); err != nil {
+ return fmt.Errorf("applying fleet config for volume_purchasing_program teams: %w", err)
}
return nil
}
diff --git a/cmd/fleetctl/gitops_enterprise_integration_test.go b/cmd/fleetctl/gitops_enterprise_integration_test.go
index ab3f8bf835..235d89dca8 100644
--- a/cmd/fleetctl/gitops_enterprise_integration_test.go
+++ b/cmd/fleetctl/gitops_enterprise_integration_test.go
@@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/suite"
)
-func TestEnterpriseIntegrationsGitops(t *testing.T) {
+func TestIntegrationsEnterpriseGitops(t *testing.T) {
testingSuite := new(enterpriseIntegrationGitopsTestSuite)
testingSuite.suite = &testingSuite.Suite
suite.Run(t, testingSuite)
@@ -176,6 +176,7 @@ contexts:
fmt.Sprintf(
`
controls:
+software:
queries:
policies:
agent_options:
@@ -186,6 +187,9 @@ team_settings:
),
)
require.NoError(t, err)
+
+ test.CreateInsertGlobalVPPToken(t, s.ds)
+
// Apply the team to be deleted
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", deletedTeamFile.Name()})
@@ -230,5 +234,4 @@ team_settings:
for _, fileName := range teamFileNames {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", fileName})
}
-
}
diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go
index 826dd43374..1a054482f6 100644
--- a/cmd/fleetctl/gitops_test.go
+++ b/cmd/fleetctl/gitops_test.go
@@ -2,8 +2,6 @@ package main
import (
"context"
- "crypto/rand"
- "encoding/base64"
"encoding/json"
"fmt"
"net/http"
@@ -12,9 +10,11 @@ import (
"path/filepath"
"slices"
"strings"
+ "sync"
"testing"
"time"
+ "github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -25,6 +25,7 @@ import (
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
+ "github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -36,13 +37,13 @@ const (
orgName = "GitOps Test"
)
-func TestFilenameValidation(t *testing.T) {
+func TestGitOpsFilenameValidation(t *testing.T) {
filename := strings.Repeat("a", filenameMaxLength+1)
_, err := runAppNoChecks([]string{"gitops", "-f", filename})
assert.ErrorContains(t, err, "file name must be less than")
}
-func TestBasicGlobalFreeGitOps(t *testing.T) {
+func TestGitOpsBasicGlobalFree(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
_, ds := runServerWithMockedDS(t)
@@ -50,13 +51,13 @@ func TestBasicGlobalFreeGitOps(t *testing.T) {
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.NewActivityFunc = func(
@@ -142,6 +143,28 @@ org_settings:
require.Error(t, err)
assert.Contains(t, err.Error(), "organization name must be present")
+ // Missing controls.
+ tmpFile2, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = tmpFile2.WriteString(
+ `
+queries:
+policies:
+agent_options:
+org_settings:
+ server_settings:
+ server_url: https://example.com
+ org_info:
+ contact_url: https://example.com/contact
+ org_name: Foobar
+ secrets:
+`,
+ )
+ require.NoError(t, err)
+ _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile2.Name()})
+ require.Error(t, err)
+ assert.Equal(t, `'controls' must be set on global config`, err.Error())
+
// Dry run
t.Setenv("ORG_NAME", orgName)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
@@ -154,26 +177,27 @@ org_settings:
assert.Empty(t, enrolledSecrets)
}
-func TestBasicGlobalPremiumGitOps(t *testing.T) {
+func TestGitOpsBasicGlobalPremium(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
- License: license,
+ License: license,
+ KeyValueStore: newMemKeyValueStore(),
},
)
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.NewActivityFunc = func(
@@ -207,6 +231,12 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
+ ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
+ return nil
+ }
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -238,6 +268,7 @@ org_settings:
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets:
+software:
`,
)
require.NoError(t, err)
@@ -254,18 +285,19 @@ org_settings:
assert.Empty(t, enrolledSecrets)
}
-func TestBasicTeamGitOps(t *testing.T) {
+func TestGitOpsBasicTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
- License: license,
+ License: license,
+ KeyValueStore: newMemKeyValueStore(),
},
)
const secret = "TestSecret"
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
@@ -274,13 +306,13 @@ func TestBasicTeamGitOps(t *testing.T) {
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.NewActivityFunc = func(
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
@@ -350,6 +382,9 @@ func TestBasicTeamGitOps(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
enrolledTeamSecrets = secrets
return nil
@@ -360,6 +395,9 @@ func TestBasicTeamGitOps(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
+ ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ return nil, 0, nil, nil
+ }
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -381,6 +419,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: ${TEST_SECRET}
+software:
`,
)
require.NoError(t, err)
@@ -391,6 +430,26 @@ team_settings:
require.Error(t, err)
assert.Contains(t, err.Error(), "'name' is required")
+ // Invalid name for "No team" file (dry and real).
+ t.Setenv("TEST_TEAM_NAME", "no TEam")
+ _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
+ t.Setenv("TEST_TEAM_NAME", "no TEam")
+ _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", tmpFile.Name()))
+
+ t.Setenv("TEST_TEAM_NAME", "All teams")
+ _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)
+
+ t.Setenv("TEST_TEAM_NAME", "All TEAMS")
+ _, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), `"All teams" is a reserved team name`)
+
// Dry run
t.Setenv("TEST_TEAM_NAME", teamName)
_ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
@@ -413,7 +472,7 @@ team_settings:
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
}
-func TestFullGlobalGitOps(t *testing.T) {
+func TestGitOpsFullGlobal(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
// mdm test configuration must be set so that activating windows MDM works.
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
@@ -446,13 +505,14 @@ func TestFullGlobalGitOps(t *testing.T) {
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
- ) error {
+ ) (updates fleet.MDMProfilesUpdates, err error) {
appliedMacProfiles = macProfiles
appliedWinProfiles = winProfiles
- return nil
+ return fleet.MDMProfilesUpdates{}, nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
@@ -537,16 +597,10 @@ func TestFullGlobalGitOps(t *testing.T) {
)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
- t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
+ t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)
file := "./testdata/gitops/global_config_no_paths.yml"
- // Dry run should fail because Apple BM Default Team does not exist and premium license is not set
- _, err = runAppNoChecks([]string{"gitops", "-f", file, "--dry-run"})
- require.Error(t, err)
- assert.True(t, strings.Contains(err.Error(), "missing or invalid license"))
-
// Dry run
- t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
_ = runAppForTest(t, []string{"gitops", "-f", file, "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
assert.Len(t, enrolledSecrets, 0)
@@ -579,7 +633,7 @@ func TestFullGlobalGitOps(t *testing.T) {
assert.Equal(t, "https://activities_webhook_url", savedAppConfig.WebhookSettings.ActivitiesWebhook.DestinationURL)
}
-func TestFullTeamGitOps(t *testing.T) {
+func TestGitOpsFullTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
@@ -599,6 +653,7 @@ func TestFullTeamGitOps(t *testing.T) {
MDMPusher: mockPusher{},
FleetConfig: &fleetCfg,
NoCacheDatastore: true,
+ KeyValueStore: newMemKeyValueStore(),
},
)
@@ -627,13 +682,14 @@ func TestFullTeamGitOps(t *testing.T) {
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
- ) error {
+ ) (updates fleet.MDMProfilesUpdates, err error) {
appliedMacProfiles = macProfiles
appliedWinProfiles = winProfiles
- return nil
+ return fleet.MDMProfilesUpdates{}, nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
@@ -659,6 +715,9 @@ func TestFullTeamGitOps(t *testing.T) {
// Team
var savedTeam *fleet.Team
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
+ if name == "Conflict" {
+ return &fleet.Team{}, nil
+ }
if savedTeam != nil && savedTeam.Name == name {
return savedTeam, nil
}
@@ -754,10 +813,15 @@ func TestFullTeamGitOps(t *testing.T) {
appliedQueries = queries
return nil
}
+ var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
+ appliedSoftwareInstallers = installers
return nil
}
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
@@ -767,6 +831,9 @@ func TestFullTeamGitOps(t *testing.T) {
enrolledSecrets = secrets
return nil
}
+ ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ return nil, 0, nil, nil
+ }
startSoftwareInstallerServer(t)
@@ -774,8 +841,8 @@ func TestFullTeamGitOps(t *testing.T) {
// Dry run
const baseFilename = "team_config_no_paths.yml"
- file := "./testdata/gitops/" + baseFilename
- _ = runAppForTest(t, []string{"gitops", "-f", file, "--dry-run"})
+ gitopsFile := "./testdata/gitops/" + baseFilename
+ _ = runAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
assert.Nil(t, savedTeam)
assert.Len(t, enrolledSecrets, 0)
assert.Len(t, appliedPolicySpecs, 0)
@@ -783,13 +850,14 @@ func TestFullTeamGitOps(t *testing.T) {
assert.Len(t, appliedScripts, 0)
assert.Len(t, appliedMacProfiles, 0)
assert.Len(t, appliedWinProfiles, 0)
+ assert.Empty(t, appliedSoftwareInstallers)
// Real run
// Setting global calendar config
appConfig.Integrations = fleet.Integrations{
GoogleCalendar: []*fleet.GoogleCalendarIntegration{{}},
}
- _ = runAppForTest(t, []string{"gitops", "-f", file})
+ _ = runAppForTest(t, []string{"gitops", "-f", gitopsFile})
require.NotNil(t, savedTeam)
assert.Equal(t, teamName, savedTeam.Name)
assert.Contains(t, string(*savedTeam.Config.AgentOptions), "distributed_denylist_duration")
@@ -809,17 +877,30 @@ func TestFullTeamGitOps(t *testing.T) {
require.NotNil(t, savedTeam.Config.Integrations.GoogleCalendar)
assert.True(t, savedTeam.Config.Integrations.GoogleCalendar.Enable)
assert.Equal(t, baseFilename, *savedTeam.Filename)
+ require.Len(t, appliedSoftwareInstallers, 2)
+ packageID := `"ruby"`
+ uninstallScriptProcessed := strings.ReplaceAll(file.GetUninstallScript("deb"), "$PACKAGE_ID", packageID)
+ assert.ElementsMatch(t, []string{fmt.Sprintf("echo 'uninstall' %s\n", packageID), uninstallScriptProcessed},
+ []string{appliedSoftwareInstallers[0].UninstallScript, appliedSoftwareInstallers[1].UninstallScript})
// Change team name
newTeamName := "New Team Name"
t.Setenv("TEST_TEAM_NAME", newTeamName)
- _ = runAppForTest(t, []string{"gitops", "-f", file, "--dry-run"})
- _ = runAppForTest(t, []string{"gitops", "-f", file})
+ _ = runAppForTest(t, []string{"gitops", "-f", gitopsFile, "--dry-run"})
+ _ = runAppForTest(t, []string{"gitops", "-f", gitopsFile})
require.NotNil(t, savedTeam)
assert.Equal(t, newTeamName, savedTeam.Name)
assert.Equal(t, baseFilename, *savedTeam.Filename)
+ // Try to change team name again, but this time the new name conflicts with an existing team
+ t.Setenv("TEST_TEAM_NAME", "Conflict")
+ _, err = runAppNoChecks([]string{"gitops", "-f", gitopsFile, "--dry-run"})
+ assert.ErrorContains(t, err, "team name already exists")
+ _, err = runAppNoChecks([]string{"gitops", "-f", gitopsFile})
+ assert.ErrorContains(t, err, "team name already exists")
+
// Now clear the settings
+ t.Setenv("TEST_TEAM_NAME", newTeamName)
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
secret := "TestSecret"
@@ -834,6 +915,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
+software:
`,
)
require.NoError(t, err)
@@ -863,12 +945,13 @@ team_settings:
assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename)
}
-func TestBasicGlobalAndTeamGitOps(t *testing.T) {
+func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := runServerWithMockedDS(
t, &service.TestServerOpts{
- License: license,
+ License: license,
+ KeyValueStore: newMemKeyValueStore(),
},
)
@@ -882,7 +965,7 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
return nil
}
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
@@ -917,10 +1000,10 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
- ) error {
+ ) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, macProfiles)
assert.Empty(t, winProfiles)
- return nil
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
assert.Empty(t, scripts)
@@ -928,9 +1011,9 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
- ) error {
+ ) (updates fleet.MDMProfilesUpdates, err error) {
assert.Empty(t, profileUUIDs)
- return nil
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
@@ -989,6 +1072,12 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
+ ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ return nil, 0, nil, nil
+ }
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -1011,6 +1100,7 @@ org_settings:
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
secrets: [{"secret":"globalSecret"}]
+software:
`,
)
require.NoError(t, err)
@@ -1030,6 +1120,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
+software:
`,
)
require.NoError(t, err)
@@ -1045,6 +1136,7 @@ agent_options:
name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
+software:
`,
)
require.NoError(t, err)
@@ -1119,10 +1211,367 @@ team_settings:
assert.True(t, ds.DeleteTeamFuncInvoked)
}
-func TestFullGlobalAndTeamGitOps(t *testing.T) {
+func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
+ // Cannot run t.Parallel() because runServerWithMockedDS sets the FLEET_SERVER_ADDRESS
+ // environment variable.
+
+ license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
+ _, ds := runServerWithMockedDS(
+ t, &service.TestServerOpts{
+ License: license,
+ KeyValueStore: newMemKeyValueStore(),
+ },
+ )
+ // Mock appConfig
+ savedAppConfig := &fleet.AppConfig{}
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return &fleet.AppConfig{}, nil
+ }
+ ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
+ savedAppConfig = config
+ return nil
+ }
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
+ return nil
+ }
+ ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
+ return nil
+ }
+
+ const (
+ fleetServerURL = "https://fleet.example.com"
+ orgName = "GitOps Test"
+ secret = "TestSecret"
+ )
+ var enrolledSecrets []*fleet.EnrollSecret
+ var enrolledTeamSecrets []*fleet.EnrollSecret
+ var savedTeam *fleet.Team
+ team := &fleet.Team{
+ ID: 1,
+ CreatedAt: time.Now(),
+ Name: teamName,
+ }
+
+ ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
+ return true, nil
+ }
+ ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
+ if teamID == nil {
+ enrolledSecrets = secrets
+ } else {
+ enrolledTeamSecrets = secrets
+ }
+ return nil
+ }
+ ds.BatchSetMDMProfilesFunc = func(
+ ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
+ macDecls []*fleet.MDMAppleDeclaration,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ assert.Empty(t, macProfiles)
+ assert.Empty(t, winProfiles)
+ return fleet.MDMProfilesUpdates{}, nil
+ }
+ ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
+ assert.Empty(t, scripts)
+ return nil
+ }
+ ds.BulkSetPendingMDMHostProfilesFunc = func(
+ ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ assert.Empty(t, profileUUIDs)
+ return fleet.MDMProfilesUpdates{}, nil
+ }
+ ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
+ return nil
+ }
+ ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
+ require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus})
+ return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
+ }
+ ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
+ ds.ListTeamPoliciesFunc = func(
+ ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
+ ) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
+ return nil, nil, nil
+ }
+ ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
+ return nil, nil
+ }
+ ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
+ ds.NewActivityFunc = func(
+ ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
+ ) error {
+ return nil
+ }
+ ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
+ job.ID = 1
+ return job, nil
+ }
+ ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
+ if tid == team.ID {
+ return savedTeam, nil
+ }
+ return nil, nil
+ }
+ ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
+ if name == teamName && savedTeam != nil {
+ return savedTeam, nil
+ }
+ return nil, ¬FoundError{}
+ }
+ ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
+ if savedTeam != nil && *savedTeam.Filename == filename {
+ return savedTeam, nil
+ }
+ return nil, ¬FoundError{}
+ }
+ ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) {
+ newTeam.ID = team.ID
+ savedTeam = newTeam
+ enrolledTeamSecrets = newTeam.Secrets
+ return newTeam, nil
+ }
+ ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
+ savedTeam = team
+ return team, nil
+ }
+ ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
+ return nil
+ }
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
+ ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ return nil, 0, nil, nil
+ }
+
+ globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+
+ _, err = globalFileBasic.WriteString(fmt.Sprintf(
+ `
+controls:
+queries:
+policies:
+agent_options:
+org_settings:
+ server_settings:
+ server_url: %s
+ org_info:
+ contact_url: https://example.com/contact
+ org_logo_url: ""
+ org_logo_url_light_background: ""
+ org_name: %s
+ secrets: [{"secret":"globalSecret"}]
+software:
+`, fleetServerURL, orgName),
+ )
+ require.NoError(t, err)
+
+ globalFileWithSoftware, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = globalFileWithSoftware.WriteString(fmt.Sprintf(
+ `
+controls:
+queries:
+policies:
+agent_options:
+org_settings:
+ server_settings:
+ server_url: %s
+ org_info:
+ contact_url: https://example.com/contact
+ org_logo_url: ""
+ org_logo_url_light_background: ""
+ org_name: %s
+ secrets: [{"secret":"globalSecret"}]
+software:
+ packages:
+ - url: https://example.com
+`, fleetServerURL, orgName),
+ )
+ require.NoError(t, err)
+
+ globalFileWithControls, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = globalFileWithControls.WriteString(fmt.Sprintf(
+ `
+controls:
+ ios_updates:
+ deadline: "2022-02-02"
+ minimum_version: "17.6"
+queries:
+policies:
+agent_options:
+org_settings:
+ server_settings:
+ server_url: %s
+ org_info:
+ contact_url: https://example.com/contact
+ org_logo_url: ""
+ org_logo_url_light_background: ""
+ org_name: %s
+ secrets: [{"secret":"globalSecret"}]
+software:
+`, fleetServerURL, orgName),
+ )
+ require.NoError(t, err)
+
+ globalFileWithoutControlsAndSoftwareKeys, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = globalFileWithoutControlsAndSoftwareKeys.WriteString(fmt.Sprintf(
+ `
+queries:
+policies:
+agent_options:
+org_settings:
+ server_settings:
+ server_url: %s
+ org_info:
+ contact_url: https://example.com/contact
+ org_logo_url: ""
+ org_logo_url_light_background: ""
+ org_name: %s
+ secrets: [{"secret":"globalSecret"}]
+`, fleetServerURL, orgName),
+ )
+ require.NoError(t, err)
+
+ teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = teamFile.WriteString(fmt.Sprintf(`
+controls:
+queries:
+policies:
+agent_options:
+name: %s
+team_settings:
+ secrets: [{"secret":"%s"}]
+software:
+`, teamName, secret),
+ )
+ require.NoError(t, err)
+
+ noTeamFilePath := filepath.Join(t.TempDir(), "no-team.yml")
+ noTeamFile, err := os.Create(noTeamFilePath)
+ require.NoError(t, err)
+ _, err = noTeamFile.WriteString(`
+controls:
+policies:
+name: No team
+software:
+`)
+ require.NoError(t, err)
+
+ noTeamFilePathPoliciesCalendarPath := filepath.Join(t.TempDir(), "no-team.yml")
+ noTeamFilePathPoliciesCalendar, err := os.Create(noTeamFilePathPoliciesCalendarPath)
+ require.NoError(t, err)
+ _, err = noTeamFilePathPoliciesCalendar.WriteString(`
+controls:
+policies:
+ - name: Foobar
+ query: SELECT 1 FROM osquery_info WHERE start_time < 0;
+ calendar_events_enabled: true
+name: No team
+software:
+`)
+ require.NoError(t, err)
+
+ noTeamFilePathWithControls := filepath.Join(t.TempDir(), "no-team.yml")
+ noTeamFileWithControls, err := os.Create(noTeamFilePathWithControls)
+ require.NoError(t, err)
+ _, err = noTeamFileWithControls.WriteString(`
+controls:
+ ipados_updates:
+ deadline: "2023-03-03"
+ minimum_version: "18.0"
+policies:
+name: No team
+software:
+`)
+ require.NoError(t, err)
+
+ noTeamFilePathWithoutControls := filepath.Join(t.TempDir(), "no-team.yml")
+ noTeamFileWithoutControls, err := os.Create(noTeamFilePathWithoutControls)
+ require.NoError(t, err)
+ _, err = noTeamFileWithoutControls.WriteString(`
+policies:
+name: No team
+software:
+`)
+ require.NoError(t, err)
+
+ // Dry run, global defines software, should fail.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file"))
+ // Real run, global defines software, should fail.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithSoftware.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'software' cannot be set on global file"))
+
+ // Dry run, both global and no-team.yml define controls.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
+ // Real run, both global and no-team.yml define controls.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name()})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
+
+ // Dry run, both global and no-team.yml defines policy with calendar events enabled.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
+ // Real run, both global and no-team.yml define controls.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name()})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
+
+ // Dry run, controls should be defined somewhere, either in no-team.yml or global.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
+ // Real run, both global and no-team.yml define controls.
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name()})
+ require.Error(t, err)
+ assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
+
+ // Dry run, global file without controls and software keys.
+ _ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
+ assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
+
+ // Real run, global file without controls and software keys.
+ _ = runAppForTest(t, []string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
+ assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
+ assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
+ assert.Len(t, enrolledSecrets, 1)
+ require.NotNil(t, savedTeam)
+ assert.Equal(t, teamName, savedTeam.Name)
+ require.Len(t, enrolledTeamSecrets, 1)
+ assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
+
+ // Restore to test below.
+ savedAppConfig = &fleet.AppConfig{}
+
+ // Dry run
+ _ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name(), "--dry-run"})
+ assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
+ // Real run
+ _ = runAppForTest(t, []string{"gitops", "-f", globalFileBasic.Name(), "-f", teamFile.Name(), "-f", noTeamFile.Name()})
+ assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
+ assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
+ assert.Len(t, enrolledSecrets, 1)
+ require.NotNil(t, savedTeam)
+ assert.Equal(t, teamName, savedTeam.Name)
+ require.Len(t, enrolledTeamSecrets, 1)
+ assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
+}
+
+func TestGitOpsFullGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
// mdm test configuration must be set so that activating windows MDM works.
- ds, savedAppConfigPtr, savedTeamPtr := setupFullGitOpsPremiumServer(t)
+ ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
startSoftwareInstallerServer(t)
var enrolledSecrets []*fleet.EnrollSecret
@@ -1150,9 +1599,9 @@ func TestFullGlobalAndTeamGitOps(t *testing.T) {
}
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
team.ID = 1
- *savedTeamPtr = team
enrolledTeamSecrets = team.Secrets
- return *savedTeamPtr, nil
+ savedTeams[team.Name] = &team
+ return team, nil
}
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes()
@@ -1171,7 +1620,7 @@ func TestFullGlobalAndTeamGitOps(t *testing.T) {
}, nil
}
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
@@ -1181,11 +1630,6 @@ func TestFullGlobalAndTeamGitOps(t *testing.T) {
globalFile := "./testdata/gitops/global_config_no_paths.yml"
teamFile := "./testdata/gitops/team_config_no_paths.yml"
- // Dry run on global file should fail because Apple BM Default Team does not exist (and has not been provided)
- _, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "--dry-run"})
- require.Error(t, err)
- assert.True(t, strings.Contains(err.Error(), "team name not found"))
-
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile, "-f", teamFile, "--dry-run", "--delete-other-teams"})
assert.False(t, ds.SaveAppConfigFuncInvoked)
@@ -1199,30 +1643,31 @@ func TestFullGlobalAndTeamGitOps(t *testing.T) {
assert.Equal(t, orgName, (*savedAppConfigPtr).OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, (*savedAppConfigPtr).ServerSettings.ServerURL)
assert.Len(t, enrolledSecrets, 2)
- require.NotNil(t, *savedTeamPtr)
- assert.Equal(t, teamName, (*savedTeamPtr).Name)
+ require.NotNil(t, *savedTeams[teamName])
+ assert.Equal(t, teamName, (*savedTeams[teamName]).Name)
require.Len(t, enrolledTeamSecrets, 2)
}
-func TestTeamSofwareInstallersGitOps(t *testing.T) {
+func TestGitOpsTeamSofwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
cases := []struct {
file string
wantErr string
}{
- {"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
+ {"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
- {"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
+ {"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
+ {"testdata/gitops/team_software_installer_uninstall_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
- {"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field TeamSpecSoftware.packages of type bool"},
+ {"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
@@ -1238,7 +1683,7 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
}
}
-func TestTeamSoftwareInstallersGitopsQueryEnv(t *testing.T) {
+func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
startSoftwareInstallerServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
@@ -1250,12 +1695,56 @@ func TestTeamSoftwareInstallersGitopsQueryEnv(t *testing.T) {
}
return nil
}
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
_, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_software_installer_valid_env_query.yml"})
require.NoError(t, err)
}
-func TestTeamVPPAppsGitOps(t *testing.T) {
+func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
+ startSoftwareInstallerServer(t)
+
+ cases := []struct {
+ noTeamFile string
+ wantErr string
+ }{
+ {"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
+ {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
+ {"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
+ {"testdata/gitops/no_team_software_installer_valid.yml", ""},
+ {"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
+ {"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
+ {"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
+ {"testdata/gitops/no_team_software_installer_uninstall_not_found.yml", "no such file or directory"},
+ {"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
+ {"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
+ {"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
+ }
+ for _, c := range cases {
+ t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
+ setupFullGitOpsPremiumServer(t)
+
+ t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
+ globalFile := "./testdata/gitops/global_config_no_paths.yml"
+ dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
+ t.Cleanup(func() {
+ os.Remove(dstPath)
+ })
+ err := file.Copy(c.noTeamFile, dstPath, 0o755)
+ require.NoError(t, err)
+ _, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
+ if c.wantErr == "" {
+ require.NoError(t, err)
+ } else {
+ require.ErrorContains(t, err, c.wantErr)
+ }
+ })
+ }
+}
+
+func TestGitOpsTeamVPPApps(t *testing.T) {
config := &appleVPPConfigSrvConf{
Assets: []vpp.Asset{
{
@@ -1305,34 +1794,37 @@ func TestTeamVPPAppsGitOps(t *testing.T) {
tokenExpiration time.Time
}{
{"testdata/gitops/team_vpp_valid_app.yml", "", time.Now().Add(24 * time.Hour)},
- {"testdata/gitops/team_vpp_valid_app.yml", "", time.Now().Add(24 * time.Hour)},
+ {"testdata/gitops/team_vpp_valid_app_self_service.yml", "", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(-24 * time.Hour)},
{"testdata/gitops/team_vpp_valid_app.yml", "VPP token expired", time.Now().Add(-24 * time.Hour)},
{"testdata/gitops/team_vpp_invalid_app.yml", "app not available on vpp account", time.Now().Add(24 * time.Hour)},
+ {"testdata/gitops/team_vpp_incorrect_type.yml", "\"app_store_apps.app_store_id\" must be a string, found number", time.Now().Add(24 * time.Hour)},
+ {"testdata/gitops/team_vpp_empty_adamid.yml", "software app store id required", time.Now().Add(24 * time.Hour)},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
ds, _, _ := setupFullGitOpsPremiumServer(t)
- token, err := createVPPDataToken(c.tokenExpiration, "fleet", "ca")
+ token, err := test.CreateVPPTokenEncoded(c.tokenExpiration, "fleet", "ca")
require.NoError(t, err)
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
- ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
- asset := map[fleet.MDMAssetName]fleet.MDMConfigAsset{
- fleet.MDMAssetVPPToken: {
- Name: fleet.MDMAssetVPPToken,
- Value: token,
- },
- }
- return asset, nil
+ ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
+ return &fleet.VPPTokenDB{
+ ID: 1,
+ OrgName: "Fleet",
+ Location: "Earth",
+ RenewDate: c.tokenExpiration,
+ Token: string(token),
+ Teams: nil,
+ }, nil
}
_, err = runAppNoChecks([]string{"gitops", "-f", c.file})
@@ -1345,35 +1837,7 @@ func TestTeamVPPAppsGitOps(t *testing.T) {
}
}
-func createVPPDataToken(expiration time.Time, orgName, location string) ([]byte, error) {
- var randBytes [32]byte
- _, err := rand.Read(randBytes[:])
- if err != nil {
- return nil, fmt.Errorf("generating random bytes: %w", err)
- }
- token := base64.StdEncoding.EncodeToString(randBytes[:])
- raw := fleet.VPPTokenRaw{
- OrgName: orgName,
- Token: token,
- ExpDate: expiration.Format("2006-01-02T15:04:05Z0700"),
- }
- rawJson, err := json.Marshal(raw)
- if err != nil {
- return nil, fmt.Errorf("marshalling vpp raw token: %w", err)
- }
-
- base64Token := base64.StdEncoding.EncodeToString(rawJson)
-
- dataToken := fleet.VPPTokenData{Token: base64Token, Location: location}
- dataTokenJson, err := json.Marshal(dataToken)
- if err != nil {
- return nil, fmt.Errorf("marshalling vpp data token: %w", err)
- }
-
- return dataTokenJson, nil
-}
-
-func TestCustomSettingsGitOps(t *testing.T) {
+func TestGitOpsCustomSettings(t *testing.T) {
cases := []struct {
file string
wantErr string
@@ -1409,7 +1873,7 @@ func TestCustomSettingsGitOps(t *testing.T) {
}
return ret, nil
}
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
@@ -1594,7 +2058,7 @@ func startVPPApplyServer(t *testing.T, config *appleVPPConfigSrvConf) {
t.Cleanup(srv.Close)
}
-func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, **fleet.Team) {
+func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, map[string]**fleet.Team) {
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
@@ -1610,6 +2074,7 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
FleetConfig: &fleetCfg,
License: license,
NoCacheDatastore: true,
+ KeyValueStore: newMemKeyValueStore(),
},
)
@@ -1628,14 +2093,14 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
savedAppConfig = &appConfigCopy
return nil
}
- ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
+ ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
- var savedTeam *fleet.Team
+ savedTeams := map[string]**fleet.Team{}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
return nil
@@ -1651,14 +2116,14 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
- ) error {
- return nil
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
@@ -1677,8 +2142,12 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
return nil, nil, nil
}
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
- if savedTeam != nil {
- return []*fleet.Team{savedTeam}, nil
+ if savedTeams != nil {
+ var result []*fleet.Team
+ for _, t := range savedTeams {
+ result = append(result, *t)
+ }
+ return result, nil
}
return nil, nil
}
@@ -1696,33 +2165,39 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
return job, nil
}
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
- team.ID = 1
- savedTeam = team
- return savedTeam, nil
+ team.ID = uint(len(savedTeams) + 1)
+ savedTeams[team.Name] = &team
+ return team, nil
}
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, ¬FoundError{}
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
- if savedTeam != nil && tid == savedTeam.ID {
- return savedTeam, nil
+ for _, tm := range savedTeams {
+ if (*tm).ID == tid {
+ return *tm, nil
+ }
}
return nil, ¬FoundError{}
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
- if savedTeam != nil && name == teamName {
- return savedTeam, nil
+ for _, tm := range savedTeams {
+ if (*tm).Name == name {
+ return *tm, nil
+ }
}
return nil, ¬FoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
- if savedTeam != nil && *savedTeam.Filename == filename {
- return savedTeam, nil
+ for _, tm := range savedTeams {
+ if *(*tm).Filename == filename {
+ return *tm, nil
+ }
}
return nil, ¬FoundError{}
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
- savedTeam = team
+ savedTeams[team.Name] = &team
return team, nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (
@@ -1734,11 +2209,734 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
+ ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
+ return nil, nil
+ }
+
+ ds.InsertVPPTokenFunc = func(ctx context.Context, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
+ return &fleet.VPPTokenDB{}, nil
+ }
+ ds.GetVPPTokenFunc = func(ctx context.Context, tokenID uint) (*fleet.VPPTokenDB, error) {
+ return &fleet.VPPTokenDB{}, err
+ }
+ ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
+ return &fleet.VPPTokenDB{}, nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return nil, nil
+ }
+ ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
+ return &fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
+ }
+ ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ return nil, 0, nil, nil
+ }
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
t.Setenv("TEST_TEAM_NAME", teamName)
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
- return ds, &savedAppConfig, &savedTeam
+ return ds, &savedAppConfig, savedTeams
+}
+
+func TestGitOpsABM(t *testing.T) {
+ global := func(mdm string) string {
+ return fmt.Sprintf(`
+controls:
+queries:
+policies:
+agent_options:
+software:
+org_settings:
+ server_settings:
+ server_url: "https://foo.example.com"
+ org_info:
+ org_name: GitOps Test
+ secrets:
+ - secret: "global"
+ mdm:
+ %s
+ `, mdm)
+ }
+
+ team := func(name string) string {
+ return fmt.Sprintf(`
+name: %s
+team_settings:
+ secrets:
+ - secret: "%s-secret"
+agent_options:
+controls:
+policies:
+queries:
+software:
+`, name, name)
+ }
+
+ workstations := team("💻 Workstations")
+ iosTeam := team("📱🏢 Company-owned iPhones")
+ ipadTeam := team("🔳🏢 Company-owned iPads")
+
+ cases := []struct {
+ name string
+ cfgs []string
+ dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
+ realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
+ tokens []*fleet.ABMToken
+ }{
+ {
+ name: "backwards compat",
+ cfgs: []string{
+ global("apple_bm_default_team: 💻 Workstations"),
+ workstations,
+ },
+ tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Equal(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam, "💻 Workstations")
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ // {
+ // name: "deprecated config with two tokens in the db fails",
+ // cfgs: []string{
+ // global("apple_bm_default_team: 💻 Workstations"),
+ // workstations,
+ // },
+ // tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Second Token LLC"}},
+ // dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ // require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
+ // assert.Empty(t, appCfg.MDM.AppleBussinessManager.Value)
+ // assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ // assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ // },
+ // realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ // require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
+ // assert.Empty(t, appCfg.MDM.AppleBussinessManager.Value)
+ // assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ // assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ // },
+ // },
+ {
+ name: "new key all valid",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"`),
+ workstations,
+ iosTeam,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.AppleBusinessManager.Value,
+ []fleet.MDMAppleABMAssignmentInfo{
+ {
+ OrganizationName: "Fleet Device Management Inc.",
+ MacOSTeam: "💻 Workstations",
+ IOSTeam: "📱🏢 Company-owned iPhones",
+ IpadOSTeam: "🔳🏢 Company-owned iPads",
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "new key multiple elements",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Foo Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"`),
+ workstations,
+ iosTeam,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.AppleBusinessManager.Value,
+ []fleet.MDMAppleABMAssignmentInfo{
+ {
+ OrganizationName: "Fleet Device Management Inc.",
+ MacOSTeam: "💻 Workstations",
+ IOSTeam: "📱🏢 Company-owned iPhones",
+ IpadOSTeam: "🔳🏢 Company-owned iPads",
+ },
+ {
+ OrganizationName: "Foo Inc.",
+ MacOSTeam: "💻 Workstations",
+ IOSTeam: "📱🏢 Company-owned iPhones",
+ IpadOSTeam: "🔳🏢 Company-owned iPads",
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "both keys errors",
+ cfgs: []string{
+ global(`
+ apple_bm_default_team: "💻 Workstations"
+ apple_business_manager:
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"`),
+ workstations,
+ iosTeam,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
+ assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ require.ErrorContains(t, err, "mdm.apple_bm_default_team has been deprecated")
+ assert.NotContains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "using an undefined team errors",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"`),
+ workstations,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "apple_business_manager team \"📱🏢 Company-owned iPhones\" not found in team configs")
+ },
+ },
+ {
+ name: "no team is supported",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "No team"
+ ios_team: "No team"
+ ipados_team: "No team"`),
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.AppleBusinessManager.Value,
+ []fleet.MDMAppleABMAssignmentInfo{
+ {
+ OrganizationName: "Fleet Device Management Inc.",
+ MacOSTeam: "No team",
+ IOSTeam: "No team",
+ IpadOSTeam: "No team",
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "not provided teams defaults to no team",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "No team"
+ ios_team: ""`),
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.AppleBusinessManager.Value,
+ []fleet.MDMAppleABMAssignmentInfo{
+ {
+ OrganizationName: "Fleet Device Management Inc.",
+ MacOSTeam: "No team",
+ IOSTeam: "",
+ IpadOSTeam: "",
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "non existent org name fails",
+ cfgs: []string{
+ global(`
+ apple_business_manager:
+ - organization_name: Does not exist
+ macos_team: "No team"`),
+ },
+ tokens: []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}},
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "token with organization name Does not exist doesn't exist")
+ assert.Empty(t, appCfg.MDM.AppleBusinessManager.Value)
+ assert.Empty(t, appCfg.MDM.DeprecatedAppleBMDefaultTeam)
+ assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ if len(tt.tokens) > 0 {
+ return tt.tokens, nil
+ }
+ return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
+ }
+
+ ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
+ var res []*fleet.TeamSummary
+ for _, tm := range savedTeams {
+ res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
+ }
+ return res, nil
+ }
+
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ args := []string{"gitops"}
+ for _, cfg := range tt.cfgs {
+ if cfg != "" {
+ tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = tmpFile.WriteString(cfg)
+ require.NoError(t, err)
+ args = append(args, "-f", tmpFile.Name())
+ }
+ }
+
+ // Dry run
+ out, err := runAppNoChecks(append(args, "--dry-run"))
+ tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+ if t.Failed() {
+ t.FailNow()
+ }
+
+ // Real run
+ out, err = runAppNoChecks(args)
+ tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+
+ // Second real run, now that all the teams are saved
+ out, err = runAppNoChecks(args)
+ tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+ })
+ }
+}
+
+func TestGitOpsVPP(t *testing.T) {
+ global := func(mdm string) string {
+ return fmt.Sprintf(`
+controls:
+queries:
+policies:
+agent_options:
+software:
+org_settings:
+ server_settings:
+ server_url: "https://foo.example.com"
+ org_info:
+ org_name: GitOps Test
+ secrets:
+ - secret: "global"
+ mdm:
+ %s
+ `, mdm)
+ }
+
+ team := func(name string) string {
+ return fmt.Sprintf(`
+name: %s
+team_settings:
+ secrets:
+ - secret: "%s-secret"
+agent_options:
+controls:
+policies:
+queries:
+software:
+`, name, name)
+ }
+
+ workstations := team("💻 Workstations")
+ iosTeam := team("📱🏢 Company-owned iPhones")
+ ipadTeam := team("🔳🏢 Company-owned iPads")
+
+ cases := []struct {
+ name string
+ cfgs []string
+ dryRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
+ realRunAssertion func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error)
+ }{
+ {
+ name: "new key all valid",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:
+ - "💻 Workstations"
+ - "📱🏢 Company-owned iPhones"
+ - "🔳🏢 Company-owned iPads"`),
+ workstations,
+ iosTeam,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.VolumePurchasingProgram.Value,
+ []fleet.MDMAppleVolumePurchasingProgramInfo{
+ {
+ Location: "Fleet Device Management Inc.",
+ Teams: []string{
+ "💻 Workstations",
+ "📱🏢 Company-owned iPhones",
+ "🔳🏢 Company-owned iPads",
+ },
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "new key multiple elements",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Acme Inc.
+ teams:
+ - "💻 Workstations"
+ - location: Fleet Device Management Inc.
+ teams:
+ - "📱🏢 Company-owned iPhones"
+ - "🔳🏢 Company-owned iPads"`),
+ workstations,
+ iosTeam,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.VolumePurchasingProgram.Value,
+ []fleet.MDMAppleVolumePurchasingProgramInfo{
+ {
+ Location: "Acme Inc.",
+ Teams: []string{
+ "💻 Workstations",
+ },
+ },
+ {
+ Location: "Fleet Device Management Inc.",
+ Teams: []string{
+ "📱🏢 Company-owned iPhones",
+ "🔳🏢 Company-owned iPads",
+ },
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "using an undefined team errors",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:
+ - "💻 Workstations"
+ - "📱🏢 Company-owned iPhones"
+ - "🔳🏢 Company-owned iPads"`),
+ workstations,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "volume_purchasing_program team 📱🏢 Company-owned iPhones not found in team configs")
+ },
+ },
+ {
+ name: "no team is supported",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:
+ - "💻 Workstations"
+ - "📱🏢 Company-owned iPhones"
+ - "No team"`),
+ workstations,
+ iosTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.VolumePurchasingProgram.Value,
+ []fleet.MDMAppleVolumePurchasingProgramInfo{
+ {
+ Location: "Fleet Device Management Inc.",
+ Teams: []string{
+ "💻 Workstations",
+ "📱🏢 Company-owned iPhones",
+ "No team",
+ },
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "all teams is supported",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:
+ - "All teams"`),
+ workstations,
+ iosTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.VolumePurchasingProgram.Value,
+ []fleet.MDMAppleVolumePurchasingProgramInfo{
+ {
+ Location: "Fleet Device Management Inc.",
+ Teams: []string{
+ "All teams",
+ },
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "not provided teams defaults to no team",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:`),
+ workstations,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.Contains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.NoError(t, err)
+ assert.ElementsMatch(
+ t,
+ appCfg.MDM.VolumePurchasingProgram.Value,
+ []fleet.MDMAppleVolumePurchasingProgramInfo{
+ {
+ Location: "Fleet Device Management Inc.",
+ Teams: nil,
+ },
+ },
+ )
+ assert.Contains(t, out, "[!] gitops succeeded")
+ },
+ },
+ {
+ name: "non existent location fails",
+ cfgs: []string{
+ global(`
+ volume_purchasing_program:
+ - location: Does not exist
+ teams:`),
+ workstations,
+ ipadTeam,
+ },
+ dryRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ },
+ realRunAssertion: func(t *testing.T, appCfg *fleet.AppConfig, ds fleet.Datastore, out string, err error) {
+ assert.ErrorContains(t, err, "token with location Does not exist doesn't exist")
+ assert.Empty(t, appCfg.MDM.VolumePurchasingProgram.Value)
+ assert.NotContains(t, out, "[!] gitops dry run succeeded")
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ ds, savedAppConfigPtr, savedTeams := setupFullGitOpsPremiumServer(t)
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{{Location: "Fleet Device Management Inc."}, {Location: "Acme Inc."}}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
+ }
+
+ ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
+ var res []*fleet.TeamSummary
+ for _, tm := range savedTeams {
+ res = append(res, &fleet.TeamSummary{Name: (*tm).Name, ID: (*tm).ID})
+ }
+ return res, nil
+ }
+
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ args := []string{"gitops"}
+ for _, cfg := range tt.cfgs {
+ if cfg != "" {
+ tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
+ require.NoError(t, err)
+ _, err = tmpFile.WriteString(cfg)
+ require.NoError(t, err)
+ args = append(args, "-f", tmpFile.Name())
+ }
+ }
+
+ // Dry run
+ out, err := runAppNoChecks(append(args, "--dry-run"))
+ tt.dryRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+ if t.Failed() {
+ t.FailNow()
+ }
+
+ // Real run
+ out, err = runAppNoChecks(args)
+ tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+
+ // Second real run, now that all the teams are saved
+ out, err = runAppNoChecks(args)
+ tt.realRunAssertion(t, *savedAppConfigPtr, ds, out.String(), err)
+ })
+ }
+}
+
+type memKeyValueStore struct {
+ m sync.Map
+}
+
+func newMemKeyValueStore() *memKeyValueStore {
+ return &memKeyValueStore{}
+}
+
+func (m *memKeyValueStore) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
+ m.m.Store(key, value)
+ return nil
+}
+
+func (m *memKeyValueStore) Get(ctx context.Context, key string) (*string, error) {
+ v, ok := m.m.Load(key)
+ if !ok {
+ return nil, nil
+ }
+ vAsString := v.(string)
+ return &vAsString, nil
}
diff --git a/cmd/fleetctl/hosts_test.go b/cmd/fleetctl/hosts_test.go
index a24fc79cbe..6220d6d75c 100644
--- a/cmd/fleetctl/hosts_test.go
+++ b/cmd/fleetctl/hosts_test.go
@@ -43,8 +43,9 @@ func TestHostsTransferByHosts(t *testing.T) {
return nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
@@ -114,8 +115,9 @@ func TestHostsTransferByLabel(t *testing.T) {
return nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
@@ -184,8 +186,9 @@ func TestHostsTransferByStatus(t *testing.T) {
return nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
@@ -243,8 +246,9 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
return nil
}
- ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
- return nil
+ ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
+ ) (updates fleet.MDMProfilesUpdates, err error) {
+ return fleet.MDMProfilesUpdates{}, nil
}
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
diff --git a/cmd/fleetctl/preview.go b/cmd/fleetctl/preview.go
index a48734871c..ef06f3c8cc 100644
--- a/cmd/fleetctl/preview.go
+++ b/cmd/fleetctl/preview.go
@@ -74,7 +74,7 @@ func (d dockerCompose) Command(arg ...string) *exec.Cmd {
func newDockerCompose() (dockerCompose, error) {
// first, check if `docker compose` is available
- if err := exec.Command("docker compose").Run(); err == nil {
+ if err := exec.Command("docker", "compose").Run(); err == nil {
return dockerCompose{dockerComposeV2}, nil
}
@@ -387,7 +387,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
}
// this only applies standard queries, the base directory is not used,
// so pass in the current working directory.
- _, err = client.ApplyGroup(c.Context, specs, ".", logf, fleet.ApplyClientSpecOptions{})
+ _, _, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
if err != nil {
return err
}
diff --git a/cmd/fleetctl/preview_test.go b/cmd/fleetctl/preview_test.go
index 76c68eb190..c3a75a73e1 100644
--- a/cmd/fleetctl/preview_test.go
+++ b/cmd/fleetctl/preview_test.go
@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestPreview(t *testing.T) {
+func TestIntegrationsPreview(t *testing.T) {
nettest.Run(t)
t.Setenv("FLEET_SERVER_ADDRESS", "https://localhost:8412")
@@ -74,6 +74,7 @@ func gitRootPath(t *testing.T) string {
}
func TestDockerCompose(t *testing.T) {
+ t.Parallel()
t.Run("returns the right command according to the version", func(t *testing.T) {
v1 := dockerCompose{dockerComposeV1}
cmd1 := v1.Command("up")
diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json
index 399e880a93..ece537e7b9 100644
--- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json
+++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json
@@ -96,7 +96,8 @@
"apple_bm_terms_expired": false,
"apple_bm_enabled_and_configured": false,
"enabled_and_configured": false,
- "apple_bm_default_team": "",
+ "apple_business_manager": null,
+ "volume_purchasing_program": null,
"windows_enabled_and_configured": false,
"enable_disk_encryption": false,
"macos_updates": {
diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
index b76d40bd52..5f19ffcb8a 100644
--- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
+++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
@@ -21,7 +21,8 @@ spec:
apple_bm_terms_expired: false
apple_bm_enabled_and_configured: false
enabled_and_configured: false
- apple_bm_default_team: ""
+ apple_business_manager: null
+ volume_purchasing_program: null
windows_enabled_and_configured: false
enable_disk_encryption: false
macos_migration:
diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
index 76a89e6493..9fa625b676 100644
--- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
+++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
@@ -46,7 +46,8 @@
"enable_software_inventory": false
},
"mdm": {
- "apple_bm_default_team": "",
+ "apple_business_manager": null,
+ "volume_purchasing_program": null,
"apple_bm_terms_expired": false,
"apple_bm_enabled_and_configured": false,
"enabled_and_configured": false,
diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
index f6ca79a4eb..b28ab395b3 100644
--- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
+++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
@@ -18,7 +18,8 @@ spec:
jira: null
zendesk: null
mdm:
- apple_bm_default_team: ""
+ apple_business_manager: null
+ volume_purchasing_program: null
apple_bm_enabled_and_configured: false
apple_bm_terms_expired: false
enabled_and_configured: false
diff --git a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml
index 76936e3ad5..894841a933 100644
--- a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml
+++ b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml
@@ -146,7 +146,6 @@ org_settings:
"private_key": "google_calendar_private_key",
}
mdm:
- apple_bm_default_team: $APPLE_BM_DEFAULT_TEAM
end_user_authentication:
entity_id: ""
idp_name: ""
@@ -187,3 +186,4 @@ org_settings:
secrets: # These secrets are used to enroll hosts to the "All teams" team
- secret: SampleSecret123
- secret: ABC
+software:
diff --git a/cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml b/cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml
index 177c1c80cf..b098442585 100644
--- a/cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml
+++ b/cmd/fleetctl/testdata/gitops/global_macos_custom_settings_valid_deprecated.yml
@@ -91,3 +91,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
+software:
\ No newline at end of file
diff --git a/cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml b/cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml
index da75847cd5..e6231bf030 100644
--- a/cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml
+++ b/cmd/fleetctl/testdata/gitops/global_macos_windows_custom_settings_valid.yml
@@ -97,3 +97,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
+software:
diff --git a/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml
index 9d2ac6e69f..1a100de6f3 100644
--- a/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml
+++ b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_invalid_label_mix.yml
@@ -93,3 +93,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
+software:
\ No newline at end of file
diff --git a/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml
index ba1d06f784..5208ba7248 100644
--- a/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml
+++ b/cmd/fleetctl/testdata/gitops/global_windows_custom_settings_unknown_label.yml
@@ -91,3 +91,4 @@ org_settings:
databases_path: ""
secrets:
- secret: ABC
+software:
\ No newline at end of file
diff --git a/cmd/fleetctl/testdata/gitops/lib/uninstall_ruby.sh b/cmd/fleetctl/testdata/gitops/lib/uninstall_ruby.sh
new file mode 100644
index 0000000000..c6c41b5e01
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/lib/uninstall_ruby.sh
@@ -0,0 +1 @@
+echo 'uninstall' ${PACKAGE_ID}
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml
new file mode 100644
index 0000000000..58bae27ae9
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml
@@ -0,0 +1,8 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
+ install_script:
+ path: lib/notfound.sh
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml
new file mode 100644
index 0000000000..b333e7816e
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml
@@ -0,0 +1,7 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
+ self_service: "not a boolean"
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml
new file mode 100644
index 0000000000..d897af7b43
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml
@@ -0,0 +1,11 @@
+name: No TEAM
+controls:
+policies:
+software:
+ packages:
+ - install_script:
+ path: lib/install_ruby.sh
+ pre_install_query:
+ path: lib/query_ruby.yml
+ post_install_script:
+ path: lib/post_install_ruby.sh
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml
new file mode 100644
index 0000000000..590458e78b
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml
@@ -0,0 +1,6 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/notfound.deb
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml
new file mode 100644
index 0000000000..12b2598d59
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml
@@ -0,0 +1,10 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
+ install_script:
+ path: lib/install_ruby.sh
+ post_install_script:
+ path: lib/notfound.sh
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml
new file mode 100644
index 0000000000..15ddcb438c
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml
@@ -0,0 +1,12 @@
+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_multiple.yml
+ post_install_script:
+ path: lib/post_install_ruby.sh
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml
new file mode 100644
index 0000000000..48e6ff42e5
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml
@@ -0,0 +1,10 @@
+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/notfound.yml
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml
new file mode 100644
index 0000000000..23ba8dbe80
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml
@@ -0,0 +1,6 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_uninstall_not_found.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_uninstall_not_found.yml
new file mode 100644
index 0000000000..c5c8838267
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_uninstall_not_found.yml
@@ -0,0 +1,8 @@
+name: No team
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
+ uninstall_script:
+ path: lib/notfound.sh
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml
new file mode 100644
index 0000000000..ace876a8d5
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml
@@ -0,0 +1,6 @@
+name: "No team"
+controls:
+policies:
+software:
+ packages:
+ - url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
diff --git a/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml b/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml
new file mode 100644
index 0000000000..4599698d1d
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml
@@ -0,0 +1,16 @@
+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
+ - url: ${SOFTWARE_INSTALLER_URL}/other.deb
+ self_service: true
diff --git a/cmd/fleetctl/testdata/gitops/team_config_no_paths.yml b/cmd/fleetctl/testdata/gitops/team_config_no_paths.yml
index 785ba5d215..e671d17d29 100644
--- a/cmd/fleetctl/testdata/gitops/team_config_no_paths.yml
+++ b/cmd/fleetctl/testdata/gitops/team_config_no_paths.yml
@@ -124,5 +124,7 @@ software:
path: lib/query_ruby.yml
post_install_script:
path: lib/post_install_ruby.sh
+ uninstall_script:
+ path: lib/uninstall_ruby.sh
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
self_service: true
diff --git a/cmd/fleetctl/testdata/gitops/team_software_installer_uninstall_not_found.yml b/cmd/fleetctl/testdata/gitops/team_software_installer_uninstall_not_found.yml
new file mode 100644
index 0000000000..1fc9903d6b
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/team_software_installer_uninstall_not_found.yml
@@ -0,0 +1,19 @@
+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
+ uninstall_script:
+ path: lib/notfound.sh
diff --git a/cmd/fleetctl/testdata/gitops/team_software_installer_valid.yml b/cmd/fleetctl/testdata/gitops/team_software_installer_valid.yml
index e894112249..0733758ced 100644
--- a/cmd/fleetctl/testdata/gitops/team_software_installer_valid.yml
+++ b/cmd/fleetctl/testdata/gitops/team_software_installer_valid.yml
@@ -21,5 +21,7 @@ software:
path: lib/query_ruby.yml
post_install_script:
path: lib/post_install_ruby.sh
+ uninstall_script:
+ path: lib/uninstall_ruby.sh
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
self_service: true
diff --git a/cmd/fleetctl/testdata/gitops/team_vpp_empty_adamid.yml b/cmd/fleetctl/testdata/gitops/team_vpp_empty_adamid.yml
new file mode 100644
index 0000000000..675618c609
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/team_vpp_empty_adamid.yml
@@ -0,0 +1,17 @@
+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:
diff --git a/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml b/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml
new file mode 100644
index 0000000000..74e7b17806
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml
@@ -0,0 +1,17 @@
+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
diff --git a/cmd/fleetctl/testdata/gitops/team_vpp_valid_app_self_service.yml b/cmd/fleetctl/testdata/gitops/team_vpp_valid_app_self_service.yml
new file mode 100644
index 0000000000..a2daad396e
--- /dev/null
+++ b/cmd/fleetctl/testdata/gitops/team_vpp_valid_app_self_service.yml
@@ -0,0 +1,18 @@
+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"
+ self_service: true
diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
index fc886f7658..02adaad3ac 100644
--- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
+++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
@@ -18,7 +18,8 @@ spec:
jira: null
zendesk: null
mdm:
- apple_bm_default_team: ""
+ apple_business_manager:
+ volume_purchasing_program:
apple_bm_enabled_and_configured: false
apple_bm_terms_expired: false
enabled_and_configured: true
diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
index 5a57cc2500..2dd2f93adf 100644
--- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
+++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
@@ -18,7 +18,8 @@ spec:
jira: null
zendesk: null
mdm:
- apple_bm_default_team: ""
+ apple_business_manager:
+ volume_purchasing_program:
apple_bm_enabled_and_configured: false
apple_bm_terms_expired: false
enabled_and_configured: true
diff --git a/cmd/fleetctl/testing_utils.go b/cmd/fleetctl/testing_utils.go
index 7a278ebc3c..918a12b94e 100644
--- a/cmd/fleetctl/testing_utils.go
+++ b/cmd/fleetctl/testing_utils.go
@@ -127,24 +127,24 @@ func runServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http
require.NoError(t, err)
ds.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
return map[fleet.MDMAssetName]string{
- fleet.MDMAssetABMCert: "abmcert",
- fleet.MDMAssetABMKey: "abmkey",
- fleet.MDMAssetABMToken: "abmtoken",
- fleet.MDMAssetAPNSCert: "apnscert",
- fleet.MDMAssetAPNSKey: "apnskey",
- fleet.MDMAssetCACert: "scepcert",
- fleet.MDMAssetCAKey: "scepkey",
+ fleet.MDMAssetABMCert: "abmcert",
+ fleet.MDMAssetABMKey: "abmkey",
+ fleet.MDMAssetABMTokenDeprecated: "abmtoken",
+ fleet.MDMAssetAPNSCert: "apnscert",
+ fleet.MDMAssetAPNSKey: "apnskey",
+ fleet.MDMAssetCACert: "scepcert",
+ fleet.MDMAssetCAKey: "scepkey",
}, nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
- fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
- fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
- fleet.MDMAssetABMToken: {Name: fleet.MDMAssetABMToken, Value: tokenBytes},
- fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: apnsCert},
- fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: apnsKey},
- fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
- fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
+ fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
+ fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
+ fleet.MDMAssetABMTokenDeprecated: {Name: fleet.MDMAssetABMTokenDeprecated, Value: tokenBytes},
+ fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: apnsCert},
+ fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: apnsKey},
+ fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
+ fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
}, nil
}
diff --git a/cmd/fleetctl/vulnerability_data_stream_test.go b/cmd/fleetctl/vulnerability_data_stream_test.go
index 0e61949eea..d1316a3251 100644
--- a/cmd/fleetctl/vulnerability_data_stream_test.go
+++ b/cmd/fleetctl/vulnerability_data_stream_test.go
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestVulnerabilityDataStream(t *testing.T) {
+func TestIntegrationsVulnerabilityDataStream(t *testing.T) {
nettest.Run(t)
runAppCheckErr(t, []string{"vulnerability-data-stream"}, "No directory provided")
diff --git a/cmd/osquery-perf/README.md b/cmd/osquery-perf/README.md
index 1641283d59..eb179dd13c 100644
--- a/cmd/osquery-perf/README.md
+++ b/cmd/osquery-perf/README.md
@@ -111,3 +111,11 @@ Example of running the agent with MDM. Note that `enroll_secret` is not needed f
```
go run agent.go --os_templates ipad_13.18,iphone_14.6 --host_count 10 --mdm_scep_challenge 0d53306e-6d7a-9d14-a372-f9e53f9d62db
```
+
+## Installing software
+
+The agent can install software for "macos", "ubuntu", and "windows" OSs when running with orbit agent. The following options control the installation behavior:
+
+- `--software_installer_pre_install_fail_prob`: default 0.05, `select 1` always passes and `select 0` always fails
+- `--software_installer_install_fail_prob`: default 0.05, `exit 0` always passes and `exit 1` always fails
+- `--software_installer_post_install_fail_prob`: default 0.05, `exit 0` always passes and `exit 1` always fails
diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go
index 04851dfc41..10e8ee21b0 100644
--- a/cmd/osquery-perf/agent.go
+++ b/cmd/osquery-perf/agent.go
@@ -25,6 +25,8 @@ import (
"text/template"
"time"
+ "github.com/fleetdm/fleet/v4/cmd/osquery-perf/installer_cache"
+ "github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
@@ -54,6 +56,8 @@ var (
vsCodeExtensionsVulnerableSoftware []fleet.Software
windowsSoftware []map[string]string
ubuntuSoftware []map[string]string
+
+ installerMetadataCache installer_cache.Metadata
)
func loadMacOSVulnerableSoftware() {
@@ -490,6 +494,11 @@ type agent struct {
softwareQueryFailureProb float64
softwareVSCodeExtensionsFailProb float64
+ softwareInstaller softwareInstaller
+
+ // Software installed on the host via Fleet. Key is the software name + version + bundle identifier.
+ installedSoftware sync.Map
+
//
// The following are exported to be used by the templates.
//
@@ -544,6 +553,12 @@ type softwareExtraEntityCount struct {
uniqueSoftwareUninstallCount int
uniqueSoftwareUninstallProb float64
}
+type softwareInstaller struct {
+ preInstallFailureProb float64
+ installFailureProb float64
+ postInstallFailureProb float64
+ mu *sync.Mutex
+}
func newAgent(
agentIndex int,
@@ -552,6 +567,7 @@ func newAgent(
configInterval, logInterval, queryInterval, mdmCheckInInterval time.Duration,
softwareQueryFailureProb float64,
softwareVSCodeExtensionsQueryFailureProb float64,
+ softwareInstaller softwareInstaller,
softwareCount softwareEntityCount,
softwareVSCodeExtensionsCount softwareExtraEntityCount,
userCount entityCount,
@@ -642,6 +658,7 @@ func newAgent(
softwareQueryFailureProb: softwareQueryFailureProb,
softwareVSCodeExtensionsFailProb: softwareVSCodeExtensionsQueryFailureProb,
+ softwareInstaller: softwareInstaller,
macMDMClient: macMDMClient,
winMDMClient: winMDMClient,
@@ -967,6 +984,11 @@ func (a *agent) runOrbitLoop() {
// that will simulate executing them.
go a.execScripts(cfg.Notifications.PendingScriptExecutionIDs, orbitClient)
}
+ if len(cfg.Notifications.PendingSoftwareInstallerIDs) > 0 {
+ // there are pending software installations on this host, start a
+ // goroutine that will download the software
+ go a.installSoftware(cfg.Notifications.PendingSoftwareInstallerIDs, orbitClient)
+ }
if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment &&
!a.mdmEnrolled() &&
a.winMDMClient != nil &&
@@ -1242,6 +1264,130 @@ func (a *agent) execScripts(execIDs []string, orbitClient *service.OrbitClient)
}
}
+func (a *agent) installSoftware(installerIDs []string, orbitClient *service.OrbitClient) {
+ // Only allow one software install to happen at a time.
+ if a.softwareInstaller.mu.TryLock() {
+ defer a.softwareInstaller.mu.Unlock()
+ for _, installerID := range installerIDs {
+ a.installSoftwareItem(installerID, orbitClient)
+ }
+ }
+}
+
+func (a *agent) installSoftwareItem(installerID string, orbitClient *service.OrbitClient) {
+ payload := &fleet.HostSoftwareInstallResultPayload{}
+ payload.InstallUUID = installerID
+ installer, err := orbitClient.GetInstallerDetails(installerID)
+ if err != nil {
+ log.Println("get installer details:", err)
+ return
+ }
+ failed := false
+ if installer.PreInstallCondition != "" {
+ time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
+ if installer.PreInstallCondition == "select 1" {
+ // Always pass
+ payload.PreInstallConditionOutput = ptr.String("1")
+ } else if installer.PreInstallCondition == "select 0" ||
+ a.softwareInstaller.preInstallFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.preInstallFailureProb {
+ // Fail
+ payload.PreInstallConditionOutput = ptr.String("")
+ failed = true
+ } else {
+ payload.PreInstallConditionOutput = ptr.String("1")
+ }
+ }
+
+ var meta *file.InstallerMetadata
+ if !failed {
+ var cacheMiss bool
+ // Download the file if needed to get its metadata
+ meta, cacheMiss, err = installerMetadataCache.Get(installer.InstallerID, orbitClient)
+ if err != nil {
+ return
+ }
+
+ if !cacheMiss {
+ // If we didn't download and analyze the file, we do a download and don't save the result
+ err = orbitClient.DownloadAndDiscardSoftwareInstaller(installer.InstallerID)
+ if err != nil {
+ log.Println("download and discard software installer:", err)
+ return
+ }
+ }
+
+ time.Sleep(time.Duration(rand.Intn(30)) * time.Second)
+ if installer.InstallScript == "exit 0" {
+ // Always pass
+ payload.InstallScriptExitCode = ptr.Int(0)
+ payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (always pass)")
+ } else if installer.InstallScript == "exit 1" {
+ payload.InstallScriptExitCode = ptr.Int(1)
+ payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (always fail)")
+ failed = true
+ } else if a.softwareInstaller.installFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.installFailureProb {
+ payload.InstallScriptExitCode = ptr.Int(1)
+ payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (fail)")
+ failed = true
+ } else {
+ payload.InstallScriptExitCode = ptr.Int(0)
+ payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (pass)")
+ }
+ }
+ if !failed {
+ if meta.Name == "" {
+ log.Printf("WARNING: installer metadata is missing a name for installer:%d\n", installer.InstallerID)
+ } else {
+ key := meta.Name + "+" + meta.Version + "+" + meta.BundleIdentifier
+ if _, ok := a.installedSoftware.Load(key); !ok {
+ source := ""
+ switch a.os {
+ case "macos":
+ source = "apps"
+ case "windows":
+ source = "programs"
+ case "ubuntu":
+ source = "deb_packages"
+ default:
+ log.Printf("unknown OS to software installer: %s", a.os)
+ return
+ }
+ a.installedSoftware.Store(key, map[string]string{
+ "name": meta.Name,
+ "version": meta.Version,
+ "bundle_identifier": meta.BundleIdentifier,
+ "source": source,
+ "installed_path": os.DevNull,
+ })
+ }
+ }
+
+ if installer.PostInstallScript != "" {
+ time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
+ if installer.PostInstallScript == "exit 0" {
+ // Always pass
+ payload.PostInstallScriptExitCode = ptr.Int(0)
+ payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (always pass)")
+ } else if installer.PostInstallScript == "exit 1" {
+ payload.PostInstallScriptExitCode = ptr.Int(1)
+ payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (always fail)")
+ } else if a.softwareInstaller.postInstallFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.postInstallFailureProb {
+ payload.PostInstallScriptExitCode = ptr.Int(1)
+ payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (fail)")
+ } else {
+ payload.PostInstallScriptExitCode = ptr.Int(0)
+ payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (pass)")
+ }
+ }
+ }
+
+ err = orbitClient.SaveInstallerResult(payload)
+ if err != nil {
+ log.Println("save installer result:", err)
+ return
+ }
+}
+
func (a *agent) waitingDo(fn func() *http.Request) *http.Response {
response, err := http.DefaultClient.Do(fn())
for err != nil || response.StatusCode != http.StatusOK {
@@ -1525,6 +1671,10 @@ func (a *agent) softwareMacOS() []map[string]string {
}
software := append(commonSoftware, uniqueSoftware...)
software = append(software, randomVulnerableSoftware...)
+ a.installedSoftware.Range(func(key, value interface{}) bool {
+ software = append(software, value.(map[string]string))
+ return true
+ })
rand.Shuffle(len(software), func(i, j int) {
software[i], software[j] = software[j], software[i]
})
@@ -2023,6 +2173,10 @@ func (a *agent) processQuery(name, query string) (
}
if ss == fleet.StatusOK {
results = windowsSoftware
+ a.installedSoftware.Range(func(key, value interface{}) bool {
+ results = append(results, value.(map[string]string))
+ return true
+ })
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_linux":
@@ -2034,6 +2188,10 @@ func (a *agent) processQuery(name, query string) (
switch a.os {
case "ubuntu":
results = ubuntuSoftware
+ a.installedSoftware.Range(func(key, value interface{}) bool {
+ results = append(results, value.(map[string]string))
+ return true
+ })
}
}
return true, results, &ss, nil, nil
@@ -2351,7 +2509,7 @@ func main() {
// osquery-perf will send log requests with results only if there are scheduled queries configured AND it's their time to run.
logInterval = flag.Duration("logger_tls_period", 10*time.Second, "Interval for scheduled queries log requests")
queryInterval = flag.Duration("query_interval", 10*time.Second, "Interval for distributed query requests")
- mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 10*time.Second, "Interval for performing MDM check-ins (applies to both macOS and Windows)")
+ mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 1*time.Minute, "Interval for performing MDM check-ins (applies to both macOS and Windows)")
onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled")
nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use")
@@ -2361,6 +2519,13 @@ func main() {
softwareQueryFailureProb = flag.Float64("software_query_fail_prob", 0.5, "Probability of the software query failing")
softwareVSCodeExtensionsQueryFailureProb = flag.Float64("software_vscode_extensions_query_fail_prob", 0.0, "Probability of the software vscode_extensions query failing")
+ softwareInstallerPreInstallFailureProb = flag.Float64("software_installer_pre_install_fail_prob", 0.05,
+ "Probability of the pre-install query failing")
+ softwareInstallerInstallFailureProb = flag.Float64("software_installer_install_fail_prob", 0.05,
+ "Probability of the install script failing")
+ softwareInstallerPostInstallFailureProb = flag.Float64("software_installer_post_install_fail_prob", 0.05,
+ "Probability of the post-install script failing")
+
commonSoftwareCount = flag.Int("common_software_count", 10, "Number of common installed applications reported to fleet")
commonVSCodeExtensionsSoftwareCount = flag.Int("common_vscode_extensions_software_count", 5, "Number of common vscode_extensions installed applications reported to fleet")
commonSoftwareUninstallCount = flag.Int("common_software_uninstall_count", 1, "Number of common software to uninstall")
@@ -2527,6 +2692,12 @@ func main() {
*mdmCheckInInterval,
*softwareQueryFailureProb,
*softwareVSCodeExtensionsQueryFailureProb,
+ softwareInstaller{
+ preInstallFailureProb: *softwareInstallerPreInstallFailureProb,
+ installFailureProb: *softwareInstallerInstallFailureProb,
+ postInstallFailureProb: *softwareInstallerPostInstallFailureProb,
+ mu: new(sync.Mutex),
+ },
softwareEntityCount{
entityCount: entityCount{
common: *commonSoftwareCount,
diff --git a/cmd/osquery-perf/installer_cache/installer-cache.go b/cmd/osquery-perf/installer_cache/installer-cache.go
new file mode 100644
index 0000000000..57994de5f6
--- /dev/null
+++ b/cmd/osquery-perf/installer_cache/installer-cache.go
@@ -0,0 +1,66 @@
+package installer_cache
+
+import (
+ "log"
+ "os"
+ "sync"
+
+ "github.com/fleetdm/fleet/v4/pkg/file"
+ "github.com/fleetdm/fleet/v4/server/service"
+)
+
+// Metadata holds the metadata for software installers.
+// To extract the metadata, we must download the file. Once the file has been downloaded once and analyzed,
+// the other agents can use the cache to get the appropriate metadata.
+type Metadata struct {
+ mu sync.Mutex
+ cache map[uint]*file.InstallerMetadata
+}
+
+func (c *Metadata) Get(key uint, orbitClient *service.OrbitClient) (meta *file.InstallerMetadata,
+ cacheMiss bool, err error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.cache == nil {
+ c.cache = make(map[uint]*file.InstallerMetadata, 1)
+ }
+
+ meta, ok := c.cache[key]
+ if !ok {
+ var err error
+ meta, err = populateMetadata(orbitClient, key)
+ if err != nil {
+ return nil, false, err
+ }
+ c.cache[key] = meta
+ cacheMiss = true
+ }
+ return meta, cacheMiss, nil
+}
+
+func populateMetadata(orbitClient *service.OrbitClient, installerID uint) (*file.InstallerMetadata, error) {
+ tmpDir, err := os.MkdirTemp("", "")
+ if err != nil {
+ log.Println("create temp dir:", err)
+ return nil, err
+ }
+ defer os.RemoveAll(tmpDir)
+ path, err := orbitClient.DownloadSoftwareInstaller(installerID, tmpDir)
+ if err != nil {
+ log.Println("download software installer:", err)
+ return nil, err
+ }
+ // Figure out what we're actually installing here and add it to software inventory
+ f, err := os.Open(path)
+ if err != nil {
+ log.Println("open installer:", err)
+ return nil, err
+ }
+ defer f.Close()
+ item, err := file.ExtractInstallerMetadata(f)
+ if err != nil {
+ log.Println("extract installer metadata:", err)
+ return nil, err
+ }
+ return item, nil
+}
diff --git a/cmd/osquery-perf/macos_13.6.2.tmpl b/cmd/osquery-perf/macos_13.6.2.tmpl
index 8f59d00e9e..83ee06cc5f 100644
--- a/cmd/osquery-perf/macos_13.6.2.tmpl
+++ b/cmd/osquery-perf/macos_13.6.2.tmpl
@@ -173,6 +173,10 @@
{{- end }}
{{- end }}
+{{ define "fleet_detail_query_mdm_config_profiles_darwin" -}}
+[]
+{{- end }}
+
{{/* all hosts */}}
{{ define "fleet_label_query_6" -}}
[
diff --git a/cmd/osquery-perf/macos_14.1.2.tmpl b/cmd/osquery-perf/macos_14.1.2.tmpl
index 8aa29e1c4a..4c768d34c1 100644
--- a/cmd/osquery-perf/macos_14.1.2.tmpl
+++ b/cmd/osquery-perf/macos_14.1.2.tmpl
@@ -174,6 +174,10 @@
{{- end }}
{{- end }}
+{{ define "fleet_detail_query_mdm_config_profiles_darwin" -}}
+[]
+{{- end }}
+
{{/* all hosts */}}
{{ define "fleet_label_query_6" -}}
[
diff --git a/codecov.yml b/codecov.yml
index d0a398457d..cac1d5c0d1 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -22,3 +22,7 @@ flag_management:
- name: frontend
paths:
- frontend/
+
+ignore:
+ - "server/mock"
+ - "server/fleet/activities.go" # mostly contains code for documentation -- not interesting for tests
diff --git a/docker-compose.yml b/docker-compose.yml
index 437e5ddc71..6a393010e0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -167,12 +167,6 @@ services:
volumes:
- data-minio:/data
- toxiproxy:
- image: shopify/toxiproxy
- ports:
- - "22220:22220"
- - "8474:8474"
-
volumes:
mysql-persistent-volume:
data-minio:
diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md
index 76e25658d6..89fa9072ee 100644
--- a/docs/Configuration/fleet-server-configuration.md
+++ b/docs/Configuration/fleet-server-configuration.md
@@ -4,65 +4,16 @@ Fleet server configuration options update the internals of the Fleet server (MyS
Only self-managed users and customers can modify this configuration. If you're a managed-cloud customer, please reach out to Fleet about modifying the configuration.
+## Configuration options
+
You can specify configuration options in the following formats:
1. YAML file
2. Environment variables
3. Command-line flags
-All duration-based settings accept valid time units of `s`, `m`, `h`.
-
-## YAML file
-
-```sh
-echo '
-
-mysql:
- address: 127.0.0.1:3306
- database: fleet
- username: root
- password: toor
-redis:
- address: 127.0.0.1:6379
-server:
- cert: /tmp/server.cert
- key: /tmp/server.key
-logging:
- json: true
-' > /tmp/fleet.yml
-fleet serve --config /tmp/fleet.yml
-```
-
-## Environment variables
-
-```sh
-FLEET_MYSQL_ADDRESS=127.0.0.1:3306 \
-FLEET_MYSQL_DATABASE=fleet \
-FLEET_MYSQL_USERNAME=root \
-FLEET_MYSQL_PASSWORD=toor \
-FLEET_REDIS_ADDRESS=127.0.0.1:6379 \
-FLEET_SERVER_CERT=/tmp/server.cert \
-FLEET_SERVER_KEY=/tmp/server.key \
-FLEET_LOGGING_JSON=true \
-/usr/bin/fleet serve
-```
-
-## Command-line flags
-
-```sh
-/usr/bin/fleet serve \
---mysql_address=127.0.0.1:3306 \
---mysql_database=fleet \
---mysql_username=root \
---mysql_password=toor \
---redis_address=127.0.0.1:6379 \
---server_cert=/tmp/server.cert \
---server_key=/tmp/server.key \
---logging_json
-```
-
-
-## Configuration options
+- All duration-based settings accept valid time units of `s`, `m`, `h`.
+- Command-line flags can also be piped in via stdin.
#### MySQL
@@ -2203,7 +2154,7 @@ for the email address specified in the Source parameter of SendRawEmail.
##### s3_software_installers_bucket
-Name of the S3 bucket for storing software.
+Name of the S3 bucket for storing software and bootstrap package.
- Default value: none
- Environment variable: `FLEET_S3_SOFTWARE_INSTALLERS_BUCKET`
@@ -2891,7 +2842,7 @@ packaging:
region: us-east-1
```
-## Mobile device management (MDM)
+#### Mobile device management (MDM)
> The [`server_private_key` configuration option](#server_private_key) is required for macOS MDM features.
@@ -2962,11 +2913,6 @@ The content of the Windows WSTEP identity key. An RSA private key, PEM-encoded.
-----END RSA PRIVATE KEY-----
```
-
-## Managing osquery configurations
-
-We recommend that you use an infrastructure configuration management tool to manage these osquery configurations consistently across your environment. If you're unsure about what configuration management tools your organization uses, contact your company's system administrators. If you are evaluating new solutions for this problem, the founders of Fleet have successfully managed configurations in large production environments using [Chef](https://www.chef.io/chef/) and [Puppet](https://puppet.com/).
-
Running with systemd
This content was moved to [Systemd](http://fleetdm.com/docs/deploy/system-d) on Sept 6th, 2023.
diff --git a/docs/Using Fleet/GitOps.md b/docs/Configuration/yaml-files.md
similarity index 74%
rename from docs/Using Fleet/GitOps.md
rename to docs/Configuration/yaml-files.md
index ce2680bda6..7599fd259f 100644
--- a/docs/Using Fleet/GitOps.md
+++ b/docs/Configuration/yaml-files.md
@@ -1,4 +1,4 @@
-# GitOps
+# YAML files
Use Fleet's best practice GitOps workflow to manage your computers as code.
@@ -6,14 +6,16 @@ To learn how to set up a GitOps workflow see the [Fleet GitOps repo](https://git
## File structure
-- `default.yml`- File where you define the queries, policies, controls, and agent options for all hosts. If you're using Fleet Premium, this file updates queries and policies that run on all hosts ("All teams"). Controls and agent options are defined for hosts on "No team."
-- `teams/` - Folder where you define your teams in Fleet. These `teams/team-name.yml` files define the controls, queries, policies, and agent options for hosts assigned to the specified team. Teams are available in Fleet Premium.
+- `default.yml` - File where you define the queries, policies and agent options for all hosts. If you're using Fleet Premium, this file updates queries and policies that run on all hosts ("All teams").
+- `teams/no-team.yml` - File where you define the policies, controls, and software for hosts on "No team". Available in Fleet Premium.
+- `teams/` - Folder where you define your teams in Fleet. These `teams/team-name.yml` files define the controls, queries, policies, software, and agent options for hosts assigned to the specified team. Available in Fleet Premium.
- `lib/` - Folder where you define policies, queries, configuration profiles, scripts, and agent options. These files can be referenced in top level keys in the `default.yml` file and the files in the `teams/` folder.
- `.github/workflows/workflow.yml` - The GitHub workflow file where you can add [environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow).
-The following files are responsible for running the GitHub action. Most users don't need to edit these files.
+The following files are responsible for running the GitHub action or GitLab CI/CD. Most users don't need to edit these files.
- `gitops.sh` - The bash script that applies the latest configuration to Fleet. This script is used in the GitHub action file.
- `.github/gitops-action/action.yml` - The GitHub action that runs `gitops.sh`. This action is used in the GitHub workflow file. It can also be used in other workflows.
+- `.gitlab-ci.yml` - The GitLab CI/CD file that applies the latest configuration to Fleet.
## Configuration options
@@ -24,8 +26,7 @@ name: # Only teams/team-name.yml. To edit a team's name, change `name` but don't
policies:
queries:
agent_options:
-controls:
-software:
+controls: # Can be defined in teams/no-team.yml too.
org_settings: # Only default.yml
team_settings: # Only teams/team-name.yml
```
@@ -40,6 +41,8 @@ team_settings: # Only teams/team-name.yml
### policies
Polcies can be specified inline in your `default.yml` file or `teams/team-name.yml` files. They can also be specified in separate files in your `lib/` folder.
+Policies defined in `default.yml` run on **all** hosts.
+Policies defined in `teams/no-team.yml` run on hosts that belong to "No team".
#### Options
@@ -81,9 +84,16 @@ policies:
platform: darwin
critical: false
calendar_event_enabled: false
+- name: Firefox on Linux installed and up to date
+ platform: linux
+ description: "This policy checks that Firefox is installed and up to date."
+ resolution: "Install Firefox version 129.0.2 or higher."
+ query: "SELECT 1 FROM deb_packages WHERE name = 'firefox' AND version_compare(version, '129.0.2') >= 0;"
+ install_software:
+ package_path: "../lib/linux-firefox.deb.package.yml"
```
-`default.yml` or `teams/team-name.yml`
+`default.yml`, `teams/team-name.yml`, or `teams/no-team.yml`
```yaml
policies:
@@ -210,6 +220,8 @@ queries:
The `controls` section allows you to configure scripts and device management (MDM) features in Fleet.
+Controls for hosts that are in "No team" can be defined in `default.yml` or in `teams/no-team.yml` (but not in both files).
+
- `scripts` is a list of paths to macOS, Windows, or Linux scripts.
- `windows_enabled_and_configured` specifies whether or not to turn on Windows MDM features (default: `false`). Can only be configured for all teams (`default.yml`).
- `enable_disk_encryption` specifies whether or not to enforce disk encryption on macOS and Windows hosts (default: `false`).
@@ -262,6 +274,16 @@ controls:
- `deadline_days` (default: null)
- `grace_period_days` (default: null)
+#### ios_updates
+
+- `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`).
+- `minimum_version` specifies the minimum required iOS version (default: `""`).
+
+#### ipados_updates
+
+- `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`).
+- `minimum_version` specifies the minimum required iPadOS version (default: `""`).
+
#### macos_settings and windows_settings
- `macos_settings.custom_settings` is a list of paths to macOS configuration profiles (.mobileconfig) or declaration profiles (.json).
@@ -273,14 +295,16 @@ Use `labels_include_all` to only apply (scope) profiles to hosts that have all t
#### macos_setup
-The `macos_setup` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that automatically enrolled to your old MDM solution.
+The `macos_setup` section lets you control the out-of-the-box macOS [setup experience](https://fleetdm.com/guides/macos-setup-experience) for hosts that use Automated Device Enrollment (ADE).
- `bootstrap_package` is the URL to a bootstap package. Fleet will download the bootstrap package (default: `""`).
- `enable_end_user_authentication` specifies whether or not to require end user authentication when the user first sets up their macOS host.
-- `macos_setup_assistant` is a path to a custom automatic enrollment (DEP) profile (.json).
+- `macos_setup_assistant` is a path to a custom automatic enrollment (ADE) profile (.json).
#### macos_migration
+The `macos_migration` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that enrolled to your old MDM solution.
+
- `enable` specifies whether or not to enable end user migration workflow (default: `false`)
- `mode` specifies whether the end user initiates migration (`voluntary`) or they're nudged every 15-20 minutes to migrate (`forced`) (default: `""`).
- `webhook_url` is the URL that Fleet sends a webhook to when the end user selects **Start**. Receive this webhook using your automation tool (ex. Tines) to unenroll your end users from your old MDM solution.
@@ -289,12 +313,18 @@ Can only be configure for all teams (`default.yml`).
### software
+> **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.
+
The `software` section allows you to configure packages and Apple App Store apps that you want to install on your hosts.
+Software for hosts that belong to "No team" have to be defined in `teams/no-team.yml`.
+Software can also be specified in separate files in your `lib/` folder.
- `packages` is a list of software packages (.pkg, .msi, .exe, or .deb) and software specific options.
- `app_store_apps` is a list of Apple App Store apps.
-##### Example
+#### Example
+
+##### Inline
```yaml
software:
@@ -309,10 +339,10 @@ software:
self_service: true
- url: https://github.com/organinzation/repository/package-2.msi
app_store_apps:
- - app_store_id: 1091189122
+ - app_store_id: '1091189122'
```
-#### packages
+##### packages
- `url` specifies the URL at which the software is located. Fleet will download the software and upload it to S3 (default: `""`).
- `install_script.path` specifies the command Fleet will run on hosts to install software. The [default script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) is dependent on the software type (i.e. .pkg).
@@ -320,9 +350,40 @@ software:
- `post_install_script.path` is the script Fleet will run on hosts after intalling software (default: `""`).
- `self_service` specifies whether or not end users can install from **Fleet Desktop > Self-service**.
-#### app_store_apps
+##### app_store_apps
-- `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447` (default: `0`).
+- `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447`.
+
+> Make sure to include only the ID itself, and not the `id` prefix shown in the URL. The ID must be wrapped in quotes as shown in the example so that it is processed as a string.
+
+##### Separate file
+
+`lib/software-name.package.yml`:
+
+```yaml
+url: https://dl.tailscale.com/stable/tailscale-setup-1.72.0.exe
+install_script:
+ path: ../lib/software/tailscale-install-script.ps1
+self_service: true
+```
+
+`lib/software/tailscale-install-script.ps1`
+
+```yaml
+$exeFilePath = "${env:INSTALLER_PATH}"
+$installProcess = Start-Process $exeFilePath `
+ -ArgumentList "/quiet /norestart" `
+ -PassThru -Verb RunAs -Wait
+```
+
+`default.yml`, `teams/team-name.yml`, or `teams/no-team.yml`
+
+```yaml
+software:
+ packages:
+ - path: ../lib/software-name.package.yml
+# path is relative to default.yml or teams/team-name.yml
+```
### org_settings and team_settings
@@ -569,21 +630,74 @@ Can only be configured for all teams (`org_settings`).
#### mdm
-The `mdm` section lets you enable MDM features in Fleet.
+##### apple_business_manager
-- `apple_bm_default_team` - is name of the team that macOS hosts in Apple Business Manager automatically enroll to when they're first set up. If empty, hosts will enroll to "No team" (default: `""`).
+- `organization_name` is the organization name associated with the Apple Business Manager account.
+- `macos_team` is the team where macOS hosts are automatically added when they appear in Apple Business Manager.
+- `ios_team` is the the team where iOS hosts are automatically added when they appear in Apple Business Manager.
+- `ipados_team` is the team where iPadOS hosts are automatically added when they appear in Apple Business Manager.
##### Example
```yaml
org_settings:
mdm:
- apple_bm_default_team: "Workstations" # Available in Fleet Premium
+ apple_business_manager: # Available in Fleet Premium
+ - organization_name: Fleet Device Management Inc.
+ macos_team: "💻 Workstations"
+ ios_team: "📱🏢 Company-owned iPhones"
+ ipados_team: "🔳🏢 Company-owned iPads"
+```
+
+> Apple Business Manager settings can only be configured for all teams (`org_settings`).
+
+##### volume_purchasing_program
+
+- `location` is the name of the location in the Apple Business Manager account.
+- `teams` is a list of team names. If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, App Store apps are available to install on hosts in all teams.
+
+##### Example
+
+```yaml
+org_settings:
+ mdm:
+ volume_purchasing_program: # Available in Fleet Premium
+ - location: Fleet Device Management Inc.
+ teams:
+ - "💻 Workstations"
+ - "💻🐣 Workstations (canary)"
+ - "📱🏢 Company-owned iPhones"
+ - "🔳🏢 Company-owned iPads"
```
Can only be configured for all teams (`org_settings`).
-
+##### end_user_authentication
+
+The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula).
+
+Once the IdP settings are configured, you can use the [`controls.macos_setup.enable_end_user_authentication`](#macos_setup) key to control the end user experience during ADE.
+
+- `idp_name` is the human-friendly name for the identity provider that will provide single sign-on authentication (default: `""`).
+- `entity_id` is the entity ID: a Uniform Resource Identifier (URI) that you use to identify Fleet when configuring the identity provider. It must exactly match the Entity ID field used in identity provider configuration (default: `""`).
+- `metadata` is the metadata (in XML format) provided by the identity provider. (default: `""`)
+- `metadata_url` is the URL that references the identity provider metadata. Only one of `metadata` or `metadata_url` is required (default: `""`).
+
+Can only be configured for all teams (`org_settings`).
+
+##### end_user_authentication
+
+The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula).
+
+Once the IdP settings are configured, you can use the [`controls.macos_setup.enable_end_user_authentication`](#macos_setup) key to control the end user experience during ADE.
+
+- `idp_name` is the human-friendly name for the identity provider that will provide single sign-on authentication (default: `""`).
+- `entity_id` is the entity ID: a Uniform Resource Identifier (URI) that you use to identify Fleet when configuring the identity provider. It must exactly match the Entity ID field used in identity provider configuration (default: `""`).
+- `metadata` is the metadata (in XML format) provided by the identity provider. (default: `""`)
+- `metadata_url` is the URL that references the identity provider metadata. Only one of `metadata` or `metadata_url` is required (default: `""`).
+
+Can only be configured for all teams (`org_settings`).
+
+
-
diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md
index 170ff1c8ea..0b830dbc07 100644
--- a/docs/Contributing/API-for-contributors.md
+++ b/docs/Contributing/API-for-contributors.md
@@ -11,9 +11,9 @@
- [Scripts](#scripts)
- [Software](#software)
-This document includes the internal Fleet API routes that are helpful when developing or contributing to Fleet.
+> These endpoints are used by the Fleet UI, Fleet Desktop, and `fleetctl` clients and frequently change to reflect current functionality.
-These endpoints are used by the Fleet UI, Fleet Desktop, and `fleetctl` clients and will frequently change to reflect current functionality.
+This document includes the internal Fleet API routes that are helpful when developing or contributing to Fleet.
If you are interested in gathering information from Fleet in a production environment, please see the [public Fleet REST API documentation](https://fleetdm.com/docs/using-fleet/rest-api).
@@ -531,15 +531,26 @@ The MDM endpoints exist to support the related command-line interface sub-comman
- [Generate Apple Business Manager public key (ADE)](#generate-apple-business-manager-public-key-ade)
- [Request Certificate Signing Request (CSR)](#request-certificate-signing-request-csr)
- [Upload APNS certificate](#upload-apns-certificate)
-- [Upload ABM Token](#upload-abm-token)
+- [Add ABM token](#add-abm-token)
- [Turn off Apple MDM](#turn-off-apple-mdm)
-- [Disable automatic enrollment (ADE)](#disable-automatic-enrollment-ade)
+- [Update ABM token's teams](#update-abm-tokens-teams)
+- [Renew ABM token](#renew-abm-token)
+- [Delete ABM token](#delete-abm-token)
+- [Add VPP token](#add-VPP-token)
+- [Update VPP token's teams](#update-vpp-tokens-teams)
+- [Renew VPP token](#renew-vpp-token)
+- [Delete VPP token](#delete-vpp-token)
- [Batch-apply MDM custom settings](#batch-apply-mdm-custom-settings)
- [Initiate SSO during DEP enrollment](#initiate-sso-during-dep-enrollment)
- [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
+- [Over the air enrollment](#over-the-air-enrollment)
- [Preassign profiles to devices](#preassign-profiles-to-devices)
- [Match preassigned profiles](#match-preassigned-profiles)
- [Get FileVault statistics](#get-filevault-statistics)
+- [Upload VPP content token](#upload-vpp-content-token)
+- [Disable VPP](#disable-vpp)
+- [Get an over the air (OTA) enrollment profile](#get-an-over-the-air-ota-enrollment-profile)
+
### Generate Apple Business Manager public key (ADE)
@@ -617,9 +628,9 @@ Content-Type: application/octet-stream
`Status: 200`
-### Upload ABM Token
+### Add ABM token
-`POST /api/v1/fleet/mdm/apple/abm_token`
+`POST /api/v1/fleet/abm_tokens`
#### Parameters
@@ -629,7 +640,7 @@ Content-Type: application/octet-stream
#### Example
-`POST /api/v1/fleet/mdm/apple/abm_token`
+`POST /api/v1/fleet/abm_tokens`
##### Request header
@@ -650,11 +661,23 @@ Content-Type: application/octet-stream
--------------------------f02md47480und42y
```
-
##### Default response
`Status: 200`
+```json
+"abm_token": {
+ "id": 1,
+ "apple_id": "apple@example.com",
+ "org_name": "Fleet Device Management Inc.",
+ "mdm_server_url": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2024-10-20T00:00:00Z",
+ "terms_expired": false,
+ "macos_team": null,
+ "ios_team": null,
+ "ipados_team": null
+}
+```
### Turn off Apple MDM
@@ -668,19 +691,265 @@ Content-Type: application/octet-stream
`Status: 204`
+### Update ABM token's teams
-### Disable automatic enrollment (ADE)
+`PATCH /api/v1/fleet/abm_tokens/:id/teams`
-`DELETE /api/v1/fleet/mdm/apple/abm_token`
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The ABM token's ID |
+| macos_team_id | integer | body | macOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
+| ios_team_id | integer | body | iOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
+| ipados_team_id | integer | body | iPadOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
#### Example
-`DELETE /api/v1/fleet/mdm/apple/abm_token`
+`PATCH /api/v1/fleet/abm_tokens/1/teams`
+
+##### Request body
+
+```json
+{
+ "macos_team_id": 1,
+ "ios_team_id": 2,
+ "ipados_team_id": 3
+}
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+"abm_token": {
+ "id": 1,
+ "apple_id": "apple@example.com",
+ "org_name": "Fleet Device Management Inc.",
+ "mdm_server_url": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2024-11-29T00:00:00Z",
+ "terms_expired": false,
+ "macos_team": 1,
+ "ios_team": 2,
+ "ipados_team": 3
+}
+```
+
+### Renew ABM token
+
+`PATCH /api/v1/fleet/abm_tokens/:id/renew`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The ABM token's ID |
+
+#### Example
+
+`PATCH /api/v1/fleet/abm_tokens/1/renew`
+
+##### Request header
+
+```http
+Content-Length: 850
+Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
+```
+
+##### Request body
+
+```http
+--------------------------f02md47480und42y
+Content-Disposition: form-data; name="token"; filename="server_token_abm.p7m"
+Content-Type: application/octet-stream
+
+
+
+--------------------------f02md47480und42y
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+"abm_token": {
+ "id": 1,
+ "apple_id": "apple@example.com",
+ "org_name": "Fleet Device Management Inc.",
+ "mdm_server_url": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2025-10-20T00:00:00Z",
+ "terms_expired": false,
+ "macos_team": null,
+ "ios_team": null,
+ "ipados_team": null
+}
+```
+
+### Delete ABM token
+
+`DELETE /api/v1/fleet/abm_tokens/:id`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The ABM token's ID |
+
+#### Example
+
+`DELETE /api/v1/fleet/abm_tokens/1`
##### Default response
`Status: 204`
+### Add VPP token
+
+`POST /api/v1/fleet/vpp_tokens`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| token | file | form | *Required* The file containing the content token (.vpptoken) from Apple Business Manager |
+
+#### Example
+
+`POST /api/v1/fleet/vpp_tokens`
+
+##### Request header
+
+```http
+Content-Length: 850
+Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
+```
+
+##### Request body
+
+```http
+--------------------------f02md47480und42y
+Content-Disposition: form-data; name="token"; filename="sToken_for_Acme.vpptoken"
+Content-Type: application/octet-stream
+
+--------------------------f02md47480und42y
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+"vpp_token": {
+ "id": 1,
+ "org_name": "Fleet Device Management Inc.",
+ "location": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2024-10-20T00:00:00Z",
+ "terms_expired": false,
+ "teams": null
+}
+```
+
+### Update VPP token's teams
+
+`PATCH /api/v1/fleet/vpp_tokens/:id/teams`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The ABM token's ID |
+| team_ids | list | body | If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, defaults to all teams. |
+
+#### Example
+
+`PATCH /api/v1/fleet/vpp_tokens/1/teams`
+
+##### Request body
+
+```json
+{
+ "team_ids": [1, 2, 3]
+}
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+"vpp_token": {
+ "id": 1,
+ "org_name": "Fleet Device Management Inc.",
+ "location": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2024-10-20T00:00:00Z",
+ "terms_expired": false,
+ "teams": [1, 2, 3]
+}
+```
+
+### Renew VPP token
+
+`PATCH /api/v1/fleet/vpp_tokens/:id/renew`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The VPP token's ID |
+
+##### Request header
+
+```http
+Content-Length: 850
+Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
+```
+
+##### Request body
+
+```http
+--------------------------f02md47480und42y
+Content-Disposition: form-data; name="token"; filename="sToken_for_Acme.vpptoken"
+Content-Type: application/octet-stream
+
+
+
+--------------------------f02md47480und42y
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+"vpp_token": {
+ "id": 1,
+ "org_name": "Fleet Device Management Inc.",
+ "location": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2025-10-20T00:00:00Z",
+ "terms_expired": false,
+ "teams": [1, 2, 3]
+}
+```
+
+### Delete VPP token
+
+`DELETE /api/v1/fleet/vpp_token/:id`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| id | integer | path | *Required* The VPP token's ID |
+
+#### Example
+
+`DELETE /api/v1/fleet/vpp_tokens/1`
+
+##### Default response
+
+`Status: 204`
### Batch-apply MDM custom settings
@@ -762,6 +1031,34 @@ If the credentials are valid, the server redirects the client to the Fleet UI. T
- `profile_token` is a token that can be used to download an enrollment profile (.mobileconfig).
- `eula_token` (optional) if an EULA was uploaded, this contains a token that can be used to view the EULA document.
+### Over the air enrollment
+
+This endpoint handles over the air (OTA) MDM enrollments
+
+`POST /api/v1/fleet/ota_enrollment`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ------------------- | ------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| enroll_secret | string | url | **Required** Assigns the host to a team with a matching enroll secret |
+| XML device response | XML | body | **Required**. The XML response from the device. Fields are documented [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/ConfigurationProfileExamples/ConfigurationProfileExamples.html#//apple_ref/doc/uid/TP40009505-CH4-SW7) |
+
+> Note: enroll secrets can contain special characters. Ensure any special characters are [properly escaped](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding).
+
+#### Example
+
+`POST /api/v1/fleet/ota_enrollment?enroll_secret=0Z6IuKpKU4y7xl%2BZcrp2gPcMi1kKNs3p`
+
+##### Default response
+
+`Status: 200`
+
+Per [the spec](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009505-CH1-SW1), the response is different depending on the signature of the XML device response:
+
+- If the body is signed with a certificate that can be validated by our root SCEP certificate, it returns an enrollment profile.
+- Otherwise, it returns a SCEP payload.
+
### Preassign profiles to devices
_Available in Fleet Premium_
@@ -868,6 +1165,55 @@ This endpoint uses the profiles stored by the [Preassign profiles to devices](#p
`Status: 204`
+### Upload VPP content token
+
+`POST /api/v1/fleet/mdm/apple/vpp_token`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| token | file | form | *Required* The file containing the content token (.vpptoken) from Apple Business Manager |
+
+#### Example
+
+`POST /api/v1/fleet/mdm/apple/vpp_token`
+
+##### Request header
+
+```http
+Content-Length: 850
+Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
+```
+
+##### Request body
+
+```http
+--------------------------f02md47480und42y
+Content-Disposition: form-data; name="token"; filename="sToken_for_Acme.vpptoken"
+Content-Type: application/octet-stream
+
+--------------------------f02md47480und42y
+```
+
+##### Default response
+
+`Status: 200`
+
+
+### Disable VPP
+
+`DELETE /api/v1/fleet/mdm/apple/vpp_token`
+
+#### Example
+
+`DELETE /api/v1/fleet/mdm/apple/vpp_token`
+
+##### Default response
+
+`Status: 204`
+
+
## Get or apply configuration files
These API routes are used by the `fleetctl` CLI tool. Users can manage Fleet with `fleetctl` and [configuration files in YAML syntax](https://fleetdm.com/docs/using-fleet/configuration-files/).
@@ -1137,12 +1483,14 @@ NOTE: when updating a policy, team and platform will be ignored.
"name": "new policy",
"description": "This will be a new policy because a policy with the name 'new policy' doesn't exist in Fleet.",
"query": "SELECT * FROM osquery_info",
+ "team": "No team",
"resolution": "some resolution steps here",
"critical": false
},
{
"name": "Is FileVault enabled on macOS devices?",
"query": "SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT “” AND filevault_status = ‘on’ LIMIT 1;",
+ "team": "Workstations",
"description": "Checks to make sure that the FileVault feature is enabled on macOS devices.",
"resolution": "Choose Apple menu > System Preferences, then click Security & Privacy. Click the FileVault tab. Click the Lock icon, then enter an administrator name and password. Click Turn On FileVault.",
"platform": "darwin",
@@ -1381,7 +1729,9 @@ If the `name` is not already associated with an existing team, this API route cr
| mdm.windows_settings | object | body | The Windows-specific MDM settings. |
| mdm.windows_settings.custom_settings | list | body | The list of objects consists of a `path` to XML files and `labels_include_all` or `labels_exclude_any` list of label names. |
| scripts | list | body | A list of script files to add to this team so they can be executed at a later time. |
-| software | list | body | An array of software objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `self_service` boolean. |
+| software | object | body | The team's software that will be available for install. |
+| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `self_service` boolean. |
+| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app formatted as a string (in quotes) rather than a number. |
| mdm.macos_settings.enable_disk_encryption | bool | body | Whether disk encryption should be enabled for hosts that belong to this team. |
| force | bool | query | Force apply the spec even if there are (ignorable) validation errors. Those are unknown keys and agent options-related validations. |
| dry_run | bool | query | Validate the provided JSON for unknown keys and invalid value types and return any validation errors, but do not apply the changes. |
@@ -1462,14 +1812,21 @@ If the `name` is not already associated with an existing team, this API route cr
}
},
"scripts": ["path/to/script.sh"],
- "software": [
- {
- "url": "https://cdn.zoom.us/prod/5.16.10.26186/x64/ZoomInstallerFull.msi",
- "pre_install_query": "SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';",
- "post_install_script": "sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX",
- "self_service": true
- }
- ]
+ "software": {
+ "packages": [
+ {
+ "url": "https://cdn.zoom.us/prod/5.16.10.26186/x64/ZoomInstallerFull.msi",
+ "pre_install_query": "SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';",
+ "post_install_script": "sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX",
+ "self_service": true,
+ }
+ ],
+ "app_store_apps": [
+ {
+ "app_store_id": "12464567",
+ }
+ ]
+ }
}
]
}
@@ -2481,6 +2838,7 @@ Gets all information required by Fleet Desktop, this includes things like the nu
```json
{
"failing_policies_count": 3,
+ "self_service": true,
"notifications": {
"needs_mdm_migration": true,
"renew_enrollment_profile": false,
@@ -2540,35 +2898,58 @@ Lists the software installed on the current device.
{
"id": 121,
"name": "Google Chrome.app",
+ "software_package": {
+ "name": "GoogleChrome.pkg"
+ "version": "125.12.2"
+ "self_service": true,
+ "last_install": {
+ "install_uuid": "8bbb8ac2-b254-4387-8cba-4d8a0407368b",
+ "installed_at": "2024-05-15T15:23:57Z"
+ },
+ },
+ "app_store_app": null,
"source": "apps",
"status": "failed",
- "last_install": {
- "install_uuid": "8bbb8ac2-b254-4387-8cba-4d8a0407368b",
- "installed_at": "2024-05-15T15:23:57Z"
- },
"installed_versions": [
- {
+ {
"version": "121.0",
"last_opened_at": "2024-04-01T23:03:07Z",
"vulnerabilities": ["CVE-2023-1234","CVE-2023-4321","CVE-2023-7654"],
"installed_paths": ["/Applications/Google Chrome.app"]
}
- ]
+ ],
+ "software_package": {
+ "name": "google-chrome-124-0-6367-207.pkg",
+ "version": "121.0",
+ "self_service": true,
+ "icon_url": null,
+ "last_install": null
+ },
+ "app_store_app": null
},
{
"id": 143,
"name": "Firefox.app",
+ "software_package": null,
+ "app_store_app": null,
"source": "apps",
"status": null,
- "last_install": null,
"installed_versions": [
- {
+ {
"version": "125.6",
"last_opened_at": "2024-04-01T23:03:07Z",
"vulnerabilities": ["CVE-2023-1234","CVE-2023-4321","CVE-2023-7654"],
"installed_paths": ["/Applications/Firefox.app"]
}
- ]
+ ],
+ "software_package": null,
+ "app_store_app": {
+ "app_store_id": "12345",
+ "version": "125.6",
+ "self_service": false,
+ "icon_url": "https://example.com/logo-light.jpg",
+ "last_install": null
+ },
}
],
"meta": {
@@ -2917,7 +3298,7 @@ If the Fleet instance is provided required parameters to complete setup.
## Scripts
-### Batch-apply scripts
+### Batch-apply scripts
_Available in Fleet Premium_
@@ -2946,7 +3327,7 @@ If both `team_id` and `team_name` parameters are included, this endpoint will re
## Software
-### Batch-apply software
+### Batch-apply software
_Available in Fleet Premium._
@@ -2956,10 +3337,12 @@ _Available in Fleet Premium._
| Name | Type | In | Description |
| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. |
-| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. |
+| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. Ommitting these parameters will add software to 'No Team'. |
+| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. Ommitting these parameters will add software to 'No Team'. |
| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
-| software | list | body | An array of software objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, and `post_install_script` - script that runs after software install. |
+| software | object | body | The team's software that will be available for install. |
+| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `uninstall_script` - command that Fleet runs to uninstall software. |
+| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app. |
If both `team_id` and `team_name` parameters are included, this endpoint will respond with an error. If no `team_name` or `team_id` is provided, the scripts will be applied for **all hosts**.
@@ -2969,6 +3352,62 @@ If both `team_id` and `team_name` parameters are included, this endpoint will re
##### Default response
+`Status: 200`
+
+```json
+{
+ "packages": [
+ {
+ "team_id": 3,
+ "software_title_id": 6690,
+ "url": "https://dl.tailscale.com/stable/tailscale-setup-1.72.0.exe"
+ },
+ {
+ "team_id": 3,
+ "software_title_id": 10412,
+ "url": "https://ftp.mozilla.org/pub/firefox/releases/129.0.2/win64/en-US/Firefox%20Setup%20129.0.2.msi"
+ }
+ ]
+}
+```
+
+### Batch-apply VPP apps
+
+_Available in Fleet Premium._
+
+`POST /api/latest/fleet/software/app_store_apps/batch`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| team_name | string | query | The name of the team to add the software package to. Ommitting this parameter will add software to 'No Team'. |
+| dry_run | bool | query | If `true`, will validate the provided VPP apps and return any validation errors, but will not apply the changes. |
+| app_store_apps | list | body | An array of objects. Each object contains `app_store_id` and `self_service`. |
+| app_store_apps.app_store_id | string | body | ID of the App Store app. |
+| app_store_apps.self_service | boolean | body | Whether the VPP app is "Self-service" or not. |
+
+#### Example
+
+`POST /api/latest/fleet/software/app_store_apps/batch`
+```json
+{
+ "team_name": "Foobar",
+ "app_store_apps": {
+ {
+ "app_store_id": "597799333",
+ "self_service": false,
+ },
+ {
+ "app_store_id": "497799835",
+ "self_service": true,
+ }
+ }
+}
+```
+
+##### Default response
+
`Status: 204`
### Run live script
@@ -3010,3 +3449,130 @@ Run a live script and get results back (5 minute timeout). Live scripts only run
"exit_code": 0
}
```
+
+### Get token to download package
+
+_Available in Fleet Premium._
+
+`POST /api/v1/fleet/software/titles/:software_title_id/package/token?alt=media`
+
+The returned token is a one-time use token that expires after 10 minutes.
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|-------|------------------------------------------------------------------|
+| software_title_id | integer | path | **Required**. The ID of the software title for software package. |
+| team_id | integer | query | **Required**. The team ID containing the software package. |
+| alt | integer | query | **Required**. Must be specified and set to "media". |
+
+#### Example
+
+`POST /api/v1/fleet/software/titles/123/package/token?alt=media&team_id=2`
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "token": "e905e33e-07fe-4f82-889c-4848ed7eecb7"
+}
+```
+
+### Download package using a token
+
+_Available in Fleet Premium._
+
+`GET /api/v1/fleet/software/titles/:software_title_id/package/token/:token?alt=media`
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|------|--------------------------------------------------------------------------|
+| software_title_id | integer | path | **Required**. The ID of the software title to download software package. |
+| token | string | path | **Required**. The token to download the software package. |
+
+#### Example
+
+`GET /api/v1/fleet/software/titles/123/package/token/e905e33e-07fe-4f82-889c-4848ed7eecb7`
+
+##### Default response
+
+`Status: 200`
+
+```http
+Status: 200
+Content-Type: application/octet-stream
+Content-Disposition: attachment
+Content-Length:
+Body:
+```
+
+### Get an over the air (OTA) enrollment profile
+
+`GET /api/v1/fleet/enrollment_profiles/ota`
+
+The returned value is a signed `.mobileconfig` OTA profile.
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|-------|----------------------------------------------------------------------------------|
+| enroll_secret | string | query | **Required**. The enroll secret of the team this host will be assigned to. |
+
+#### Example
+
+`GET /api/v1/fleet/enrollment_profiles/ota?enroll_secret=foobar`
+
+##### Default response
+
+`Status: 200`
+
+**Note** To confirm success, it is important for clients to match content length with the response
+header (this is done automatically by most clients, including the browser) rather than relying
+solely on the response status code returned by this endpoint.
+
+##### Example response headers
+
+```http
+ Content-Length: 542
+ Content-Type: application/x-apple-aspen-config; charset=urf-8
+ Content-Disposition: attachment;filename="fleet-mdm-enrollment-profile.mobileconfig"
+ X-Content-Type-Options: nosniff
+```
+
+###### Example response body
+
+```xml
+
+
+
+
+ PayloadContent
+
+ URL
+ https://foo.example.com/api/fleet/ota_enrollment?enroll_secret=foobar
+ DeviceAttributes
+
+ UDID
+ VERSION
+ PRODUCT
+ SERIAL
+
+
+ PayloadOrganization
+ Acme Inc.
+ PayloadDisplayName
+ Acme Inc. enrollment
+ PayloadVersion
+ 1
+ PayloadUUID
+ fdb376e5-b5bb-4d8c-829e-e90865f990c9
+ PayloadIdentifier
+ com.fleetdm.fleet.mdm.apple.ota
+ PayloadType
+ Profile Service
+
+
+```
diff --git a/docs/Using Fleet/Audit-logs.md b/docs/Contributing/Audit-logs.md
similarity index 94%
rename from docs/Using Fleet/Audit-logs.md
rename to docs/Contributing/Audit-logs.md
index 2ddd3bc055..25062d463f 100644
--- a/docs/Using Fleet/Audit-logs.md
+++ b/docs/Contributing/Audit-logs.md
@@ -1170,6 +1170,29 @@ This activity contains the following fields:
}
```
+## uninstalled_software
+
+Generated when a software is uninstalled on a host.
+
+This activity contains the following fields:
+- "host_id": ID of the host.
+- "host_display_name": Display name of the host.
+- "software_title": Name of the software.
+- "script_execution_id": ID of the software uninstall script.
+- "status": Status of the software uninstallation.
+
+#### Example
+
+```json
+{
+ "host_id": 1,
+ "host_display_name": "Anna's MacBook Pro",
+ "software_title": "Falcon.app",
+ "script_execution_id": "ece8d99d-4313-446a-9af2-e152cd1bad1e",
+ "status": "uninstalled"
+}
+```
+
## added_software
Generated when a software installer is uploaded to Fleet.
@@ -1193,6 +1216,29 @@ This activity contains the following fields:
}
```
+## edited_software
+
+Generated when a software installer is updated in Fleet.
+
+This activity contains the following fields:
+- "software_title": Name of the software.
+- "software_package": Filename of the installer as of this update (including if unchanged).
+- "team_name": Name of the team on which this software was updated. `null` if it was updated on no team.
+- "team_id": The ID of the team on which this software was updated. `null` if it was updated on no team.
+- "self_service": Whether the software is available for installation by the end user.
+
+#### Example
+
+```json
+{
+ "software_title": "Falcon.app",
+ "software_package": "FalconSensor-6.44.pkg",
+ "team_name": "Workstations",
+ "team_id": 123,
+ "self_service": true
+}
+```
+
## deleted_software
Generated when a software installer is deleted from Fleet.
@@ -1218,15 +1264,33 @@ This activity contains the following fields:
## enabled_vpp
-Generated when the VPP feature is enabled in Fleet.
+Generated when VPP features are enabled in Fleet.
+This activity contains the following fields:
+- "location": Location associated with the VPP content token for the enabled VPP features.
+#### Example
+
+```json
+{
+ "location": "Acme Inc."
+}
+```
## disabled_vpp
-Generated when the VPP feature is disabled in Fleet.
+Generated when VPP features are disabled in Fleet.
+This activity contains the following fields:
+- "location": Location associated with the VPP content token for the disabled VPP features.
+#### Example
+
+```json
+{
+ "location": "Acme Inc."
+}
+```
## added_app_store_app
@@ -1236,6 +1300,7 @@ This activity contains the following fields:
- "software_title": Name of the App Store app.
- "app_store_id": ID of the app on the Apple App Store.
- "platform": Platform of the app (`darwin`, `ios`, or `ipados`).
+- "self_service": App installation can be initiated by device owner.
- "team_name": Name of the team to which this App Store app was added, or `null` if it was added to no team.
- "team_id": ID of the team to which this App Store app was added, or `null`if it was added to no team.
@@ -1246,6 +1311,7 @@ This activity contains the following fields:
"software_title": "Logic Pro",
"app_store_id": "1234567",
"platform": "darwin",
+ "self_service": false,
"team_name": "Workstations",
"team_id": 1
}
@@ -1280,6 +1346,7 @@ Generated when an App Store app is installed on a device.
This activity contains the following fields:
- host_id: ID of the host on which the app was installed.
+- self_service: App installation was initiated by device owner.
- host_display_name: Display name of the host.
- software_title: Name of the App Store app.
- app_store_id: ID of the app on the Apple App Store.
@@ -1290,6 +1357,7 @@ This activity contains the following fields:
```json
{
"host_id": 42,
+ "self_service": true,
"host_display_name": "Anna's MacBook Pro",
"software_title": "Logic Pro",
"app_store_id": "1234567",
diff --git a/docs/Contributing/File-carving.md b/docs/Contributing/File-carving.md
index 4daad97fb0..b283f11f4c 100644
--- a/docs/Contributing/File-carving.md
+++ b/docs/Contributing/File-carving.md
@@ -77,7 +77,7 @@ The same is not true if S3 is used as the storage backend. In that scenario, it
### Alternative carving backends
-#### Minio
+#### MinIO
Configure the following:
- `FLEET_S3_ENDPOINT_URL=minio_host:port`
@@ -87,6 +87,11 @@ Configure the following:
- `FLEET_S3_FORCE_S3_PATH_STYLE=true`
- `FLEET_S3_REGION=minio` or any non-empty string otherwise Fleet will attempt to derive the region.
+If you're testing file carving locally with the docker-compose environment, the `--dev` flag on Fleet server will
+automatically point carves to the local MinIO container and write to the `carves-dev` bucket without needing to set
+additional configuration. Note that this bucket is *not* created automatically when bringing MinIO up; you'll need to
+log in via `http://localhost:9001` with credentials `minio` / `minio123!` to create the bucket.
+
### Troubleshooting
#### Check carve status in osquery
diff --git a/docs/Contributing/Simulate-slow-network.md b/docs/Contributing/Simulate-slow-network.md
index 3b342a0f3a..6fa02d6f83 100644
--- a/docs/Contributing/Simulate-slow-network.md
+++ b/docs/Contributing/Simulate-slow-network.md
@@ -7,6 +7,17 @@ The guide assumes you'll build and run the Fleet server locally with the `make f
(Has been tested on macOS only.)
+## 0. Edit docker-compose.yml
+
+Add the following service to the main `docker-compose.yml`:
+```yml
+ toxiproxy:
+ image: shopify/toxiproxy
+ ports:
+ - "22220:22220"
+ - "8474:8474"
+```
+
## 1. Start services
```sh
@@ -44,4 +55,4 @@ curl -s -XPOST -d '{"type" : "latency", "attributes" : {"latency" : 1000, "jitte
```
-
\ No newline at end of file
+
diff --git a/docs/Contributing/Testing-and-local-development.md b/docs/Contributing/Testing-and-local-development.md
index a3445e3ccd..45c5a93fe0 100644
--- a/docs/Contributing/Testing-and-local-development.md
+++ b/docs/Contributing/Testing-and-local-development.md
@@ -81,6 +81,14 @@ REDIS_TEST=1 MYSQL_TEST=1 make test
The integration tests in the `server/service` package can generate a lot of logs mixed with the test results output. To make it easier to identify a failing test in this package, you can set the `FLEET_INTEGRATION_TESTS_DISABLE_LOG=1` environment variable so that logging is disabled.
+The MDM integration tests are run with a random selection of software installer storage backends (local filesystem or S3/minio), and similar for the bootstrap packages storage (DB or S3/minio). You can force usage of the S3 backend by setting `FLEET_INTEGRATION_TESTS_SOFTWARE_INSTALLER_STORE=s3`. Note that `MINIO_STORAGE_TEST=1` must also be set for the S3 backend to be used.
+
+When the S3 backend is used, this line will be printed in the tests' output (as this could be relevant to understand and debug the test failure):
+
+```
+ integration_mdm_test.go:196: >>> using S3/minio software installer store
+```
+
Note that on a Linux system, the Redis tests will include running in cluster mode, so the docker Redis Cluster setup must be running. This implies starting the docker dependencies as follows:
```sh
@@ -481,7 +489,9 @@ FLEET_SERVER_SANDBOX_ENABLED=1 FLEET_PACKAGING_GLOBAL_ENROLL_SECRET=xyz ./build
Be sure to replace the `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` value above with the global enroll
secret from the `fleetctl package` command used to build the installers.
-MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`.
+MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`. When starting the
+Fleet server up with `--dev` the server will look for installers in the `software-installers-dev` MinIO bucket. You can
+create this bucket via the MinIO web UI (it is *not* created by default when setting up the docker-compose environment).
## Telemetry
diff --git a/docs/Using Fleet/Understanding-host-vitals.md b/docs/Contributing/Understanding-host-vitals.md
similarity index 100%
rename from docs/Using Fleet/Understanding-host-vitals.md
rename to docs/Contributing/Understanding-host-vitals.md
diff --git a/docs/Contributing/Vulnerability-processing.md b/docs/Contributing/Vulnerability-processing.md
index 6f7db26328..7a159bc000 100644
--- a/docs/Contributing/Vulnerability-processing.md
+++ b/docs/Contributing/Vulnerability-processing.md
@@ -35,7 +35,7 @@ be able write a query to retrieve all CVEs).
- Fleet combines two sources to get accurate and up-to-date CVE information:
- [National Vulnerability Database](https://nvd.nist.gov/developers/vulnerabilities)'s CVE feeds.
- [VulnCheck](https://vulncheck.com/)
-- To reduce the load and complexity of processing these datasets, Fleet uses two Github repositories (https://github.com/fleetdm/nvd and https://github.com/fleetdm/vulndb) that fetch, pre-process and expose the resulting dataset as Github releases.
+- To reduce the load and complexity of processing these datasets, Fleet uses two Github repositories (https://github.com/fleetdm/nvd and https://github.com/fleetdm/vulnerabilities) that fetch, pre-process and expose the resulting dataset as Github releases.
- The Fleet servers then download these Github releases and run vulnerability processing using the downloaded datasets and the software information fetched from hosts.
```mermaid
@@ -78,16 +78,17 @@ information and what vulnerabilities were patched with the release, we then exam
macOS apps and if an Office app is found we compare its version with the release notes metadata
and report back any vulnerabilities to which the software is susceptible.
-### Linux hosts
+### Linux hosts (via OVAL)
First, we determine what Linux distributions are part of your fleet (keep in mind that there will
-be a small delay between the time a new Linux hosts is added and the time the host is 'detected'). We then
-use
-that information to determine what OVAL definitions need to be downloaded and parsed - you can find
-a list of all the OVAL definitions we use
-[here](https://github.com/fleetdm/nvd/blob/master/oval_sources.json). OVAL definitions will be
+be a small delay between the time a new Linux host is added and the time the host is "detected"). We then
+use that information to determine what OVAL definitions need to be downloaded and parsed - you can find
+a list of all the OVAL definitions we use [here](https://github.com/fleetdm/nvd/blob/master/oval_sources.json). OVAL definitions will be
refreshed on a daily basis.
+*NOTE:* Amazon Linux 2 is included in the OVAL mapping but vulnerabilities are no longer pulled via that file
+as of 4.56.0 due to false positives (Amazon backports fixes and releases updates independent of RHEL).
+
Finally, we look at the software inventory of each host and execute the assertions contained in the
corresponding OVAL file - any match is reported using the same channels as with Windows/Mac OS vulnerabilities
@@ -104,6 +105,20 @@ available in the OVAL feed, it uses the NVD feed to look for vulnerabilities mat
CPE pattern:
`cpe:2.3:o:linux:linux_kernel:*;*:*:*:*:*:*:*:*`
+### Amazon Linux hosts (via goval-dictionary)
+
+Amazon Linux uses [ALAS](https://alas.aws.amazon.com/) rather than OVAL files to provide vulnerability info. The
+[goval-dictionary](https://github.com/vulsio/goval-dictionary) tool supports fetching those bulletins into database
+formats including sqlite. The database contains mappings of CVEs to package releases where that CVE was fixed. We run
+this tool for each Amazon Linux version as part of the release process in https://github.com/fleetdm/vulnerabilities.
+
+As part of the (default hourly) vulnerabilities check run, we download the sqlite files for relevant OS versions (keep in
+mind that there will be a small delay between the time a new Linux host is added and the time the host is "detected"),
+then for each host check if each package on that host is mentioned in the database. For each package found, we report
+a vulnerability when the installed version of the package is older than the version where the vulnerability was fixed.
+
+ALAS does *not* use CPE lookups.
+
## Performance
### Windows/Mac OS
@@ -127,9 +142,9 @@ For example, when running a development instance of Fleet on an Apple Macbook Pr
The CPU and memory usages are in burst once every hour (or the configured periodicity) on the
instance that does the processing. RAM spikes are expected to not exceed the 2GBs.
-### Linux
+### Linux (OVAL)
-As with Windows/Mac OS, vulnerability detection for Linux is performed in a single Fleet instance. The
+As with Windows/Mac OS, vulnerability detection for Linux is performed on a single Fleet server. The
files downloaded will vary depending on what distributions are on your fleet. The list of all the
OVAL files we use can be found [here](https://github.com/fleetdm/nvd/blob/master/oval_sources.json).
@@ -151,6 +166,19 @@ The performance will be a function of three variables:
That said, the performance characteristic should be linear (if scanning 200 hosts take
~20 seconds, then scanning 2000 hosts should take ~200 seconds).
+### Linux (goval-dictionary)
+
+As with OVAL, vulnerability detection for Linux via goval-dictionary is performed on a single Fleet server.
+Fleet will download one xz'd sqlite file per supported (see oval_platform.go -> IsGovalDictionarySupported) OS/version
+combination every time vulnerability scanning is run. OS version identifiers are constructed the same way
+as for OVAL, so Amazon Linux 2 becomes `amzn_02` and Amazon Linux 2023 becomes `amzn_2023`. sqlite databases are
+preprocessed as part of our vulnerabilities feed build, so are used as-is after download/extraction; the largest
+database is currently for Amazon Linux 2, at ~10 MiB. Filename format on disk is
+`fleet_goval_dictionary_platform.sqlite3`, corresponding to `platform.sqlite3` in the `vulnerabilities` repo releases.
+
+Like OVAL, execution time currently scales with the size of the database, the number of hosts to scan, and the number
+of packages installed. Scanning 2000 hosts will take about 10x longer than scanning 200 hosts.
+
## Detection pipeline
There are several steps that go into the vulnerability detection process. In this section we'll dive into what they are and how it works.
@@ -184,7 +212,7 @@ The whole pipeline exists to compensate for these differences, and it can be div
process2-->interval
```
- ### Windows/Mac OS
+### Windows/Mac OS
```mermaid
graph TD;
@@ -194,7 +222,7 @@ The whole pipeline exists to compensate for these differences, and it can be div
cveDownload-->cveMap[CVE detection]
```
- ### Linux
+### Linux (OVAL)
```mermaid
graph TD;
@@ -206,6 +234,14 @@ The whole pipeline exists to compensate for these differences, and it can be div
parse --> execute
```
+### Linux (goval-dictionary)
+
+ ```mermaid
+ graph TD;
+ process[Process Linux hosts] --> download(Download vulnerability databases from Fleet)
+ download --> execute(Match vulnerabilities to software older than fixed versions)
+ ```
+
### Ingesting software lists from hosts
The ingestion of software varies per platform. We run a `UNION` of several queries in each:
diff --git a/docs/Contributing/fleetctl-apply.md b/docs/Contributing/fleetctl-apply.md
index a502a1f516..dac4b25630 100644
--- a/docs/Contributing/fleetctl-apply.md
+++ b/docs/Contributing/fleetctl-apply.md
@@ -338,7 +338,8 @@ List of saved scripts that can be run on hosts that are part of the team.
- Default value: none
- Config file format:
- ```yaml
+
+```yaml
apiVersion: v1
kind: team
spec:
@@ -347,7 +348,7 @@ spec:
scripts:
- path/to/script1.sh
- path/to/script2.sh
- ```
+```
## Organization settings
diff --git a/docs/Deploy/Reference-Architectures.md b/docs/Deploy/Reference-Architectures.md
index 06a7d8dd5a..eecc179c36 100644
--- a/docs/Deploy/Reference-Architectures.md
+++ b/docs/Deploy/Reference-Architectures.md
@@ -2,10 +2,10 @@
## The Fleet binary
-The Fleet application contains two single static binaries which provide web based administration, REST API, and CLI interface to Fleet.
+The Fleet application contains two single static binaries which provide web based administration, a REST API, and a [CLI interface](https://fleetdm.com/guides/fleetctl).
The `fleet` binary contains:
-- The Fleet TLS web server (no external webserver is required but it supports a proxy if desired)
+- The [Fleet TLS web server](https://fleetdm.com/docs/configuration/fleet-server-configuration) (no external webserver is required but it supports a proxy if desired)
- The Fleet web interface
- The Fleet application management [REST API](https://fleetdm.com/docs/using-fleet/rest-api)
- The Fleet osquery API endpoints
@@ -24,15 +24,15 @@ Fleet currently has three infrastructure dependencies: MySQL, Redis, and a TLS c
### MySQL
Fleet uses MySQL extensively as its main database. Many cloud providers (such as [AWS](https://aws.amazon.com/rds/mysql/) and [GCP](https://cloud.google.com/sql/)) host reliable MySQL services which you may consider for this purpose. A well-supported MySQL [Docker image](https://hub.docker.com/_/mysql/) also exists if you would rather run MySQL in a container.
-For more information on how to configure the `fleet` binary to use the correct MySQL instance, see the [Configuration](https://fleetdm.com/docs/deploying/configuration) document.
+For more information on how to configure the `fleet` binary to use the correct MySQL instance, see the [MySQL configuration](https://fleetdm.com/docs/configuration/fleet-server-configuration#mysql) documentation.
-Fleet requires at least MySQL version 8.0, and is tested using the InnoDB storage engine.
+Fleet requires at least MySQL version 8.0.36, and is tested using the InnoDB storage engine [with versions 8.0.36 and 8.4.2](https://github.com/fleetdm/fleet/blob/main/.github/workflows/test-go.yaml#L47).
There are many "drop-in replacements" for MySQL available. If you'd like to experiment with some bleeding-edge technology and use Fleet with one of these alternative database servers, we think that's awesome! Please be aware they are not officially supported and that it is very important to set up a dev environment to thoroughly test new releases.
### Redis
-Fleet uses Redis to ingest and queue the results of distributed queries, cache data, etc. Many cloud providers (such as [AWS](https://aws.amazon.com/elasticache/) and [GCP](https://console.cloud.google.com/launcher/details/click-to-deploy-images/redis)) host reliable Redis services which you may consider for this purpose. A well supported Redis [Docker image](https://hub.docker.com/_/redis/) also exists if you would rather run Redis in a container. For more information on how to configure the `fleet` binary to use the correct Redis instance, see the [Configuration](https://fleetdm.com/docs/deploying/configuration) document.
+Fleet uses Redis to ingest and queue the results of distributed queries, cache data, etc. Many cloud providers (such as [AWS](https://aws.amazon.com/elasticache/) and [GCP](https://console.cloud.google.com/launcher/details/click-to-deploy-images/redis)) host reliable Redis services which you may consider for this purpose. A well supported Redis [Docker image](https://hub.docker.com/_/redis/) also exists if you would rather run Redis in a container. For more information on how to configure the `fleet` binary to use the correct Redis instance, see the [Redis configuration](https://fleetdm.com/docs/configuration/fleet-server-configuration#redis) documentation.
## Systemd
@@ -150,7 +150,7 @@ In some cases adding a read replica can increase database performance for specif
#### Traffic load balancing
Load balancing enables distributing request traffic over many instances of the backend application. Using AWS Application
-Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of it's allocated compute dedicated
+Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of its allocated compute dedicated
to its core functionality. More details about ALB can be found [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html).
_**Note if using [terraform reference architecture](https://github.com/fleetdm/fleet/tree/main/infrastructure/dogfood/terraform/aws#terraform) all configurations can dynamically scale based on load(cpu/memory) and all configurations
@@ -167,10 +167,11 @@ assume On-Demand pricing (savings are available through Reserved Instances). Cal
| --------------- | ------------- | --- |
| 1 Fargate task | 512 CPU Units | 4GB |
-| Dependencies | Version | Instance type | Nodes |
-| ------------ | ----------------------- | ------------- | ----- |
-| Redis | 6 | t4g.small | 3 |
-| MySQL | 8.0.mysql_aurora.3.04.2 | db.t4g.medium | 2 |
+| Dependencies | Version | Instance type | Nodes |
+| ------------ | ----------------------- | --------------- | ----- |
+| Redis | 6 | cache.t4g.small | 3 |
+| MySQL | 8.0.mysql_aurora.3.04.2 | db.t4g.medium | 2 |
+
###### [Up to 25000 hosts](https://calculator.aws/#/estimate?id=d735758715f059118dbce8dc42f3ff2410adc621)
@@ -178,10 +179,10 @@ assume On-Demand pricing (savings are available through Reserved Instances). Cal
| --------------- | -------------- | --- |
| 10 Fargate task | 1024 CPU Units | 4GB |
-| Dependencies | Version | Instance type | Nodes |
-| ------------ | ----------------------- | ------------- | ----- |
-| Redis | 6 | m6g.large | 3 |
-| MySQL | 8.0.mysql_aurora.3.04.2 | db.r6g.large | 2 |
+| Dependencies | Version | Instance type | Nodes |
+| ------------ | ----------------------- | --------------- | ----- |
+| Redis | 6 | cache.m6g.large | 3 |
+| MySQL | 8.0.mysql_aurora.3.04.2 | db.r6g.large | 2 |
###### [Up to 150000 hosts](https://calculator.aws/#/estimate?id=689fea65efff361ee070b15044a01224b8d26621)
@@ -190,10 +191,11 @@ assume On-Demand pricing (savings are available through Reserved Instances). Cal
| --------------- | -------------- | --- |
| 20 Fargate task | 1024 CPU Units | 4GB |
-| Dependencies | Version | Instance type | Nodes |
-| ------------ | ----------------------- | -------------- | ----- |
-| Redis | 6 | m6g.large | 3 |
-| MySQL | 8.0.mysql_aurora.3.04.2 | db.r6g.4xlarge | 2 |
+| Dependencies | Version | Instance type | Nodes |
+| ------------ | ----------------------- | --------------- | ----- |
+| Redis | 6 | cache.m6g.large | 3 |
+| MySQL | 8.0.mysql_aurora.3.04.2 | db.r6g.4xlarge | 2 |
+
###### [Up to 300000 hosts](https://calculator.aws/#/estimate?id=19b667fde567df0d64d9fae632d4885d7fdc726a)
@@ -203,7 +205,7 @@ assume On-Demand pricing (savings are available through Reserved Instances). Cal
| Dependencies | Version | Instance type | Nodes |
| ------------ | ----------------------- | --------------- | ----- |
-| Redis | 6 | m6g.large | 3 |
+| Redis | 6 | cache.m6g.large | 3 |
| MySQL | 8.0.mysql_aurora.3.04.2 | db.r6g.16xlarge | 2 |
AWS reference architecture can be found [here](https://github.com/fleetdm/fleet/tree/main/terraform/example). This configuration includes:
diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md
index c0e80f4b2e..5f1355b80b 100644
--- a/docs/Deploy/deploy-fleet.md
+++ b/docs/Deploy/deploy-fleet.md
@@ -43,11 +43,11 @@ Render is a cloud hosting service that makes it easy to get up and running fast,
-1. Click "Deploy to Render" to open the Fleet Blueprint on Render. You will be prompted to create or log in to your Render account with associated payment information.
+1. Click "Deploy to Render" to open the Fleet Blueprint on Render. Ensure that the Redis instance is manually set to the same region as your other resources. You will be prompted to create or log in to your Render account with associated payment information.
2. Give the Blueprint a unique name like `yourcompany-fleet`.
-3. Click "**Apply.**" Render will provision your services, which should take less than five minutes.
+3. Click "**Deploy Blueprint.**" Render will provision your services, which should take less than five minutes.
4. Click the "**Dashboard**" tab in Render when provisioning is complete to see your new services.
diff --git a/docs/Get started/FAQ.md b/docs/Get started/FAQ.md
index fdcd1ab596..6e4fb060f4 100644
--- a/docs/Get started/FAQ.md
+++ b/docs/Get started/FAQ.md
@@ -45,6 +45,60 @@ When you collect data with Fleet, the [performance impact](https://fleetdm.com/r
You can test changes on a small subset of hosts first, then roll them out to the rest of your organization.
+## What browsers does Fleet supported?
+
+Fleet supports the latest, stable releases of all major browsers and platforms.
+
+We test each browser on Windows whenever possible, because our engineering team primarily uses macOS.
+
+**Note:** This information also applies to [fleetdm.com](https://www.fleetdm.com).
+
+### Desktop
+
+- Chrome
+- Firefox
+- Edge
+- Safari (macOS only)
+
+### Mobile
+
+- Mobile Safari on iOS
+- Mobile Chrome on Android
+
+### Note
+> - Mobile web is not yet supported in the Fleet product.
+> - The Fleet user interface [may not be fully supported](https://github.com/fleetdm/fleet/issues/969) in Google Chrome when the browser is running on ChromeOS.
+
+## What host operating systems does Fleet support?
+
+Fleet supports the following operating system versions on hosts.
+
+| OS | Supported version(s) |
+| :------ | :------------------------------------- |
+| macOS | 13+ (Ventura) |
+| Windows | Pro and Enterprise 10+, Server 2012+ |
+| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ |
+| ChromeOS | 112.0.5615.134+ |
+
+While Fleet may still function partially or fully with OS versions older than those above, Fleet does not actively test against unsupported versions and does not pursue bugs on them.
+
+## Some notes on compatibility
+
+### Tables
+Not all osquery tables are available for every OS. Please check out the [osquery schema](https://fleetdm.com/tables) for detailed information.
+
+If a table is not available for your host, Fleet will generally handle things behind the scenes for you.
+
+### Linux
+
+Fleet Desktop is supported on Ubuntu and Fedora.
+
+Fedora requires a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome for Fleet Desktop.
+
+On Ubuntu, Fleet Desktop currently supports Xorg as X11 server, Wayland is currently not supported. Ubuntu 24.04 comes with Wayland enabled by default. To use X11 instead of Wayland you can set `WaylandEnable=false` in `/etc/gdm3/custom.conf` and reboot.
+
+The `fleetctl package` command is not supported on DISA-STIG distribution.
+
## Is Fleet MIT licensed?
Different portions of the Fleet software are licensed differently, as noted in the [LICENSE](https://github.com/fleetdm/fleet/blob/main/LICENSE) file. The majority of Fleet is MIT licensed. Paid features require a license key.
@@ -71,7 +125,7 @@ Different portions of the Fleet software are licensed differently, as noted in t
## How do I contact Fleet for support?
-A lot of questions can be answered [in the documentation](https://fleetdm.com/docs).
+A lot of questions can be answered [in the documentation](https://fleetdm.com/docs) or [guides](https://fleetdm.com/guides).
To get help from the community, visit https://fleetdm.com/support.
@@ -614,7 +668,7 @@ Yes! Please sign up for the [Fleet Cloud Beta](https://kqphpqst851.typeform.com/
### What MySQL versions are supported?
-Fleet is tested with MySQL 8.0.36. Newer versions of MySQL 8 typically work well. AWS Aurora requires at least version 2.10.0. Please avoid using MariaDB or other MySQL variants that are not officially supported. Compatibility issues have been identified with MySQL variants, and these may not be addressed in future Fleet releases.
+Fleet is tested with MySQL 8.0.36 and 8.4.2. Newer versions of MySQL 8 typically work well. AWS Aurora requires at least version 3.07.0. Please avoid using MariaDB or other MySQL variants that are not officially supported. Compatibility issues have been identified with MySQL variants, and these may not be addressed in future Fleet releases.
### What are the MySQL user requirements?
@@ -677,7 +731,7 @@ If you would like to use Fleet's MDM features, the following endpoints need to b
### What is the minimum version of MySQL required by Fleet?
-Fleet requires at least MySQL version 8.0.
+Fleet requires at least MySQL version 8.0.36, and is tested [with versions 8.0.36 and 8.4.2](https://github.com/fleetdm/fleet/blob/main/.github/workflows/test-go.yaml#L47)
### How do I migrate from Fleet Free to Fleet Premium?
diff --git a/docs/Get started/tutorials-and-guides.md b/docs/Get started/tutorials-and-guides.md
index 0b1584f6a3..f83de79f27 100644
--- a/docs/Get started/tutorials-and-guides.md
+++ b/docs/Get started/tutorials-and-guides.md
@@ -1,71 +1,29 @@
# Tutorials and guides
-A collection of guides to help you get up and running with Fleet.
+A collection of guides to help you with Fleet.
-## Deployment guides
-
-- [Deploy Fleet on Render](http://fleetdm.com/deploy/deploying-fleet-on-render)
-
-- [Deploy Fleet on AWS](http://fleetdm.com/deploy/deploying-fleet-on-aws-with-terraform)
-
-- [Deploy Fleet on Hetzner Cloud](http://fleetdm.com/deploy/deploy-fleet-on-hetzner-cloud)
-
-- [Deploy Fleet on AWS ECS](https://fleetdm.com/docs/deploy/deploy-fleet-on-aws-ecs)
-
-- [Deploy Fleet on CentOS](https://fleetdm.com/docs/deploy/deploy-fleet-on-centos)
-
-- [Deploy Fleet on Cloud.gov](https://fleetdm.com/docs/deploy/cloudgov)
-
-- [Deploy Fleet on Kubernetes](https://fleetdm.com/docs/deploy/deploy-fleet-on-kubernetes)
-
-## How-to guides
-
-- [Querying process_file_events on CentOS 7](https://fleetdm.com/guides/querying-process-file-events-table-on-centos-7)
-
-- [Using GitHub Actions to apply configuration profiles with Fleet](https://fleetdm.com/guides/using-github-actions-to-apply-configuration-profiles-with-fleet)
-
-- [Building an effective dashboard with Fleet's REST API, Flask, and Plotly: A step-by-step guide](https://fleetdm.com/guides/building-an-effective-dashboard-with-fleet-rest-api-flask-and-plotly)
-
-- [Discovering Geacon using Fleet](https://fleetdm.com/guides/discovering-geacon-using-fleet)
-
-- [Using Fleet and Okta Workflows to generate a daily OS report](https://fleetdm.com/guides/using-fleet-and-okta-workflows-to-generate-a-daily-os-report)
-
-- [Using Fleet and Tines together](https://fleetdm.com/guides/using-fleet-and-tines-together)
-
-- [How to use Fleet for zero trust attestation](https://fleetdm.com/guides/zero-trust-attestation-with-fleet)
-
-- [How to use osquery evented tables](https://fleetdm.com/guides/osquery-evented-tables-overview)
-
-- [Enrolling a DigitalOcean Droplet on a Fleet instance](https://fleetdm.com/guides/enrolling-a-digital-ocean-droplet-on-a-fleet-instance)
-
-- [Osquery: a tool to easily ask questions about operating systems](https://fleetdm.com/guides/osquery-a-tool-to-easily-ask-questions-about-operating-systems)
-
-- [How to install osquery and enroll Linux devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-linux-devices-into-fleet)
-
-- [How to install osquery and enroll Windows devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-windows-devices-into-fleet)
-
-- [Delivering data to Snowflake from Fleet and osquery.](https://fleetdm.com/guides/delivering-data-to-snowflake-from-fleet-and-osquery)
-
-- [How to install osquery and enroll macOS devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-macos-devices-into-fleet)
-
-- [How to uninstall osquery](https://fleetdm.com/guides/how-to-uninstall-osquery)
-
-- [Converting unix timestamps with osquery](https://fleetdm.com/guides/converting-unix-timestamps-with-osquery)
-
-- [Correlate network connections with community ID in osquery.](https://fleetdm.com/guides/correlate-network-connections-with-community-id-in-osquery)
-
-- [Using Elasticsearch and Kibana to visualize osquery performance](https://fleetdm.com/guides/using-elasticsearch-and-kibana-to-visualize-osquery-performance)
-
-- [Fleet quick tips — identify systems where the ProcDump EULA has been accepted](https://fleetdm.com/guides/fleet-quick-tips-querying-procdump-eula-has-been-accepted)
-
-- [Locate device assets in the event of an emergency.](https://fleetdm.com/guides/locate-assets-with-osquery)
-
-- [Osquery: Consider joining against the users table](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
-
-- [Import and export queries in Fleet](https://fleetdm.com/guides/import-and-export-queries-in-fleet)
-
-- [Generate process trees with osquery](https://fleetdm.com/guides/generate-process-trees-with-osquery)
+- [How to install osquery and enroll macOS devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-macos-devices-into-fleet)
+- [How to install osquery and enroll Linux devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-linux-devices-into-fleet)
+- [How to install osquery and enroll Windows devices into Fleet](https://fleetdm.com/guides/how-to-install-osquery-and-enroll-windows-devices-into-fleet)
+- [Sysadmin diaries: restoring fleetd](https://fleetdm.com/guides/sysadmin-diaries-restoring-fleetd)
+- [How to uninstall osquery](https://fleetdm.com/guides/how-to-uninstall-osquery)
+- [Sysadmin diaries: device enrollment](https://fleetdm.com/guides/sysadmin-diaries-device-enrollment)
+- [Sysadmin diaries: passcode profiles](https://fleetdm.com/guides/sysadmin-diaries-passcode-profiles)
+- [Sysadmin diaries: lost device](https://fleetdm.com/guides/sysadmin-diaries-lost-device)
+- [Windows MDM setup](https://fleetdm.com/guides/windows-mdm-setup)
+- [Using GitHub Actions to apply configuration profiles with Fleet](https://fleetdm.com/guides/using-github-actions-to-apply-configuration-profiles-with-fleet)
+- [Managing labels in Fleet](https://fleetdm.com/guides/managing-labels-in-fleet)
+- [What are Fleet policies?](https://fleetdm.com/securing/what-are-fleet-policies)
+- [Understanding the intricacies of Fleet policies](https://fleetdm.com/guides/understanding-the-intricacies-of-fleet-policies)
+- [Sysadmin diaries: exporting policies](https://fleetdm.com/guides/sysadmin-diaries-exporting-policies)
+- [Locate device assets in the event of an emergency](https://fleetdm.com/guides/locate-assets-with-osquery)
+- [Osquery: Consider joining against the users table](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
+- [Using Fleet and Okta Workflows to generate a daily OS report](https://fleetdm.com/guides/using-fleet-and-okta-workflows-to-generate-a-daily-os-report)
+- [How to configure logging destinations](https://fleetdm.com/guides/how-to-configure-logging-destinations)
+- [Import and export queries in Fleet](https://fleetdm.com/guides/import-and-export-queries-in-fleet)
+- [Certificates in fleetd](https://fleetdm.com/guides/certificates-in-fleetd)
+See all guides
diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md
index e07c86e33f..d7b8b8f172 100644
--- a/docs/REST API/rest-api.md
+++ b/docs/REST API/rest-api.md
@@ -484,6 +484,22 @@ for pagination. For a comprehensive list of activity types and detailed informat
```json
{
"activities": [
+ {
+ "created_at": "2023-07-27T14:35:08Z",
+ "id": 25,
+ "actor_full_name": "Anna Chao",
+ "actor_id": 3,
+ "actor_gravatar": "",
+ "actor_email": "",
+ "type": "uninstalled_software",
+ "details": {
+ "host_id": 1,
+ "host_display_name": "Marko's MacBook Pro",
+ "software_title": "Adobe Acrobat.app",
+ "script_execution_id": "eeeddb94-52d3-4071-8b18-7322cd382abb",
+ "status": "failed"
+ }
+ },
{
"created_at": "2021-07-30T13:41:07Z",
"id": 24,
@@ -878,15 +894,20 @@ None.
"additional_queries": null
},
"mdm": {
- "apple_bm_default_team": "",
- "apple_bm_terms_expired": false,
- "enabled_and_configured": true,
"windows_enabled_and_configured": true,
"enable_disk_encryption": true,
"macos_updates": {
"minimum_version": "12.3.1",
"deadline": "2022-01-01"
},
+ "ios_updates": {
+ "minimum_version": "17.0.1",
+ "deadline": "2024-08-01"
+ },
+ "ipados_updates": {
+ "minimum_version": "17.0.1",
+ "deadline": "2024-08-01"
+ },
"windows_updates": {
"deadline_days": 5,
"grace_period_days": 1
@@ -1068,23 +1089,23 @@ Modifies the Fleet's configuration with the supplied information.
#### Parameters
-| Name | Type | In | Description |
-| --------------------- | ------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| org_info | object | body | See [org_info](#org-info). |
-| server_settings | object | body | See [server_settings](#server-settings). |
-| smtp_settings | object | body | See [smtp_settings](#smtp-settings). |
-| 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. |
-| fleet_desktop | object | body | See [fleet_desktop](#fleet-desktop). |
-| webhook_settings | object | body | See [webhook_settings](#webhook-settings). |
-| integrations | object | body | Includes `jira`, `zendesk`, and `google_calendar` arrays. See [integrations](#integrations) for details. |
-| mdm | object | body | See [mdm](#mdm). |
-| features | object | body | See [features](#features). |
-| scripts | list | body | A list of script files to add so they can be executed at a later time. |
-| force | boolean | query | Whether to force-apply the agent options even if there are validation errors. |
-| dry_run | boolean | query | Whether to validate the configuration and return any validation errors **without** applying changes. |
+| Name | Type | In | Description |
+| ----------------------- | ------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| org_info | object | body | See [org_info](#org-info). |
+| server_settings | object | body | See [server_settings](#server-settings). |
+| smtp_settings | object | body | See [smtp_settings](#smtp-settings). |
+| 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. |
+| fleet_desktop | object | body | See [fleet_desktop](#fleet-desktop). |
+| webhook_settings | object | body | See [webhook_settings](#webhook-settings). |
+| integrations | object | body | Includes `jira`, `zendesk`, and `google_calendar` arrays. See [integrations](#integrations) for details. |
+| mdm | object | body | See [mdm](#mdm). |
+| features | object | body | See [features](#features). |
+| scripts | array | body | A list of script files to add so they can be executed at a later time. |
+| force | boolean | query | Whether to force-apply the agent options even if there are validation errors. |
+| dry_run | boolean | query | Whether to validate the configuration and return any validation errors **without** applying changes. |
#### Example
@@ -1162,9 +1183,6 @@ Modifies the Fleet's configuration with the supplied information.
"expiration": "0001-01-01T00:00:00Z"
},
"mdm": {
- "apple_bm_default_team": "",
- "apple_bm_terms_expired": false,
- "apple_bm_enabled_and_configured": false,
"enabled_and_configured": false,
"windows_enabled_and_configured": false,
"enable_disk_encryption": true,
@@ -1172,6 +1190,14 @@ Modifies the Fleet's configuration with the supplied information.
"minimum_version": "12.3.1",
"deadline": "2022-01-01"
},
+ "ios_updates": {
+ "minimum_version": "17.0.1",
+ "deadline": "2024-08-01"
+ },
+ "ipados_updates": {
+ "minimum_version": "17.0.1",
+ "deadline": "2024-08-01"
+ },
"windows_updates": {
"deadline_days": 5,
"grace_period_days": 1
@@ -1310,11 +1336,11 @@ Modifies the Fleet's configuration with the supplied information.
#### org_info
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| org_name | string | The organization name. |
-| org_logo_url | string | The URL for the organization logo. |
-| org_logo_url_light_background | string | The URL for the organization logo displayed in Fleet on top of light backgrounds. |
-| contact_url | string | A URL that can be used by end users to contact the organization. |
+| --------------------- | ------- | ----------------------------------------------------------------------------------- |
+| org_name | string | The organization name. |
+| org_logo_url | string | The URL for the organization logo. |
+| org_logo_url_light_background | string | The URL for the organization logo displayed in Fleet on top of light backgrounds. |
+| contact_url | string | A URL that can be used by end users to contact the organization. |
@@ -1334,12 +1360,12 @@ Modifies the Fleet's configuration with the supplied information.
#### server_settings
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| server_url | string | The Fleet server URL. |
-| enable_analytics | boolean | Whether to send anonymous usage statistics. Always enabled for Fleet Premium customers. |
-| live_query_disabled | boolean | Whether the live query capabilities are disabled. |
-| query_reports_disabled | boolean | Whether query report capabilities are disabled. |
-| ai_features_disabled | boolean | Whether AI features are disabled. |
+| --------------------- | ------- | ------------------------------------------------------------------------------------------- |
+| server_url | string | The Fleet server URL. |
+| enable_analytics | boolean | Whether to send anonymous usage statistics. Always enabled for Fleet Premium customers. |
+| live_query_disabled | boolean | Whether the live query capabilities are disabled. |
+| query_reports_disabled | boolean | Whether query report capabilities are disabled. |
+| ai_features_disabled | boolean | Whether AI features are disabled. |
| query_report_cap | integer | The maximum number of results to store per query report before the report is clipped. If increasing this cap, we recommend enabling reports for one query at time and monitoring your infrastructure. (Default: `1000`) |
@@ -1361,7 +1387,7 @@ Modifies the Fleet's configuration with the supplied information.
#### smtp_settings
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| enable_smtp | boolean | Whether SMTP is enabled for the Fleet app. |
| sender_address | string | The sender email address for the Fleet app. An invitation email is an example of the emails that may use this sender address |
| server | string | The SMTP server for the Fleet app. |
@@ -1401,13 +1427,13 @@ Modifies the Fleet's configuration with the supplied information.
#### sso_settings
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| enable_sso | boolean | Whether or not SSO is enabled for the Fleet application. If this value is true, you must also include most of the SSO settings parameters below. |
-| entity_id | string | The required entity ID is a URI that you use to identify Fleet when configuring the identity provider. |
+| entity_id | string | The required entity ID is a URI that you use to identify Fleet when configuring the identity provider. Must be 5 or more characters. |
| issuer_uri | string | The URI you provide here must exactly match the Entity ID field used in the identity provider configuration. |
| idp_image_url | string | An optional link to an image such as a logo for the identity provider. |
-| metadata_url | string | A URL that references the identity provider metadata. If available from the identity provider, this is the preferred means of providing metadata. |
-| metadata | string | Metadata provided by the identity provider. Either `metadata` or a `metadata_url` must be provided. |
+| metadata_url | string | A URL that references the identity provider metadata. If available from the identity provider, this is the preferred means of providing metadata. Must be either https or http |
+| metadata | string | Metadata provided by the identity provider. Either `metadata` or a `metadata_url` must be provided. |
| enable_sso_idp_login | boolean | Determines whether Identity Provider (IdP) initiated login for Single sign-on (SSO) is enabled for the Fleet application. |
| enable_jit_provisioning | boolean | _Available in Fleet Premium._ When enabled, allows [just-in-time user provisioning](https://fleetdm.com/docs/deploy/single-sign-on-sso#just-in-time-jit-user-provisioning). |
@@ -1434,9 +1460,9 @@ Modifies the Fleet's configuration with the supplied information.
#### host_expiry_settings
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| host_expiry_enabled | boolean | When enabled, allows automatic cleanup of hosts that have not communicated with Fleet in some number of days. |
-| host_expiry_window | integer | If a host has not communicated with Fleet in the specified number of days, it will be removed. |
+| host_expiry_window | integer | If a host has not communicated with Fleet in the specified number of days, it will be removed. Must be greater than 0 if host_expiry_enabled is set to true. |
@@ -1454,9 +1480,9 @@ Modifies the Fleet's configuration with the supplied information.
#### activity_expiry_settings
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| activity_expiry_enabled | boolean | When enabled, allows automatic cleanup of activities (and associated live query data) older than the specified number of days. |
-| activity_expiry_window | integer | The number of days to retain activity records, if activity expiry is enabled. |
+| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| activity_expiry_enabled | boolean | When enabled, allows automatic cleanup of activities (and associated live query data) older than the specified number of days. |
+| activity_expiry_window | integer | The number of days to retain activity records, if activity expiry is enabled. |
@@ -1476,8 +1502,8 @@ Modifies the Fleet's configuration with the supplied information.
_Available in Fleet Premium._
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| transparency_url | string | The URL used to display transparency information to users of Fleet Desktop. |
+| --------------------- | ------- | -------------------------------------------------------------------------------- |
+| transparency_url | string | The URL used to display transparency information to users of Fleet Desktop. |
@@ -1500,12 +1526,12 @@ _Available in Fleet Premium._
+ [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook)
-->
-| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| host_status_webhook | list | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). |
-| failing_policies_webhook | list | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). |
-| vulnerabilities_webhook | list | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). |
-| activities_webhook | list | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
+| Name | Type | Description |
+| --------------------- | ----- | ---------------------------------------------------------------------------------------------- |
+| host_status_webhook | array | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). |
+| failing_policies_webhook | array | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). |
+| vulnerabilities_webhook | array | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). |
+| activities_webhook | array | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
@@ -1514,11 +1540,11 @@ _Available in Fleet Premium._
`webhook_settings.host_status_webhook` is an object with the following structure:
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| enable_host_status_webhook | boolean | Whether or not the host status webhook is enabled. |
-| destination_url | string | The URL to deliver the webhook request to. |
-| host_percentage | integer | The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. |
-| days_count | integer | The minimum number of days that the configured `host_percentage` must fail to check in to Fleet in order to trigger the webhook request. |
+| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| enable_host_status_webhook | boolean | Whether or not the host status webhook is enabled. |
+| destination_url | string | The URL to deliver the webhook request to. |
+| host_percentage | integer | The minimum percentage of hosts that must fail to check in to Fleet in order to trigger the webhook request. |
+| days_count | integer | The minimum number of days that the configured `host_percentage` must fail to check in to Fleet in order to trigger the webhook request. |
@@ -1527,9 +1553,9 @@ _Available in Fleet Premium._
`webhook_settings.failing_policies_webhook` is an object with the following structure:
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| enable_failing_policies_webhook | boolean | Whether or not the failing policies webhook is enabled. |
-| destination_url | string | The URL to deliver the webhook requests to. |
+| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
+| enable_failing_policies_webhook | boolean | Whether or not the failing policies webhook is enabled. |
+| destination_url | string | The URL to deliver the webhook requests to. |
| policy_ids | array | List of policy IDs to enable failing policies webhook. |
| host_batch_size | integer | Maximum number of hosts to batch on failing policy webhook requests. The default, 0, means no batching (all hosts failing a policy are sent on one request). |
@@ -1540,9 +1566,9 @@ _Available in Fleet Premium._
`webhook_settings.vulnerabilities_webhook` is an object with the following structure:
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| enable_vulnerabilities_webhook | boolean | Whether or not the vulnerabilities webhook is enabled. |
-| destination_url | string | The URL to deliver the webhook requests to. |
+| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| enable_vulnerabilities_webhook | boolean | Whether or not the vulnerabilities webhook is enabled. |
+| destination_url | string | The URL to deliver the webhook requests to. |
| host_batch_size | integer | Maximum number of hosts to batch on vulnerabilities webhook requests. The default, 0, means no batching (all vulnerable hosts are sent on one request). |
@@ -1552,9 +1578,9 @@ _Available in Fleet Premium._
`webhook_settings.activities_webhook` is an object with the following structure:
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| enable_activities_webhook | boolean | Whether or not the activity feed webhook is enabled. |
-| destination_url | string | The URL to deliver the webhook requests to. |
+| --------------------- | ------- | --------------------------------------------------------- |
+| enable_activities_webhook | boolean | Whether or not the activity feed webhook is enabled. |
+| destination_url | string | The URL to deliver the webhook requests to. |
@@ -1596,11 +1622,11 @@ _Available in Fleet Premium._
+ [`integrations.google_calendar`](#integrations-google-calendar)
-->
-| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| jira | list | See [`integrations.jira`](#integrations-jira). |
-| zendesk | list | See [`integrations.zendesk`](#integrations-zendesk). |
-| google_calendar | list | See [`integrations.google_calendar`](#integrations-google-calendar). |
+| Name | Type | Description |
+| --------------------- | ----- | -------------------------------------------------------------------- |
+| jira | array | See [`integrations.jira`](#integrations-jira). |
+| zendesk | array | See [`integrations.zendesk`](#integrations-zendesk). |
+| google_calendar | array | See [`integrations.google_calendar`](#integrations-google-calendar). |
> Note that when making changes to the `integrations` object, all integrations must be provided (not just the one being modified). This is because the endpoint will consider missing integrations as deleted.
@@ -1642,8 +1668,8 @@ _Available in Fleet Premium._
`integrations.google_calendar` is an array of objects with the following structure:
| Name | Type | Description |
-| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| domain | string | The domain for the Google Workspace service account to be used for this calendar integration. |
+| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
+| domain | string | The domain for the Google Workspace service account to be used for this calendar integration. |
| api_key_json | object | The private key JSON downloaded when generating the service account API key to be used for this calendar integration. |
@@ -1678,10 +1704,12 @@ _Available in Fleet Premium._
| Name | Type | Description |
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| apple_bm_default_team | string | _Available in Fleet Premium._ The default team to use with Apple Business Manager. |
| windows_enabled_and_configured | boolean | Enables Windows MDM support. |
| enable_disk_encryption | boolean | _Available in Fleet Premium._ Hosts that belong to no team will have disk encryption enabled if set to true. |
| 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). |
+| windows_updates | object | See [`mdm.window_updates`](#mdm-windows-updates). |
| macos_migration | object | See [`mdm.macos_migration`](#mdm-macos-migration). |
| macos_setup | object | See [`mdm.macos_setup`](#mdm-macos-setup). |
| macos_settings | object | See [`mdm.macos_settings`](#mdm-macos-settings). |
@@ -1702,6 +1730,32 @@ _Available in Fleet Premium._
+##### mdm.ios_updates
+
+_Available in Fleet Premium._
+
+`mdm.ios_updates` is an object with the following structure:
+
+| Name | Type | Description |
+| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| minimum_version | string | Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their iOS is at or above this version. |
+| deadline | string | Hosts that belong to no team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. |
+
+
+
+##### mdm.ipados_updates
+
+_Available in Fleet Premium._
+
+`mdm.ipados_updates` is an object with the following structure:
+
+| Name | Type | Description |
+| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| minimum_version | string | Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their iPadOS is at or above this version. |
+| deadline | string | Hosts that belong to no team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. |
+
+
+
##### mdm.windows_updates
_Available in Fleet Premium._
@@ -1747,7 +1801,7 @@ _Available in Fleet Premium._
| Name | Type | Description |
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| custom_settings | list | macOS hosts that belong to no team will have custom profiles applied. |
+| custom_settings | array | macOS hosts that belong to no team will have custom profiles applied. |
@@ -1757,7 +1811,7 @@ _Available in Fleet Premium._
| Name | Type | Description |
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| custom_settings | list | Windows hosts that belong to no team will have custom profiles applied. |
+| custom_settings | array | Windows hosts that belong to no team will have custom profiles applied. |
@@ -1766,7 +1820,6 @@ _Available in Fleet Premium._
```json
{
"mdm": {
- "apple_bm_default_team": "",
"windows_enabled_and_configured": false,
"enable_disk_encryption": true,
"macos_updates": {
@@ -2054,7 +2107,7 @@ Delete all of a team's existing enroll secrets
| 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. |
-| teams | list | 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. |
+| 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. |
#### Example
@@ -2254,7 +2307,7 @@ Verify the specified invite.
| 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. |
-| teams | list | 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. |
+| 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. |
#### Example
@@ -2378,7 +2431,6 @@ None.
- [Get host OS version](#get-host-os-version)
- [Get host's scripts](#get-hosts-scripts)
- [Get host's software](#get-hosts-software)
-- [Install software](#install-software)
- [Get hosts report in CSV](#get-hosts-report-in-csv)
- [Get host's disk encryption key](#get-hosts-disk-encryption-key)
- [Lock host](#lock-host)
@@ -2445,6 +2497,7 @@ the `software` table.
| 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'. |
+| 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.** |
| 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. |
@@ -3055,10 +3108,11 @@ Returns the information of the specified host.
"timezone": "America/New_York"
},
"mdm": {
- "encryption_key_available": false,
- "enrollment_status": null,
- "name": "",
- "server_url": null,
+ "encryption_key_available": true,
+ "enrollment_status": "On (manual)",
+ "name": "Fleet",
+ "connected_to_fleet": true,
+ "server_url": "https://acme.com/mdm/apple/mdm",
"device_status": "unlocked",
"pending_action": "",
"macos_settings": {
@@ -3474,10 +3528,11 @@ This is the API route used by the **My device** page in Fleet desktop to display
}
],
"mdm": {
- "encryption_key_available": false,
- "enrollment_status": null,
- "name": "",
- "server_url": null,
+ "encryption_key_available": true,
+ "enrollment_status": "On (manual)",
+ "name": "Fleet",
+ "connected_to_fleet": true,
+ "server_url": "https://acme.com/mdm/apple/mdm",
"macos_settings": {
"disk_encryption": null,
"action_required": null
@@ -3504,6 +3559,7 @@ This is the API route used by the **My device** page in Fleet desktop to display
]
}
},
+ "self_service": true,
"org_logo_url": "https://example.com/logo.jpg",
"license": {
"tier": "free",
@@ -3651,7 +3707,7 @@ _Available in Fleet Premium_
| Name | Type | In | Description |
| ------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| ids | list | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. |
+| ids | array | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. |
| filters | object | body | Contains any of the following four properties: `query` for search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. `status` to indicate the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. `label_id` to indicate the selected label. `team_id` to indicate the selected team. If `filters` is specified, `id` cannot be specified. `label_id` and `status` cannot be used at the same time. |
Either ids or filters are required.
@@ -4106,139 +4162,6 @@ Resends a configuration profile for the specified host.
`Status: 202`
-
-### List host OS versions
-
-Retrieves the aggregated host OS versions information.
-
-`GET /api/v1/fleet/os_versions`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| --- | --- | --- | --- |
-| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. |
-| 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` |
-| 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: `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`. |
-
-
-##### Default response
-
-`Status: 200`
-
-```json
-{
- "count": 1
- "counts_updated_at": "2023-12-06T22:17:30Z",
- "os_versions": [
- {
- "os_version_id": 123,
- "hosts_count": 21,
- "name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
- "name_only": "Microsoft Windows 11 Pro 23H2",
- "version": "10.0.22621.1234",
- "platform": "windows",
- "generated_cpes": [],
- "vulnerabilities": [
- {
- "cve": "CVE-2022-30190",
- "details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
- "cvss_score": 7.8,// Available in Fleet Premium
- "epss_probability": 0.9729,// Available in Fleet Premium
- "cisa_known_exploit": false,// Available in Fleet Premium
- "cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
- "cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
- "resolved_in_version": ""// Available in Fleet Premium
- }
- ]
- }
- ],
- "meta": {
- "has_next_results": false,
- "has_previous_results": false
- }
-}
-```
-
-OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
-
-```json
-{
- "hosts_count": 1,
- "name": "CentOS Linux 7.9.2009",
- "name_only": "CentOS",
- "version": "7.9.2009",
- "platform": "rhel",
- "generated_cpes": [],
- "vulnerabilities": []
-}
-```
-
-### Get host OS version
-
-Retrieves information about the specified OS version.
-
-`GET /api/v1/fleet/os_versions/:id`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| ---- | ---- | -- | ----------- |
-| id | integer | path | **Required.** The OS version's ID. |
-
-##### Default response
-
-`Status: 200`
-
-```json
-{
- "counts_updated_at": "2023-12-06T22:17:30Z",
- "os_version": {
- "id": 123,
- "hosts_count": 21,
- "name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
- "name_only": "Microsoft Windows 11 Pro 23H2",
- "version": "10.0.22621.1234",
- "platform": "windows",
- "generated_cpes": [],
- "vulnerabilities": [
- {
- "cve": "CVE-2022-30190",
- "details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
- "created_at": "2024-07-01T00:15:00Z",
- "cvss_score": 7.8,// Available in Fleet Premium
- "epss_probability": 0.9729,// Available in Fleet Premium
- "cisa_known_exploit": false,// Available in Fleet Premium
- "cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
- "cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
- "resolved_in_version": ""// Available in Fleet Premium
- }
- ]
- }
-}
-```
-
-OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
-
-```json
-{
- "id": 321,
- "hosts_count": 1,
- "name": "CentOS Linux 7.9.2009",
- "name_only": "CentOS",
- "version": "7.9.2009",
- "platform": "rhel",
- "generated_cpes": [],
- "vulnerabilities": []
-}
-```
-
-
### Get host's scripts
`GET /api/v1/fleet/hosts/:id/scripts`
@@ -4260,46 +4183,45 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
`Status: 200`
```json
- "scripts": [
- {
- "script_id": 3,
- "name": "remove-zoom-artifacts.sh",
- "last_execution": {
- "execution_id": "e797d6c6-3aae-11ee-be56-0242ac120002",
- "executed_at": "2021-12-15T15:23:57Z",
- "status": "error"
- }
- },
- {
- "script_id": 5,
- "name": "set-timezone.sh",
- "last_execution": {
- "id": "e797d6c6-3aae-11ee-be56-0242ac120002",
- "executed_at": "2021-12-15T15:23:57Z",
- "status": "pending"
- }
- },
- {
- "script_id": 8,
- "name": "uninstall-zoom.sh",
- "last_execution": {
- "id": "e797d6c6-3aae-11ee-be56-0242ac120002",
- "executed_at": "2021-12-15T15:23:57Z",
- "status": "ran"
- }
+"scripts": [
+ {
+ "script_id": 3,
+ "name": "remove-zoom-artifacts.sh",
+ "last_execution": {
+ "execution_id": "e797d6c6-3aae-11ee-be56-0242ac120002",
+ "executed_at": "2021-12-15T15:23:57Z",
+ "status": "error"
+ }
+ },
+ {
+ "script_id": 5,
+ "name": "set-timezone.sh",
+ "last_execution": {
+ "id": "e797d6c6-3aae-11ee-be56-0242ac120002",
+ "executed_at": "2021-12-15T15:23:57Z",
+ "status": "pending"
+ }
+ },
+ {
+ "script_id": 8,
+ "name": "uninstall-zoom.sh",
+ "last_execution": {
+ "id": "e797d6c6-3aae-11ee-be56-0242ac120002",
+ "executed_at": "2021-12-15T15:23:57Z",
+ "status": "ran"
}
- ],
- "meta": {
- "has_next_results": false,
- "has_previous_results": false
}
+],
+"meta": {
+ "has_next_results": false,
+ "has_previous_results": false
}
```
### Get host's software
-> The **new keys/values added in the app management features are experimental** and may change. You can find the upcoming breaking changes [here](https://github.com/fleetdm/fleet/pull/19291/files#diff-7246bc304b15c8865ed8eaa205e9c244d0a0314e4bae60cf553dc06147c38b64L4304-L4311).
+> **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.
`GET /api/v1/fleet/hosts/:id/software`
@@ -4309,6 +4231,7 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
| ---- | ------- | ---- | ---------------------------- |
| id | integer | path | **Required**. The host's ID. |
| query | string | query | Search query keywords. Searchable fields include `name`. |
+| available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`.
| page | integer | query | Page number of the results to fetch.|
| per_page | integer | query | Results per page.|
@@ -4327,14 +4250,18 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
{
"id": 121,
"name": "Google Chrome.app",
- "package_available_for_install": "GoogleChrome.pkg",
- "self_service": true,
+ "software_package": {
+ "name": "GoogleChrome.pkg",
+ "version": "125.12.0.3",
+ "self_service": true,
+ "last_install": {
+ "install_uuid": "8bbb8ac2-b254-4387-8cba-4d8a0407368b",
+ "installed_at": "2024-05-15T15:23:57Z"
+ }
+ },
+ "app_store_app": null
"source": "apps",
"status": "failed",
- "last_install": {
- "install_uuid": "8bbb8ac2-b254-4387-8cba-4d8a0407368b",
- "installed_at": "2024-05-15T15:23:57Z"
- },
"installed_versions": [
{
"version": "121.0",
@@ -4347,32 +4274,42 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
{
"id": 134,
"name": "Falcon.app",
- "package_available_for_install": "FalconSensor-6.44.pkg",
- "self_service": false,
+ "software_package": {
+ "name": "FalconSensor-6.44.pkg"
+ "self_service": false,
+ "last_install": null
+ "last_install": null,
+ "last_uninstall": {
+ "script_execution_id": "ed579e73-0f41-46c8-aaf4-3c1e5880ed27",
+ "uninstalled_at": "2024-05-15T15:23:57Z"
+ }
+ },
+ "app_store_app": null
"source": "",
"status": null,
- "last_install": null,
+ "status": "pending_uninstall",
"installed_versions": [],
},
{
"id": 147,
- "name": "Firefox.app",
+ "name": "Logic Pro",
+ "software_package": null
+ "app_store_app": {
+ "app_store_id": "1091189122"
+ "version": "2.04",
+ "last_install": {
+ "command_uuid": "0aa14ae5-58fe-491a-ac9a-e4ee2b3aac40",
+ "installed_at": "2024-05-15T15:23:57Z"
+ },
+ },
"source": "apps",
- "bundle_identifier": "org.mozilla.firefox",
- "status": null,
- "last_install": null,
+ "status": "installed",
"installed_versions": [
{
"version": "118.0",
"last_opened_at": "2024-04-01T23:03:07Z",
"vulnerabilities": ["CVE-2023-1234"],
- "installed_paths": ["/Applications/Firefox.app"]
- },
- {
- "version": "119.0",
- "last_opened_at": "2024-04-01T23:03:07Z",
- "vulnerabilities": ["CVE-2023-4321","CVE-2023-7654"],
- "installed_paths": ["/Downloads/Firefox.app"]
+ "installed_paths": ["/Applications/Logic Pro.app"]
}
]
},
@@ -4384,29 +4321,6 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
}
```
-### Install software
-
-_Available in Fleet Premium._
-
-Install software on a macOS, Windows, or Linux (Ubuntu) host. Software title must have `software_package` added to be installed.
-
-`POST /api/v1/fleet/hosts/:id/software/install/:software_title_id`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| --------- | ---------- | ---- | -------------------------------------------- |
-| id | integer | path | **Required**. The host's ID. |
-| software_title_id | integer | path | **Required**. The software title's ID. |
-
-#### Example
-
-`POST /api/v1/fleet/hosts/123/software/install/3435`
-
-##### Default response
-
-`Status: 202`
-
### Get hosts report in CSV
Returns the list of hosts corresponding to the search criteria in CSV format, ready for download when
@@ -4423,8 +4337,8 @@ requested by a web browser.
| order_key | string | query | What to order results by. Can be any column in the hosts 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'. |
| 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. |
+| 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. |
| 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. |
@@ -4434,11 +4348,11 @@ requested by a web browser.
| 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). |
| 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'. |
+| 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'. |
| 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.** |
| 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. |
+| 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.** |
| 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. |
@@ -4654,6 +4568,38 @@ To wipe a macOS, iOS, iPadOS, or Windows host, the host must have MDM turned on.
```json
{
"activities": [
+ {
+ "created_at": "2023-07-27T14:35:08Z",
+ "actor_id": 1,
+ "actor_full_name": "Anna Chao",
+ "id": 4,
+ "actor_gravatar": "",
+ "actor_email": "",
+ "type": "uninstalled_software",
+ "details": {
+ "host_id": 1,
+ "host_display_name": "Marko’s MacBook Pro",
+ "software_title": "Adobe Acrobat.app",
+ "script_execution_id": "ecf22dba-07dc-40a9-b122-5480e948b756",
+ "status": "failed"
+ }
+ },
+ {
+ "created_at": "2023-07-27T14:35:08Z",
+ "actor_id": 1,
+ "actor_full_name": "Anna Chao",
+ "id": 3,
+ "actor_gravatar": "",
+ "actor_email": "",
+ "type": "uninstalled_software",
+ "details": {
+ "host_id": 1,
+ "host_display_name": "Marko’s MacBook Pro",
+ "software_title": "Adobe Acrobat.app",
+ "script_execution_id": "ecf22dba-07dc-40a9-b122-5480e948b756",
+ "status": "uninstalled"
+ }
+ },
{
"created_at": "2023-07-27T14:35:08Z",
"id": 2,
@@ -4716,8 +4662,25 @@ To wipe a macOS, iOS, iPadOS, or Windows host, the host must have MDM turned on.
```json
{
- "count": 2,
+ "count": 3,
"activities": [
+ {
+ "created_at": "2023-07-27T14:35:08Z",
+ "actor_id": 1,
+ "actor_full_name": "Anna Chao",
+ "uuid": "cc081637-fdf9-4d44-929f-96dfaec00f67",
+ "actor_gravatar": "",
+ "actor_email": "",
+ "type": "uninstalled_software",
+ "fleet_initiated_activity": false,
+ "details": {
+ "host_id": 1,
+ "host_display_name": "Marko's MacBook Pro",
+ "software_title": "Adobe Acrobat.app",
+ "script_execution_id": "ecf22dba-07dc-40a9-b122-5480e948b756",
+ "status": "pending_uninstall",
+ }
+ },
{
"created_at": "2023-07-27T14:35:08Z",
"uuid": "d6cffa75-b5b5-41ef-9230-15073c8a88cf",
@@ -4766,9 +4729,9 @@ Adds manual labels to a host.
#### Parameters
-| Name | Type | In | Description |
-| ---- | ------- | ---- | ---------------------------- |
-| labels | list | body | The list of label names to add to the host. |
+| Name | Type | In | Description |
+| ------ | ------- | ---- | ---------------------------- |
+| labels | array | body | The list of label names to add to the host. |
#### Example
@@ -4795,9 +4758,9 @@ Removes manual labels from a host.
#### Parameters
-| Name | Type | In | Description |
-| ---- | ------- | ---- | ---------------------------- |
-| labels | list | body | The list of label names to delete from the host. |
+| Name | Type | In | Description |
+| ------ | ------- | ---- | ---------------------------- |
+| labels | array | body | The list of label names to delete from the host. |
#### Example
@@ -4829,8 +4792,8 @@ The live query will stop if the targeted host is offline, or if the query times
| Name | Type | In | Description |
|-----------|-------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| id | integer | path | **Required**. The target host ID. |
-| query | string | body | **Required**. The query SQL. |
+| id | integer | path | **Required**. The target host ID. |
+| query | string | body | **Required**. The query SQL. |
#### Example
@@ -5392,8 +5355,8 @@ Add a configuration profile to enforce custom settings on macOS and Windows host
| ------------------------- | -------- | ---- | ------------------------------------------------------------------------------------------------------------- |
| profile | file | form | **Required.** The .mobileconfig and JSON for macOS or XML for Windows file containing the profile. |
| team_id | string | form | _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. |
-| labels_include_all | array | form | _Available in Fleet Premium_. Profile will only be applied to hosts that have all of these labels. |
-| labels_exclude_any | array | form | _Available in Fleet Premium_. Profile will be applied to hosts that don’t have any of these labels. |
+| labels_include_all | array | form | _Available in Fleet Premium_. Profile will only be applied to hosts that have all of these labels. Only one of either `labels_include_all` or `labels_exclude_any` can be included in the request. |
+| labels_exclude_any | array | form | _Available in Fleet Premium_. Profile will be applied to hosts that don’t have any of these labels. Only one of either `labels_include_all` or `labels_exclude_any` can be included in the request. |
#### Example
@@ -5510,7 +5473,8 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's
"checksum": "dGVzdAo=",
"labels_exclude_any": [
{
- "name": "Label name 1"
+ "name": "Label name 1",
+ "id": 1
}
]
},
@@ -5524,11 +5488,12 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's
"checksum": "aCLemVr)",
"labels_include_all": [
{
- "name": "Label name 1",
- "broken": true
+ "name": "Label name 2",
+ "broken": true,
},
{
- "name": "Label name 2"
+ "name": "Label name 3",
+ "id": 3
}
]
}
@@ -5576,10 +5541,12 @@ If one or more assigned labels are deleted the profile is considered broken (`br
"labels_include_all": [
{
"name": "Label name 1",
+ "id": 1
"broken": true
},
{
"name": "Label name 2",
+ "id": 2
}
]
}
@@ -6213,12 +6180,12 @@ Body:
## Commands
-- [Run custom MDM command](#run-custom-mdm-command)
-- [Get custom MDM command results](#get-custom-mdm-command-results)
-- [List custom MDM commands](#list-custom-mdm-commands)
+- [Run MDM command](#run-mdm-command)
+- [Get MDM command results](#get-mdm-command-results)
+- [List MDM commands](#list-mdm-commands)
-### Run custom MDM command
+### Run MDM command
> `POST /api/v1/fleet/mdm/apple/enqueue` API endpoint is deprecated as of Fleet 4.40. It is maintained for backward compatibility. Please use the new API endpoint below. See old API endpoint docs [here](https://github.com/fleetdm/fleet/blob/fleet-v4.39.0/docs/REST%20API/rest-api.md#run-custom-mdm-command).
@@ -6251,12 +6218,22 @@ Note that the `EraseDevice` and `DeviceLock` commands are _available in Fleet Pr
```
-### Get custom MDM command results
+### Get MDM command results
> `GET /api/v1/fleet/mdm/apple/commandresults` API endpoint is deprecated as of Fleet 4.40. It is maintained for backward compatibility. Please use the new API endpoint below. See old API endpoint docs [here](https://github.com/fleetdm/fleet/blob/fleet-v4.39.0/docs/REST%20API/rest-api.md#get-custom-mdm-command-results).
This endpoint returns the results for a specific custom MDM command.
+In the reponse, the possible `status` values for macOS, iOS, and iPadOS hosts are the following:
+
+* Pending: the command has yet to run on the host. The host will run the command the next time it comes online.
+* NotNow: the host responded with "NotNow" status via the MDM protocol: the host received the command, but couldn’t execute it. The host will try to run the command the next time it comes online.
+* Acknowledged: the host responded with "Acknowledged" status via the MDM protocol: the host processed the command successfully.
+* Error: the host responded with "Error" status via the MDM protocol: an error occurred. Run the `fleetctl get mdm-command-results --id= Note: If the server has not yet received a result for a command, it will return an empty object (`{}`).
-
-### List custom MDM commands
+### List MDM commands
> `GET /api/v1/fleet/mdm/apple/commands` API endpoint is deprecated as of Fleet 4.40. It is maintained for backward compatibility. Please use the new API endpoint below. See old API endpoint docs [here](https://github.com/fleetdm/fleet/blob/fleet-v4.39.0/docs/REST%20API/rest-api.md#list-custom-mdm-commands).
@@ -6348,7 +6324,8 @@ This endpoint returns the list of custom MDM commands that have been executed.
## Integrations
- [Get Apple Push Notification service (APNs)](#get-apple-push-notification-service-apns)
-- [Get Apple Business Manager (ABM)](#get-apple-business-manager-abm)
+- [List Apple Business Manager (ABM) tokens](#list-apple-business-manager-abm-tokens)
+- [List Volume Purchasing Program (VPP) tokens](#list-volume-purchasing-program-vpp-tokens)
### Get Apple Push Notification service (APNs)
@@ -6375,11 +6352,11 @@ None.
}
```
-### Get Apple Business Manager (ABM)
+### List Apple Business Manager (ABM) tokens
_Available in Fleet Premium_
-`GET /api/v1/fleet/abm`
+`GET /api/v1/fleet/abm_tokens`
#### Parameters
@@ -6387,7 +6364,95 @@ None.
#### Example
-`GET /api/v1/fleet/abm`
+`GET /api/v1/fleet/abm_tokens`
+
+##### Default response
+
+`Status: 200`
+
+```json
+"abm_tokens": [
+ {
+ "id": 1,
+ "apple_id": "apple@example.com",
+ "org_name": "Fleet Device Management Inc.",
+ "mdm_server_url": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2023-11-29T00:00:00Z",
+ "terms_expired": false,
+ "macos_team": {
+ "name": "💻 Workstations",
+ "id" 1
+ },
+ "ios_team": {
+ "name": "📱🏢 Company-owned iPhones",
+ "id": 2
+ },
+ "ipados_team": {
+ "name": "🔳🏢 Company-owned iPads",
+ "id": 3
+ }
+ }
+]
+```
+
+### List Volume Purchasing Program (VPP) tokens
+
+_Available in Fleet Premium_
+
+`GET /api/v1/fleet/vpp_tokens`
+
+#### Parameters
+
+None.
+
+#### Example
+
+`GET /api/v1/fleet/vpp_tokens`
+
+##### Default response
+
+`Status: 200`
+
+```json
+"vpp_tokens": [
+ {
+ "id": 1,
+ "org_name": "Fleet Device Management Inc.",
+ "location": "https://example.com/mdm/apple/mdm",
+ "renew_date": "2023-11-29T00:00:00Z",
+ "teams": [
+ {
+ "name": "💻 Workstations",
+ "id": 1
+ },
+ {
+ "name": "💻🐣 Workstations (canary)",
+ "id": 2
+ },
+ {
+ "name": "📱🏢 Company-owned iPhones",
+ "id": 3
+ },
+ {
+ "name": "🔳🏢 Company-owned iPads",
+ "id" 4
+ }
+ ],
+ }
+]
+```
+
+Get Volume Purchasing Program (VPP)
+
+> **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.
+
+_Available in Fleet Premium_
+
+`GET /api/v1/fleet/vpp`
+
+#### Example
+
+`GET /api/v1/fleet/vpp`
##### Default response
@@ -6395,11 +6460,9 @@ None.
```json
{
- "apple_id": "apple@example.com",
- "org_name": "Fleet Device Management",
- "mdm_server_url": "https://example.com/mdm/apple/mdm",
+ "org_name": "Acme Inc.",
"renew_date": "2023-11-29T00:00:00Z",
- "default_team": ""
+ "location": "Acme Inc. Main Address"
}
```
@@ -6623,7 +6686,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
| Name | Type | In | Description |
| -------- | ------- | ---- | ------------------------------------------------- |
-| ids | list | body | **Required.** The IDs of the policies to delete. |
+| ids | array | body | **Required.** The IDs of the policies to delete. |
#### Example
@@ -6717,8 +6780,8 @@ Triggers [automations](https://fleetdm.com/docs/using-fleet/automations#policy-a
| Name | Type | In | Description |
| ---------- | -------- | ---- | -------------------------------------------------------- |
-| policy_ids | list | body | Filters to only run policy automations for the specified policies. |
-| team_ids | list | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. |
+| 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. |
#### Example
@@ -6820,6 +6883,29 @@ Team policies work the same as policies, but at the team level.
"failing_host_count": 0,
"host_count_updated_at": "2023-12-20T15:23:57Z",
"calendar_events_enabled": false
+ },
+ {
+ "id": 3,
+ "name": "macOS - install/update Adobe Acrobat",
+ "query": "SELECT 1 FROM apps WHERE name = \"Adobe Acrobat.app\" AND bundle_short_version != \"24.002.21005\";",
+ "description": "Checks if the hard disk is encrypted on Windows devices",
+ "critical": false,
+ "author_id": 43,
+ "author_name": "Alice",
+ "author_email": "alice@example.com",
+ "team_id": 1,
+ "resolution": "Resolution steps",
+ "platform": "darwin",
+ "created_at": "2021-12-16T14:37:37Z",
+ "updated_at": "2021-12-16T16:39:00Z",
+ "passing_host_count": 2300,
+ "failing_host_count": 3,
+ "host_count_updated_at": "2023-12-20T15:23:57Z",
+ "calendar_events_enabled": false,
+ "install_software": {
+ "name": "Adobe Acrobat.app",
+ "software_title_id": 1234
+ }
}
],
"inherited_policies": [
@@ -7001,6 +7087,7 @@ The semantics for creating a team policy are the same as for global policies, se
| resolution | string | body | The resolution steps for the policy. |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
+| software_title_id | integer | body | _Available in Fleet Premium_. ID of software title to install if the policy fails. |
Either `query` or `query_id` must be provided.
@@ -7044,7 +7131,11 @@ Either `query` or `query_id` must be provided.
"passing_host_count": 0,
"failing_host_count": 0,
"host_count_updated_at": null,
- "calendar_events_enabled": false
+ "calendar_events_enabled": false,
+ "install_software": {
+ "name": "Adobe Acrobat.app",
+ "software_title_id": 1234
+ }
}
}
```
@@ -7058,7 +7149,7 @@ Either `query` or `query_id` must be provided.
| Name | Type | In | Description |
| -------- | ------- | ---- | ------------------------------------------------- |
| team_id | integer | path | **Required.** Defines what team ID to operate on |
-| ids | list | body | **Required.** The IDs of the policies to delete. |
+| ids | array | body | **Required.** The IDs of the policies to delete. |
#### Example
@@ -7099,6 +7190,7 @@ Either `query` or `query_id` must be provided.
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
| calendar_events_enabled | boolean | body | _Available in Fleet Premium_. Whether to trigger calendar events when policy is failing. |
+| software_title_id | integer | body | _Available in Fleet Premium_. ID of software title to install if the policy fails. |
#### Example
@@ -7140,7 +7232,11 @@ Either `query` or `query_id` must be provided.
"passing_host_count": 0,
"failing_host_count": 0,
"host_count_updated_at": null,
- "calendar_events_enabled": true
+ "calendar_events_enabled": true,
+ "install_software": {
+ "name": "Adobe Acrobat.app",
+ "software_title_id": 1234
+ }
}
}
```
@@ -7495,14 +7591,14 @@ Creates a global query or team query.
| name | string | body | **Required**. The name of the query. |
| query | string | body | **Required**. The query in SQL syntax. |
| description | string | body | The query's description. |
-| observer_can_run | bool | 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). |
+| 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. |
+| 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. |
| 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 | bool | body | Whether to skip saving the latest query results for each host. Default: `false`. |
+| 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`. |
#### Example
@@ -7569,13 +7665,13 @@ Modifies the query specified by ID.
| 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 | bool | 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). |
+| 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. |
| 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 | bool | body | Whether to skip saving the latest query results for each host. |
+| discard_data | boolean | body | Whether to skip saving the latest query results for each host. |
> Note that any of the following conditions will cause the existing query report to be deleted:
> - Updating the `query` (SQL) field
@@ -7679,9 +7775,9 @@ Deletes the queries specified by ID. Returns the count of queries successfully d
#### Parameters
-| Name | Type | In | Description |
-| ---- | ---- | ---- | ------------------------------------- |
-| ids | list | body | **Required.** The IDs of the queries. |
+| Name | Type | In | Description |
+| ---- | ----- | ---- | ------------------------------------- |
+| ids | array | body | **Required.** The IDs of the queries. |
#### Example
@@ -8308,12 +8404,15 @@ Gets the result of a script that was executed.
"host_timeout": false,
"host_id": 1,
"execution_id": "e797d6c6-3aae-11ee-be56-0242ac120002",
- "runtime": 20
+ "runtime": 20,
+ "created_at": "2024-09-11T20:30:24Z"
}
```
> Note: `exit_code` can be `null` if Fleet hasn't heard back from the host yet.
+> Note: `created_at` is the creation timestamp of the script execution request.
+
### Add script
Uploads a script, making it available to run on hosts assigned to the specified team (or no team).
@@ -8538,176 +8637,29 @@ Deletes the session specified by ID. When the user associated with the session n
## Software
-- [Add software](#add-software)
-- [Download software](#download-software)
-- [Delete software](#delete-software)
-- [Get installation result](#get-installation-result)
- [List software](#list-software)
- [List software versions](#list-software-versions)
+- [List operating systems](#list-operating-systems)
- [Get software](#get-software)
- [Get software version](#get-software-version)
-
-### Add software
-
-_Available in Fleet Premium._
-
-Add a software package to install on macOS, Windows, and Linux (Ubuntu) hosts.
-
-
-`POST /api/v1/fleet/software/package`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| ---- | ------- | ---- | -------------------------------------------- |
-| software | file | form | **Required**. Installer package file. Supported packages are PKG, MSI, EXE, and DEB. |
-| team_id | integer | form | **Required**. The team ID. Adds a software package to the specified team. |
-| install_script | string | form | Command that Fleet runs to install software. If not specified Fleet runs [default install command](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
-| pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. |
-| post_install_script | string | form | 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. |
-| self_service | boolean | form | Self-service software is optional and can be installed by the end user. |
-
-#### Example
-
-`POST /api/v1/fleet/software/package`
-
-##### Request header
-
-```http
-Content-Length: 8500
-Content-Type: multipart/form-data; boundary=------------------------d8c247122f594ba0
-```
-
-##### Request body
-
-```http
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="team_id"
-1
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="self_service"
-true
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="install_script"
-sudo installer -pkg /temp/FalconSensor-6.44.pkg -target /
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="pre_install_query"
-SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="post_install_script"
-sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX
---------------------------d8c247122f594ba0
-Content-Disposition: form-data; name="software"; filename="FalconSensor-6.44.pkg"
-Content-Type: application/octet-stream
-
---------------------------d8c247122f594ba0
-```
-
-##### Default response
-
-`Status: 200`
-
-
-### Download software
-
-_Available in Fleet Premium._
-
-Download a software package.
-
-`GET /api/v1/fleet/software/titles/:software_title_id/package/?alt=media`
-
-#### Parameters
-
-| 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. |
-| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. |
-
-#### Example
-
-`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2`
-
-##### Default response
-
-`Status: 200`
-
-```http
-Status: 200
-Content-Type: application/octet-stream
-Content-Disposition: attachment
-Content-Length:
-Body:
-```
-
-### Delete software
-
-> This **endpoint, added in the app management feature, is experimental** may change. You can find the upcoming breaking changes [here](https://github.com/fleetdm/fleet/pull/19291/files#diff-7246bc304b15c8865ed8eaa205e9c244d0a0314e4bae60cf553dc06147c38b64L8661-R8698).
-
-_Available in Fleet Premium._
-
-Delete a software package.
-
-`DELETE /api/v1/fleet/software/titles/:software_title_id/package`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| ---- | ------- | ---- | -------------------------------------------- |
-| software_title_id | integer | path | **Required**. The ID of the software title for the software package to delete. |
-| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. |
-
-#### Example
-
-`DELETE /api/v1/fleet/software/titles/24/package?team_id=2`
-
-##### Default response
-
-`Status: 204`
-
-### Get installation results
-
-_Available in Fleet Premium._
-
-`GET /api/v1/fleet/software/install/results/:install_uuid`
-
-Get the results of a software installation.
-
-| Name | Type | In | Description |
-| ---- | ------- | ---- | -------------------------------------------- |
-| install_uuid | string | path | **Required**. The software installation UUID.|
-
-#### Example
-
-`GET /api/v1/fleet/software/install/results/b15ce221-e22e-4c6a-afe7-5b3400a017da`
-
-##### Default response
-
-`Status: 200`
-
-```json
- {
- "install_uuid": "b15ce221-e22e-4c6a-afe7-5b3400a017da",
- "software_title": "Falcon.app",
- "software_title_id": 8353,
- "software_package": "FalconSensor-6.44.pkg",
- "host_id": 123,
- "host_display_name": "Marko's MacBook Pro",
- "status": "failed",
- "output": "Installing software...\nError: The operation can’t be completed because the item “Falcon” is in use.",
- "pre_install_query_output": "Query returned result\nSuccess",
- "post_install_script_output": "Running script...\nExit code: 1 (Failed)\nRolling back software install...\nSuccess"
- }
-```
+- [Get operating system version](#get-operating-system-version)
+- [Add package](#add-package)
+- [List App Store apps](#list-app-store-apps)
+- [Add App Store app](#add-app-store-app)
+- [Add Fleet library app](#add-fleet-library-app)
+- [Install package or App Store app](#install-package-or-app-store-app)
+- [Get package install result](#get-package-install-result)
+- [Download package](#download-package)
+- [Delete package or App Store app](#delete-package-or-app-store-app)
### List software
-> The **new keys/values added in the app management feature are experimental** and may change. You can find the upcoming breaking changes [here](https://github.com/fleetdm/fleet/pull/19291/files#diff-7246bc304b15c8865ed8eaa205e9c244d0a0314e4bae60cf553dc06147c38b64L8749-R8791).
-
Get a list of all software.
`GET /api/v1/fleet/software/titles`
+> **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.
+
#### Parameters
| Name | Type | In | Description |
@@ -8717,10 +8669,13 @@ 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. |
-| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
-| available_for_install | bool | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. |
-| self_service | bool | query | If `true` or `1`, only lists self-service software. Default is `false`. |
+| 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". |
+| 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`. |
+| 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`. |
#### Example
@@ -8738,8 +8693,12 @@ Get a list of all software.
{
"id": 12,
"name": "Firefox.app",
- "software_package": "FirefoxInstall.pkg",
- "self_service": true,
+ "software_package": {
+ "name": "FirefoxInsall.pkg",
+ "version": "125.6",
+ "self_service": true
+ },
+ "app_store_app": null,
"versions_count": 3,
"source": "apps",
"browser": "",
@@ -8766,7 +8725,7 @@ Get a list of all software.
"id": 22,
"name": "Google Chrome.app",
"software_package": null,
- "self_service": false,
+ "app_store_app": null,
"versions_count": 5,
"source": "apps",
"browser": "",
@@ -8798,7 +8757,7 @@ Get a list of all software.
"id": 32,
"name": "1Password – Password Manager",
"software_package": null,
- "self_service": false,
+ "app_store_app": null,
"versions_count": 1,
"source": "chrome_extensions",
"browser": "chrome",
@@ -8834,8 +8793,11 @@ 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. |
-| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
+| 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". |
+| 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. |
+| 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`. |
#### Example
@@ -8893,8 +8855,82 @@ Get a list of all software versions.
}
```
+### List operating systems
+
+Returns a list of all operating systems.
+
+`GET /api/v1/fleet/os_versions`
+
+#### Parameters
+
+| 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". |
+| 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` |
+| 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: `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`. |
+
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "count": 1
+ "counts_updated_at": "2023-12-06T22:17:30Z",
+ "os_versions": [
+ {
+ "os_version_id": 123,
+ "hosts_count": 21,
+ "name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
+ "name_only": "Microsoft Windows 11 Pro 23H2",
+ "version": "10.0.22621.1234",
+ "platform": "windows",
+ "generated_cpes": [],
+ "vulnerabilities": [
+ {
+ "cve": "CVE-2022-30190",
+ "details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
+ "cvss_score": 7.8,// Available in Fleet Premium
+ "epss_probability": 0.9729,// Available in Fleet Premium
+ "cisa_known_exploit": false,// Available in Fleet Premium
+ "cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
+ "cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
+ "resolved_in_version": ""// Available in Fleet Premium
+ }
+ ]
+ }
+ ],
+ "meta": {
+ "has_next_results": false,
+ "has_previous_results": false
+ }
+}
+```
+
+OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
+
+```json
+{
+ "hosts_count": 1,
+ "name": "CentOS Linux 7.9.2009",
+ "name_only": "CentOS",
+ "version": "7.9.2009",
+ "platform": "rhel",
+ "generated_cpes": [],
+ "vulnerabilities": []
+}
+```
+
### Get software
+> **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 information about the specified software. By default, `versions` are sorted in descending order by the `hosts_count` field.
`GET /api/v1/fleet/software/titles/:id`
@@ -8904,7 +8940,7 @@ 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. |
+| 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". |
#### Example
@@ -8919,22 +8955,27 @@ Returns information about the specified software. By default, `versions` are sor
"software_title": {
"id": 12,
"name": "Firefox.app",
+ "bundle_identifier": "org.mozilla.firefox",
"software_package": {
"name": "FalconSensor-6.44.pkg",
"version": "6.44",
"installer_id": 23,
"team_id": 3,
"uploaded_at": "2024-04-01T14:22:58Z",
- "install_script": "sudo installer -pkg /temp/FalconSensor-6.44.pkg -target /",
+ "install_script": "sudo installer -pkg '$INSTALLER_PATH' -target /",
"pre_install_query": "SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';",
"post_install_script": "sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX",
+ "uninstall_script": "/Library/CS/falconctl uninstall",
"self_service": true,
"status": {
"installed": 3,
- "pending": 1,
- "failed": 2,
+ "pending_install": 1,
+ "failed_install": 0,
+ "pending_uninstall": 2,
+ "failed_uninstall": 1
}
},
+ "app_store_app": null,
"source": "apps",
"browser": "",
"hosts_count": 48,
@@ -8962,6 +9003,47 @@ Returns information about the specified software. By default, `versions` are sor
}
```
+#### Example (App Store app)
+
+`GET /api/v1/fleet/software/titles/15`
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "software_title": {
+ "id": 15,
+ "name": "Logic Pro",
+ "bundle_identifier": "com.apple.logic10",
+ "software_package": null,
+ "app_store_app": {
+ "name": "Logic Pro",
+ "app_store_id": "1091189122",
+ "latest_version": "2.04",
+ "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg",
+ "status": {
+ "installed": 3,
+ "pending": 1,
+ "failed": 2,
+ }
+ },
+ "source": "apps",
+ "browser": "",
+ "hosts_count": 48,
+ "versions": [
+ {
+ "id": 123,
+ "version": "2.04",
+ "vulnerabilities": [],
+ "hosts_count": 24
+ }
+ ]
+ }
+}
+```
+
### Get software version
Returns information about the specified software version.
@@ -8973,7 +9055,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. |
+| 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". |
#### Example
@@ -9019,8 +9101,441 @@ Returns information about the specified software version.
}
```
-## Vulnerabilities
+### Get operating system version
+Retrieves information about the specified operating system (OS) version.
+
+`GET /api/v1/fleet/os_versions/:id`
+
+#### Parameters
+
+| 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". |
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "counts_updated_at": "2023-12-06T22:17:30Z",
+ "os_version": {
+ "id": 123,
+ "hosts_count": 21,
+ "name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
+ "name_only": "Microsoft Windows 11 Pro 23H2",
+ "version": "10.0.22621.1234",
+ "platform": "windows",
+ "generated_cpes": [],
+ "vulnerabilities": [
+ {
+ "cve": "CVE-2022-30190",
+ "details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
+ "created_at": "2024-07-01T00:15:00Z",
+ "cvss_score": 7.8,// Available in Fleet Premium
+ "epss_probability": 0.9729,// Available in Fleet Premium
+ "cisa_known_exploit": false,// Available in Fleet Premium
+ "cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
+ "cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
+ "resolved_in_version": ""// Available in Fleet Premium
+ }
+ ]
+ }
+}
+```
+
+OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
+
+```json
+{
+ "id": 321,
+ "hosts_count": 1,
+ "name": "CentOS Linux 7.9.2009",
+ "name_only": "CentOS",
+ "version": "7.9.2009",
+ "platform": "rhel",
+ "generated_cpes": [],
+ "vulnerabilities": []
+}
+```
+
+### Add package
+
+> **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.
+
+_Available in Fleet Premium._
+
+Add a package (.pkg, .msi, .exe, .deb) to install on macOS, Windows, or Linux (Ubuntu) hosts.
+
+
+`POST /api/v1/fleet/software/package`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ------- | ---- | -------------------------------------------- |
+| software | file | form | **Required**. Installer package file. Supported packages are PKG, MSI, EXE, and DEB. |
+| team_id | integer | form | **Required**. The team ID. Adds a software package to the specified team. |
+| install_script | string | form | Script that Fleet runs to install software. If not specified Fleet runs [default install script](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
+| pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. |
+| post_install_script | string | form | 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. |
+| self_service | boolean | form | Self-service software is optional and can be installed by the end user. |
+
+#### Example
+
+`POST /api/v1/fleet/software/package`
+
+##### Request header
+
+```http
+Content-Length: 8500
+Content-Type: multipart/form-data; boundary=------------------------d8c247122f594ba0
+```
+
+##### Request body
+
+```http
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="team_id"
+1
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="self_service"
+true
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="install_script"
+sudo installer -pkg /temp/FalconSensor-6.44.pkg -target /
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="pre_install_query"
+SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="post_install_script"
+sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="software"; filename="FalconSensor-6.44.pkg"
+Content-Type: application/octet-stream
+
+--------------------------d8c247122f594ba0
+```
+
+##### Default response
+
+`Status: 200`
+
+### Modify package
+
+_Available in Fleet Premium._
+
+Update a package to install on macOS, Windows, or Linux (Ubuntu) hosts.
+
+`PATCH /api/v1/fleet/software/titles/:title_id/package`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ------- | ---- | -------------------------------------------- |
+| software | file | form | Installer package file. Supported packages are PKG, MSI, EXE, and DEB. |
+| team_id | integer | form | **Required**. The team ID. Updates a software package in the specified team. |
+| install_script | string | form | Command that Fleet runs to install software. If not specified Fleet runs the [default install command](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
+| pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, the package will not be installed. |
+| post_install_script | string | form | 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. |
+| self_service | boolean | form | Whether this is optional self-service software that can be installed by the end user. |
+
+> Changes to the installer package will reset installation counts. Changes to any field other than `self_service` will cancel pending installs for the old package.
+#### Example
+
+`PATCH /api/v1/fleet/software/titles/1/package`
+
+##### Request header
+
+```http
+Content-Length: 8500
+Content-Type: multipart/form-data; boundary=------------------------d8c247122f594ba0
+```
+
+##### Request body
+
+```http
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="team_id"
+1
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="self_service"
+true
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="install_script"
+sudo installer -pkg /temp/FalconSensor-6.44.pkg -target /
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="pre_install_query"
+SELECT 1 FROM macos_profiles WHERE uuid='c9f4f0d5-8426-4eb8-b61b-27c543c9d3db';
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="post_install_script"
+sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX
+--------------------------d8c247122f594ba0
+Content-Disposition: form-data; name="software"; filename="FalconSensor-6.44.pkg"
+Content-Type: application/octet-stream
+
+--------------------------d8c247122f594ba0
+```
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "software_package": {
+ "name": "FalconSensor-6.44.pkg",
+ "version": "6.44",
+ "installer_id": 23,
+ "team_id": 3,
+ "uploaded_at": "2024-04-01T14:22:58Z",
+ "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';",
+ "post_install_script": "sudo /Applications/Falcon.app/Contents/Resources/falconctl license 0123456789ABCDEFGHIJKLMNOPQRSTUV-WX",
+ "self_service": true,
+ "status": {
+ "installed": 0,
+ "pending": 0,
+ "failed": 0
+ }
+ }
+}
+```
+
+### List App Store apps
+
+> **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) that can be added to the specified team. If an app is already added to the team, it's excluded from the list.
+
+`GET /api/v1/fleet/software/app_store_apps`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ------- | ---- | -- | ----------- |
+| team_id | integer | query | **Required**. The team ID. |
+
+#### Example
+
+`GET /api/v1/fleet/software/app_store_apps/?team_id=3`
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "app_store_apps": [
+ {
+ "name": "Xcode",
+ "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg",
+ "latest_version": "15.4",
+ "app_store_id": "497799835",
+ "platform": "darwin"
+ },
+ {
+ "name": "Logic Pro",
+ "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg",
+ "latest_version": "2.04",
+ "app_store_id": "634148309",
+ "platform": "ios"
+ },
+ {
+ "name": "Logic Pro",
+ "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg",
+ "latest_version": "2.04",
+ "app_store_id": "634148309",
+ "platform": "ipados"
+ },
+ ]
+}
+```
+
+### Add App Store app
+
+> **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.
+
+_Available in Fleet Premium._
+
+Add App Store (VPP) app purchased in Apple Business Manager.
+
+`POST /api/v1/fleet/software/app_store_apps`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ---- | -- | ----------- |
+| app_store_id | string | body | **Required.** The ID of App Store app. |
+| team_id | integer | body | **Required**. The team ID. Adds VPP software to the specified team. |
+| platform | string | body | The platform of the app (`darwin`, `ios`, or `ipados`). Default is `darwin`. |
+
+#### Example
+
+`POST /api/v1/fleet/software/app_store_apps?team_id=3`
+
+##### Request body
+
+```json
+{
+ "app_store_id": "497799835",
+ "team_id": 2,
+ "platform": "ipados"
+}
+```
+
+##### Default response
+
+`Status: 200`
+
+### Download package
+
+> **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.
+
+_Available in Fleet Premium._
+
+`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media`
+
+#### Parameters
+
+| 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. |
+| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. |
+
+#### Example
+
+`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2`
+
+##### Default response
+
+`Status: 200`
+
+```http
+Status: 200
+Content-Type: application/octet-stream
+Content-Disposition: attachment
+Content-Length:
+Body:
+```
+
+### Install package or App Store app
+
+> **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.
+
+_Available in Fleet Premium._
+
+Install software (package or App Store app) on a macOS, iOS, iPadOS, Windows, or Linux (Ubuntu) host. Software title must have a `software_package` or `app_store_app` added to be installed.
+
+`POST /api/v1/fleet/hosts/:id/software/:software_title_id/install`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| --------- | ---------- | ---- | -------------------------------------------- |
+| id | integer | path | **Required**. The host's ID. |
+| software_title_id | integer | path | **Required**. The software title's ID. |
+
+#### Example
+
+`POST /api/v1/fleet/hosts/123/software/3435/install`
+
+##### Default response
+
+`Status: 202`
+
+### Uninstall package
+
+> **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.
+_Available in Fleet Premium._
+
+Uninstall software (package) on a macOS, Windows, or Linux (Ubuntu) host. Software title must have a `software_package` added to be uninstalled.
+
+`POST /api/v1/fleet/hosts/:id/software/:software_title_id/uninstall`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| --------- | ---------- | ---- | -------------------------------------------- |
+| id | integer | path | **Required**. The host's ID. |
+| software_title_id | integer | path | **Required**. The software title's ID. |
+
+#### Example
+
+`POST /api/v1/fleet/hosts/123/software/3435/uninstall`
+
+##### Default response
+
+`Status: 202`
+
+### Get package install result
+
+> **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.
+
+_Available in Fleet Premium._
+
+`GET /api/v1/fleet/software/install/:install_uuid/results`
+
+Get the results of a software package install.
+
+To get the results of an App Store app install, use the [List MDM commands](#list-mdm-commands) and [Get MDM command results](#get-mdm-command-results) API enpoints. Fleet uses an MDM command to install App Store apps.
+
+| Name | Type | In | Description |
+| ---- | ------- | ---- | -------------------------------------------- |
+| install_uuid | string | path | **Required**. The software installation UUID.|
+
+#### Example
+
+`GET /api/v1/fleet/software/install/b15ce221-e22e-4c6a-afe7-5b3400a017da/results`
+
+##### Default response
+
+`Status: 200`
+
+```json
+ {
+ "install_uuid": "b15ce221-e22e-4c6a-afe7-5b3400a017da",
+ "software_title": "Falcon.app",
+ "software_title_id": 8353,
+ "software_package": "FalconSensor-6.44.pkg",
+ "host_id": 123,
+ "host_display_name": "Marko's MacBook Pro",
+ "status": "failed",
+ "output": "Installing software...\nError: The operation can’t be completed because the item “Falcon” is in use.",
+ "pre_install_query_output": "Query returned result\nSuccess",
+ "post_install_script_output": "Running script...\nExit code: 1 (Failed)\nRolling back software install...\nSuccess"
+ }
+```
+
+### Delete package or App Store app
+
+> **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.
+
+_Available in Fleet Premium._
+
+Deletes software that's available for install (package or App Store app).
+
+`DELETE /api/v1/fleet/software/titles/:software_title_id/available_for_install`
+
+#### Parameters
+
+| 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. |
+
+#### Example
+
+`DELETE /api/v1/fleet/software/titles/24/available_for_install?team_id=2`
+
+##### Default response
+
+`Status: 204`
+
+## Vulnerabilities
- [List vulnerabilities](#list-vulnerabilities)
- [Get vulnerability](#get-vulnerability)
@@ -9035,7 +9550,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. |
+| 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". |
| 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). |
@@ -9044,7 +9559,6 @@ Retrieves a list of all CVEs affecting software and/or OS versions.
| exploit | boolean | query | _Available in Fleet Premium_. If `true`, filters to only include vulnerabilities that have been actively exploited in the wild (`cisa_known_exploit: true`). Otherwise, includes vulnerabilities with any `cisa_known_exploit` value. |
-
##### Default response
`Status: 200`
@@ -9079,12 +9593,14 @@ Retrieves a list of all CVEs affecting software and/or OS versions.
Retrieve details about a vulnerability and its affected software and OS versions.
+If no vulnerable OS versions or software were found, but Fleet is aware of the vulnerability, a 204 status code is returned.
+
#### Parameters
-| Name | Type | In | Description |
-| --- | --- | --- | --- |
-| cve | string | path | The cve to get information about (including "cve-" prefix, case-insensitive). |
-| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. |
+| 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". |
`GET /api/v1/fleet/vulnerabilities/:cve`
@@ -9115,7 +9631,7 @@ Retrieve details about a vulnerability and its affected software and OS versions
"name": "macOS 14.1.2",
"name_only": "macOS",
"version": "14.1.2",
- "platform": "darwin",
+
"resolved_in_version": "14.2",
"generated_cpes": [
"cpe:2.3:o:apple:macos:*:*:*:*:*:14.2:*:*",
@@ -9597,8 +10113,8 @@ _Available in Fleet Premium_
| ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | integer | path | **Required.** The desired team's ID. |
| name | string | body | The team's name. |
-| host_ids | list | body | A list of hosts that belong to the team. |
-| user_ids | list | body | A list of users on the team. |
+| 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 contains for the team. |
| failing_policies_webhook | object | body | Failing policies webhook settings. |
| enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. |
@@ -9623,14 +10139,20 @@ _Available in Fleet Premium_
| macos_updates | object | body | macOS updates settings. |
| minimum_version | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version. |
| deadline | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. |
+| ios_updates | object | body | iOS updates settings. |
+| minimum_version | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM will be nudged until their iOS is at or above this version. |
+| deadline | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. |
+| ipados_updates | object | body | iPadOS updates settings. |
+| minimum_version | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM will be nudged until their iPadOS is at or above this version. |
+| deadline | string | body | Hosts that belong to this team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past. |
| windows_updates | object | body | Windows updates settings. |
| deadline_days | integer | body | 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 | body | 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. |
| macos_settings | object | body | macOS-specific settings. |
-| custom_settings | list | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. |
+| custom_settings | array | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. |
| enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. |
| windows_settings | object | body | Windows-specific settings. |
-| custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. |
+| custom_settings | array | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. |
| macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. |
| enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). |
| integrations | object | body | Integration settings for this team. |
@@ -9850,8 +10372,8 @@ _Available in Fleet Premium_
| Name | Type | In | Description |
| --- | --- | --- | --- |
| id | integer | path | **Required.** The desired team's ID. |
-| force | bool | query | Force apply the options even if there are validation errors. |
-| dry_run | bool | query | Validate the options and return any validation errors, but do not apply the changes. |
+| 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. |
#### Example
@@ -9962,9 +10484,9 @@ Transforms a host name into a host id. For example, the Fleet UI use this endpoi
#### Parameters
-| Name | Type | In | Description |
-| ---- | ----- | ---- | ---------------------------------------- |
-| list | array | body | **Required** list of items to translate. |
+| Name | Type | In | Description |
+| ----- | ----- | ---- | ---------------------------------------- |
+| array | array | body | **Required** list of items to translate. |
#### Example
diff --git a/docs/Using Fleet/Learn-how-to-use-Fleet.md b/docs/Using Fleet/Learn-how-to-use-Fleet.md
deleted file mode 100644
index fceea3f7db..0000000000
--- a/docs/Using Fleet/Learn-how-to-use-Fleet.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Learn how to use Fleet
-
-- [How to add your device to Fleet](#how-to-add-your-device-to-fleet)
-- [How to ask questions about your device](#how-to-ask-questions-about-your-device)
-
-### Overview
-
-In this guide, we'll cover the following concepts:
-- How to add your device to Fleet
-- How to ask questions about your device
-
-### How to add your device to Fleet
-
-Once you log into Fleet, you are presented with the **Home** page.
-
-To add your device:
-
-1. Select **Add hosts**. In Fleet, devices are referred to as "hosts."
-2. Select your device's platform.
-3. Select **Download** to download Fleet's agent (fleetd). The download may take several seconds.
-4. Open fleetd and follow the installation steps.
-
-> It may take several seconds for Fleet osquery to send your device's data to Fleet.
-
-In the background, Fleet ran several checks to assess the security hygiene of your device.
-
-> In Fleet, these checks are referred to as "policies."
-
-### How to ask questions about your device
-
-With Fleet, you can ask a multitude of questions to help you manage, monitor, and identify threats on your devices, but if you are just starting out, and unsure of what to ask, Fleet comes baked in with a [query library](https://fleetdm.com/queries) of common questions.
-
-So, let's start by asking the following question about your device:
-
-* What operating system is installed on my device and what is its version?
-
-This question can easily be answered by running this simple query: "Get operating system information."
-
-To run this query on your device:
-
-1. Select **Queries** in the top navigation.
-2. Select **Create new query** (or browse your organization's queries for "operating system information" in the search bar).
-3. Type the query you would like to run, `SELECT * FROM os_version;`.
-4. Select **Run query**, then select **All hosts** (your device may be the only host added to Fleet), and finally select **Run** to execute the query.
-
-The query may take several seconds to complete, because Fleet has to wait for the Fleet's agent (fleetd) to respond with results. Only online hosts will respond with results to a live query.
-
-> Fleet's query response time is inherently variable because of osquery's heartbeat response time. This helps prevent performance issues on hosts.
-
-When the query has finished, you should see several columns in the "Results" table:
-
-- The "name" column answers: "What operating system is installed on my device?"
-
-- The "version" column answers: "What version of the installed operating system is on my device?"
-
-
-
-
\ No newline at end of file
diff --git a/docs/Using Fleet/MDM-migration-guide.md b/docs/Using Fleet/MDM-migration-guide.md
deleted file mode 100644
index 68e55a7c81..0000000000
--- a/docs/Using Fleet/MDM-migration-guide.md
+++ /dev/null
@@ -1,214 +0,0 @@
-# Migration guide
-
-This section provides instructions for migrating your hosts away from your old MDM solution to Fleet.
-
-## Requirements
-
-1. A [deployed Fleet instance](../Deploying/Introduction.md)
-2. [Fleet connected to Apple](./mdm-setup.md)
-
-## Migrate manually enrolled hosts
-
-1. [Enroll](./Adding-hosts.md) your hosts to Fleet with [Fleetd and Fleet Desktop](https://fleetdm.com/docs/using-fleet/adding-hosts#including-fleet-desktop)
-2. Ensure your end users have access to an admin account on their Mac. End users won't be able to migrate on their own if they have a standard account.
-3. In your old MDM solution, unenroll the hosts to be migrated. MacOS does not allow multiple MDMs to be installed at once.
-4. Send [these guided instructions](#how-to-turn-on-mdm) to your end users to complete the final few steps via Fleet Desktop.
- * Note that there will be a gap in MDM coverage between when the host is unenrolled from the old MDM and when the host turns on MDM in Fleet.
-
-### End user experience
-
-1. On their **My device** page, once an end user's device is unenrolled from the old MDM solution, the end user will be given the option to manually download the MDM enrollment profile.
-
-2. Once downloaded, the user will receive a system notification that the Device Enrollment profile needs to be installed in their **System Settings > Profiles** section.
-
-3. After installation, the MDM enrollment profile can be removed by the end user at any time.
-
-### How to turn on MDM
-
-1. Select the Fleet icon in your menu bar and select **My device**.
-
-
-
-2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
- - If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
- - If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
-
-
-
-## Migrate automatically enrolled (DEP) hosts
-
-> Automatic enrollment is available in Fleet Premium or Ultimate
-
-To migrate automatically enrolled hosts, we will do the following steps:
-
-1. Prepare to migrate hosts
-2. Choose migration workflow and migrate hosts
-
-### Step 1: prepare to migrate hosts
-
-1. Connect Fleet to Apple Business Manager (ABM). Learn how [here](./mdm-setup.md#apple-business-manager-abm).
-2. [Enroll](./Adding-hosts.md) your hosts to Fleet with [Fleetd and Fleet Desktop](https://fleetdm.com/docs/using-fleet/adding-hosts#including-fleet-desktop)
-3. Ensure your end users have access to an admin account on their Mac. End users won't be able to migrate on their own if they have a standard account.
-4. Migrate your hosts to Fleet in ABM:
- 1. In ABM, unassign the existing hosts' MDM server from the old MDM solution: In ABM, select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Unassign from the current MDM**, and select **Continue**.
- 2. In ABM, assign these hosts' MDM server to Fleet: In ABM, select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Assign to the following MDM:**, select your Fleet server in the dropdown, and select **Continue**.
-
-### Step 2: choose migration workflow and migrate hosts
-
-There are two migration workflows in Fleet: default and end user.
-
-The default migration workflow requires that the IT admin unenrolls hosts from the old MDM solution before the end user can complete migration. This will result in a gap in MDM coverage until the end user completes migration.
-
-The end user migration workflow allows the end user to kick-off migration by unenrolling from the old MDM solution on their own. Once the user is unenrolled, they're prompted to turn on MDM features in Fleet. This reduces the gap in MDM coverage.
-
-Configuring the end user migration workflow requires a few additional steps.
-
-#### Default workflow
-
-1. In your old MDM solution, unenroll the hosts to be migrated. MacOS does not allow multiple MDMs to be installed at once.
-
-2. Send [these guided instructions](#how-to-turn-on-mdm-default) to your end users to complete the final few steps via Fleet Desktop.
- * Note that there will be a gap in MDM coverage between when the host is unenrolled from the old MDM and when the host turns on MDM in Fleet.
-
-##### End user experience
-
-1. The end user will receive a "Device Enrollment: <organization> can automatically configure your Mac." system notification within the macOS Notifications Center.
-
-2. After the end user clicks on the system notification, macOS will open the **System Setting > Profiles** and ask the user to "Allow Device Enrollment: <organization> can automatically configure your Mac based on settings provided by your System Administrator."
-
-3. If the end user does not install the profile, the system notification will continue to prompt the end user until the setting has been allowed.
-
-4. Once this setting has been approved, the MDM enrollment profile cannot be removed by the end user.
-
-##### How to turn on MDM (default)
-
-1. Select the Fleet icon in your menu bar and select **My device**.
-
-
-
-2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
- * If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
- * If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
-
-
-
-#### End user workflow
-
-> Available in Fleet Premium or Ultimate
-
-The end user migration workflow is supported for automatically enrolled (DEP) hosts.
-
-To watch a GIF that walks through the end user experience during the migration workflow, in the Fleet UI, head to **Settings > Integrations > Mobile device management (MDM)**, and scroll down to the **End user migration workflow** section.
-
-In Fleet, you can configure the end user workflow using the Fleet UI or fleetctl command-line tool.
-
-Fleet UI:
-
-1. Select the avatar on the right side of the top navigation and select **Settings > Integrations > Mobile device management (MDM)**.
-
-2. Scroll down to the **End user migration workflow** section and select the toggle to enable the workflow.
-
-3. Under **Mode** choose a mode and enter the webhook URL for you automation tool (ex. Tines) under **Webhook URL** and select **Save**.
-
-4. During the end user migration workflow, an end user's device will have their selected system theme (light or dark) applied. If your logo is not easy to see on both light and dark backgrounds, you can optionally set a logo for each theme:
-Head to **Settings** > **Organization settings** >
-**Organization info**, add URLs to your logos in the **Organization avatar URL (for dark backgrounds)** and **Organization avatar URL (for light backgrounds)** fields, and select **Save**.
-
-fleetctl CLI:
-
-1. Create `fleet-config.yaml` file or add to your existing `config` YAML file:
-
-```yaml
-apiVersion: v1
-kind: config
-spec:
- mdm:
- macos_migration:
- enable: true
- mode: "voluntary"
- webhook_url: "https://example.com"
- ...
-```
-
-2. Fill in the above keys under the `mdm.macos_migration` key.
-
-To learn about each option, in the Fleet UI, select the avatar on the right side of the top navigation, select **Settings > Integrations > Mobile device management (MDM)**, and scroll down to the **End user migration workflow** section.
-
-3. During the end user migration workflow, the window will show the Fleet logo on top of a dark and light background (appearance configured by end user).
-
-If want to add a your organization's logo, you can optionally set a logo for each background:
-
-```yaml
-apiVersion: v1
-kind: config
-spec:
- org_info:
- org_logo_url: https://fleetdm.com/images/press-kit/fleet-blue-logo.png
- org_logo_url_light_background: https://fleetdm.com/images/press-kit/fleet-white-logo.png
- ...
-```
-
-Add URLs to your logos that are visible on a dark background and light background in the `org_logo_url` and `org_logo_url_light_background` keys respectively. If you only set a logo for one, the Fleet logo will be used for the other.
-
-4. Run the fleetctl `apply -f fleet-config.yml` command to add your configuration.
-
-5. Confirm that your configuration was saved by running `fleetctl get config`.
-
-6. Send [these guided instructions](#how-to-turn-on-mdm-end-user) to your end users to complete the final few steps via Fleet Desktop.
-
-##### How to turn on MDM (end user)
-
-1. Select the Fleet icon in your menu bar and select **Migrate to Fleet**.
-
-2. Select **Start** in the **Migrate to Fleet** popup.
-
-2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
- * If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
- * If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
-
-## Check migration progress
-
-To see a report of which hosts have successfully migrated to Fleet, have MDM features off, or are still enrolled to your old MDM solution head to the **Dashboard** page by clicking the icon on the left side of the top navigation bar.
-
-Then, scroll down to the **Mobile device management (MDM)** section.
-
-## FileVault recovery keys
-
-_Available in Fleet Premium_
-
-When migrating from a previous MDM, end users need to take action to escrow FileVault keys to Fleet. The **My device** page in Fleet Desktop will present users with instructions to reset their key.
-
-To start, enforce FileVault (disk encryption) and escrow in Fleet. Learn how [here](./MDM-disk-encryption.md).
-
-After turning on disk encryption in Fleet, share [these guided instructions](#how-to-turn-on-disk-encryption) with your end users.
-
-If your old MDM solution did not enforce disk encryption, the end user will need to restart or log out of the host.
-
-If your old MDM solution did enforce disk encryption, the end user will need to reset their disk encryption key by following the prompt on the My device page and inputting their password.
-
-## Activation Lock Bypass codes
-
-In Fleet, the [Activation Lock](https://support.apple.com/en-us/HT208987) feature is disabled by default for automatically enrolled (DEP) hosts.
-
-If a host under the old MDM solution has Activation Lock enabled, we recommend asking the end user to follow these instructions to disable Activation Lock before migrating this host to Fleet: https://support.apple.com/en-us/HT208987.
-
-This is because if the Activation Lock is enabled, you will need the Activation Lock bypass code to successfully wipe and reuse the Mac.
-
-However, Activation Lock bypass codes can only be retrieved from the Mac up to 30 days after the device is enrolled. This means that when migrating from your old MDM solution, it’s likely that you’ll be unable to retrieve the Activation Lock bypass code.
-
-### How to turn on disk encryption
-
-1. Select the Fleet icon in your menu bar and select **My device**.
-
-
-
-2. On your **My device** page, follow the disk encryption instructions in the yellow banner.
- - If you don’t see the yellow banner, select the purple **Refetch** button at the top of the page.
- - If you still don't see the yellow banner after a couple minutes or if the **My device** page presents you with an error, please contact your IT administrator.
-
-
-
-
-
-
-
diff --git a/docs/Using Fleet/MDM-setup.md b/docs/Using Fleet/MDM-setup.md
deleted file mode 100644
index c56ddeb5d6..0000000000
--- a/docs/Using Fleet/MDM-setup.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Setup
-
-To turn on macOS, iOS, and iPadOS MDM features, follow the instructions on this page to connect Fleet to Apple Push Notification service (APNs).
-
-To use automatic enrollment (aka zero-touch) features on macOS, iOS, and iPadOS, follow instructions to connect Fleet with Apple Business Manager (ABM).
-
-To turn on Windows MDM features, head to this [Windows MDM setup article](https://fleetdm.com/guides/windows-mdm-setup).
-
-## Apple Push Notification service (APNs)
-
-Apple uses APNs to authenticate and manage interactions between Fleet and hosts.
-
-To connect Fleet to APNs or renew APNs, head to the **Settings > Integrations > Mobile device management (MDM)** page.
-
-> Apple requires that APNs certificates are renewed annually.
-> - If your certificate expires, you will have to turn MDM off and back on for all macOS hosts.
-> - Be sure to use the same Apple ID from year-to-year. If you don't, you will have to turn MDM off and back on for all macOS hosts.
-
-## Apple Business Manager (ABM)
-
-> Available in Fleet Premium
-
-To connect Fleet to ABM or renew ABM, head to the **Settings > Integrations > Automatic enrollment > Apple Business Manager** page.
-
-After connecting Fleet to ABM, set Fleet to be the MDM for all Macs:
-
-1. Log in to [Apple Business Manager](https://business.apple.com)
-2. Click your profile icon in the bottom left
-3. Click **Preferences**
-4. Click **MDM Server Assignment** and click **Edit** next to **Default Server Assignment**.
-5. Switch **Mac**, **iPhone**, and **iPad** to Fleet.
-
-New or wiped macOS, iOS, and iPadOS hosts that are in ABM, before they've been set up, appear in Fleet with **MDM status** set to "Pending".
-
-All macOS hosts that automatically enroll will be assigned to the default team. If no default team is set, then the host will be placed in "No team".
-
-> A host can be transferred to a new (not default) team before it enrolls. In the Fleet UI, you can do this under **Settings** > **Teams**.
-
-
-
-
-
diff --git a/docs/Using Fleet/Supported-browsers.md b/docs/Using Fleet/Supported-browsers.md
deleted file mode 100644
index 0ed5a0dc0e..0000000000
--- a/docs/Using Fleet/Supported-browsers.md
+++ /dev/null
@@ -1,28 +0,0 @@
-
-# Supported browsers
-
-Fleet supports the latest, stable releases of all major browsers and platforms.
-
-We test each browser on Windows whenever possible, because our engineering team primarily uses macOS.
-
-**Note:** This information also applies to [fleetdm.com](https://www.fleetdm.com).
-
-### Desktop
-
-- Chrome
-- Firefox
-- Edge
-- Safari (macOS only)
-
-### Mobile
-
-- Mobile Safari on iOS
-- Mobile Chrome on Android
-
-### Note
-> - Mobile web is not yet supported in the Fleet product.
-> - The Fleet user interface [may not be fully supported](https://github.com/fleetdm/fleet/issues/969) in Google Chrome when the browser is running on ChromeOS
-
-
-
-
diff --git a/docs/Using Fleet/Supported-host-operating-systems.md b/docs/Using Fleet/Supported-host-operating-systems.md
deleted file mode 100644
index ff8cd5480c..0000000000
--- a/docs/Using Fleet/Supported-host-operating-systems.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Supported host operating systems
-
-Fleet supports the following operating system versions on hosts.
-
-| OS | Supported version(s) |
-| :------ | :------------------------------------- |
-| macOS | 13+ (Ventura) |
-| Windows | Pro and Enterprise 10+, Server 2012+ |
-| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ |
-| ChromeOS | 112.0.5615.134+ |
-
-While Fleet may still function partially or fully with OS versions older than those above, Fleet does not actively test against unsupported versions and does not pursue bugs on them.
-
-## Some notes on compatibility
-
-### Tables
-Not all osquery tables are available for every OS. Please check out the [osquery schema](https://fleetdm.com/tables) for detailed information.
-
-If a table is not available for your host, Fleet will generally handle things behind the scenes for you.
-
-### M1 Macs
-Fleet's agent (fleetd) generated for MacOS by `fleetctl package` does not include native support for M1 Macs. Some values returned may reflect the information returned by Rosetta rather than the system. For example, a CPU will show up as `i486`.
-
-### Linux
-
-> Ubuntu Linux:
-> Fleet Desktop currently supports Xorg as X11 server, Wayland is currently not supported.
-> Ubuntu 24.04 comes with Wayland enabled by default. To use X11 instead of Wayland you can set
-> `WaylandEnable=false` in `/etc/gdm3/custom.conf` and reboot.
-
-> Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome for Fleet Desktop.
-
-> The `fleetctl package` command is not supported on DISA-STIG distribution.
-
-
-
-
diff --git a/docs/files/2024-06-14-fleet-penetration-test.pdf b/docs/files/2024-06-14-fleet-penetration-test.pdf
new file mode 100644
index 0000000000..1be4c46541
Binary files /dev/null and b/docs/files/2024-06-14-fleet-penetration-test.pdf differ
diff --git a/ee/bulk-operations-dashboard/.editorconfig b/ee/bulk-operations-dashboard/.editorconfig
new file mode 100644
index 0000000000..6d7fa7039c
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.editorconfig
@@ -0,0 +1,31 @@
+################################################
+# ╔═╗╔╦╗╦╔╦╗╔═╗╦═╗┌─┐┌─┐┌┐┌┌─┐┬┌─┐
+# ║╣ ║║║ ║ ║ ║╠╦╝│ │ ││││├┤ ││ ┬
+# o╚═╝═╩╝╩ ╩ ╚═╝╩╚═└─┘└─┘┘└┘└ ┴└─┘
+#
+# > Formatting conventions for your Sails app.
+#
+# This file (`.editorconfig`) exists to help
+# maintain consistent formatting throughout the
+# files in your Sails app.
+#
+# For the sake of convention, the Sails team's
+# preferred settings are included here out of the
+# box. You can also change this file to fit your
+# team's preferences (for example, if all of the
+# developers on your team have a strong preference
+# for tabs over spaces),
+#
+# To review what each of these options mean, see:
+# http://editorconfig.org/
+#
+################################################
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/ee/bulk-operations-dashboard/.eslintignore b/ee/bulk-operations-dashboard/.eslintignore
new file mode 100644
index 0000000000..f190c2ae4f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.eslintignore
@@ -0,0 +1,3 @@
+assets/dependencies/**/*.js
+views/**/*.ejs
+
diff --git a/ee/bulk-operations-dashboard/.eslintrc b/ee/bulk-operations-dashboard/.eslintrc
new file mode 100644
index 0000000000..37e02b3724
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.eslintrc
@@ -0,0 +1,92 @@
+{
+ // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐
+ // ║╣ ╚═╗║ ║║║║ ║ ├┬┘│
+ // o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘
+ // A set of basic code conventions designed to encourage quality and consistency
+ // across your Sails app's code base. These rules are checked against
+ // automatically any time you run `npm test`.
+ //
+ // > An additional eslintrc override file is included in the `assets/` folder
+ // > right out of the box. This is specifically to allow for variations in acceptable
+ // > global variables between front-end JavaScript code designed to run in the browser
+ // > vs. backend code designed to run in a Node.js/Sails process.
+ //
+ // > Note: If you're using mocha, you'll want to add an extra override file to your
+ // > `test/` folder so that eslint will tolerate mocha-specific globals like `before`
+ // > and `describe`.
+ // Designed for ESLint v4.
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ // For more information about any of the rules below, check out the relevant
+ // reference page on eslint.org. For example, to get details on "no-sequences",
+ // you would visit `http://eslint.org/docs/rules/no-sequences`. If you're unsure
+ // or could use some advice, come by https://sailsjs.com/support.
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ "env": {
+ "node": true
+ },
+
+ "parserOptions": {
+ "ecmaVersion": 2018
+ },
+
+ "globals": {
+ // If "no-undef" is enabled below, be sure to list all global variables that
+ // are used in this app's backend code (including the globalIds of models):
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ "Promise": true,
+ "sails": true,
+ "_": true,
+
+ // Models:
+ "User": true,
+ "UndeployedProfile": true,
+ "UndeployedScript": true
+
+ // …and any others.
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ },
+
+ "rules": {
+ "block-scoped-var": ["error"],
+ "callback-return": ["error", ["done", "proceed", "next", "onwards", "callback", "cb"]],
+ "camelcase": ["warn", {"properties":"always"}],
+ "comma-style": ["warn", "last"],
+ "curly": ["warn"],
+ "eqeqeq": ["error", "always"],
+ "eol-last": ["warn"],
+ "handle-callback-err": ["error"],
+ "indent": ["warn", 2, {
+ "SwitchCase": 1,
+ "MemberExpression": "off",
+ "FunctionDeclaration": {"body":1, "parameters":"off"},
+ "FunctionExpression": {"body":1, "parameters":"off"},
+ "CallExpression": {"arguments":"off"},
+ "ArrayExpression": 1,
+ "ObjectExpression": 1,
+ "ignoredNodes": ["ConditionalExpression"]
+ }],
+ "linebreak-style": ["error", "unix"],
+ "no-dupe-keys": ["error"],
+ "no-duplicate-case": ["error"],
+ "no-extra-semi": ["warn"],
+ "no-labels": ["error"],
+ "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
+ "no-redeclare": ["warn"],
+ "no-return-assign": ["error", "always"],
+ "no-sequences": ["error"],
+ "no-trailing-spaces": ["warn"],
+ "no-undef": ["error"],
+ "no-unexpected-multiline": ["warn"],
+ "no-unreachable": ["warn"],
+ "no-unused-vars": ["warn", {"caughtErrors":"all", "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)", "argsIgnorePattern": "^unused($|[A-Z].*$)", "varsIgnorePattern": "^unused($|[A-Z].*$)" }],
+ "no-use-before-define": ["error", {"functions":false}],
+ "one-var": ["warn", "never"],
+ "prefer-arrow-callback": ["warn", {"allowNamedFunctions":true}],
+ "quotes": ["warn", "single", {"avoidEscape":false, "allowTemplateLiterals":true}],
+ "semi": ["warn", "always"],
+ "semi-spacing": ["warn", {"before":false, "after":true}],
+ "semi-style": ["warn", "last"]
+ }
+
+}
diff --git a/ee/bulk-operations-dashboard/.gitignore b/ee/bulk-operations-dashboard/.gitignore
new file mode 100644
index 0000000000..a1ddb80e79
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.gitignore
@@ -0,0 +1,134 @@
+################################################
+# ┌─┐┬┌┬┐╦╔═╗╔╗╔╔═╗╦═╗╔═╗
+# │ ┬│ │ ║║ ╦║║║║ ║╠╦╝║╣
+# o└─┘┴ ┴ ╩╚═╝╝╚╝╚═╝╩╚═╚═╝
+#
+# > Files to exclude from your app's repo.
+#
+# This file (`.gitignore`) is only relevant if
+# you are using git.
+#
+# It exists to signify to git that certain files
+# and/or directories should be ignored for the
+# purposes of version control.
+#
+# This keeps tmp files and sensitive credentials
+# from being uploaded to your repository. And
+# it allows you to configure your app for your
+# machine without accidentally committing settings
+# which will smash the local settings of other
+# developers on your team.
+#
+# Some reasonable defaults are included below,
+# but, of course, you should modify/extend/prune
+# to fit your needs!
+#
+################################################
+
+
+################################################
+# Local Configuration
+#
+# Explicitly ignore files which contain:
+#
+# 1. Sensitive information you'd rather not push to
+# your git repository.
+# e.g., your personal API keys or passwords.
+#
+# 2. Developer-specific configuration
+# Basically, anything that would be annoying
+# to have to change every time you do a
+# `git pull` on your laptop.
+# e.g. your local development database, or
+# the S3 bucket you're using for file uploads
+# during development.
+#
+################################################
+
+config/local.js
+
+
+################################################
+# Dependencies
+#
+#
+# When releasing a production app, you _could_
+# hypothetically include your node_modules folder
+# in your git repo, but during development, it
+# is always best to exclude it, since different
+# developers may be working on different kernels,
+# where dependencies would need to be recompiled
+# anyway.
+#
+# Most of the time, the node_modules folder can
+# be excluded from your code repository, even
+# in production, thanks to features like the
+# package-lock.json file / NPM shrinkwrap.
+#
+# But no matter what, since this is a Sails app,
+# you should always push up the package-lock.json
+# or shrinkwrap file to your repository, to avoid
+# accidentally pulling in upgraded dependencies
+# and breaking your code.
+#
+# That said, if you are having trouble with
+# dependencies, (particularly when using
+# `npm link`) this can be pretty discouraging.
+# But rather than just adding the lockfile to
+# your .gitignore, try this first:
+# ```
+# rm -rf node_modules
+# rm package-lock.json
+# npm install
+# ```
+#
+# [?] For more tips/advice, come by and say hi
+# over at https://sailsjs.com/support
+#
+################################################
+
+node_modules
+
+
+################################################
+#
+# > Do you use bower?
+# > re: the bower_components dir, see this:
+# > http://addyosmani.com/blog/checking-in-front-end-dependencies/
+# > (credit Addy Osmani, @addyosmani)
+#
+################################################
+
+
+################################################
+# Temporary files generated by Sails/Waterline.
+################################################
+
+.tmp
+
+
+################################################
+# Miscellaneous
+#
+# Common files generated by text editors,
+# operating systems, file systems, dbs, etc.
+################################################
+
+*~
+*#
+.DS_STORE
+.netbeans
+nbproject
+.idea
+*.iml
+.vscode
+.node_history
+dump.rdb
+
+npm-debug.log
+lib-cov
+*.seed
+*.log
+*.out
+*.pid
+
diff --git a/ee/bulk-operations-dashboard/.htmlhintrc b/ee/bulk-operations-dashboard/.htmlhintrc
new file mode 100644
index 0000000000..c9b2ee72b6
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.htmlhintrc
@@ -0,0 +1,27 @@
+{
+ "alt-require": true,
+ "attr-lowercase": ["viewBox"],
+ "attr-no-duplication": true,
+ "attr-unsafe-chars": true,
+ "attr-value-double-quotes": true,
+ "attr-value-not-empty": false,
+ "csslint": false,
+ "doctype-first": false,
+ "doctype-html5": true,
+ "head-script-disabled": false,
+ "href-abs-or-rel": false,
+ "id-class-ad-disabled": true,
+ "id-class-value": false,
+ "id-unique": true,
+ "inline-script-disabled": true,
+ "inline-style-disabled": false,
+ "jshint": false,
+ "space-tab-mixed-disabled": "space",
+ "spec-char-escape": false,
+ "src-not-empty": true,
+ "style-disabled": false,
+ "tag-pair": true,
+ "tag-self-close": false,
+ "tagname-lowercase": true,
+ "title-require": false
+}
diff --git a/ee/bulk-operations-dashboard/.lesshintrc b/ee/bulk-operations-dashboard/.lesshintrc
new file mode 100644
index 0000000000..6dcb60f278
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.lesshintrc
@@ -0,0 +1,46 @@
+{
+ // ╦ ╔═╗╔═╗╔═╗╦ ╦╦╔╗╔╔╦╗┬─┐┌─┐
+ // ║ ║╣ ╚═╗╚═╗╠═╣║║║║ ║ ├┬┘│
+ // o╩═╝╚═╝╚═╝╚═╝╩ ╩╩╝╚╝ ╩ ┴└─└─┘
+ // Configuration designed for the lesshint linter. Describes a loose set of LESS
+ // conventions that help avoid typos, unexpected failed builds, and hard-to-debug
+ // selector and CSS rule issues.
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ // For more information about any of the rules below, check out the reference page
+ // of all rules at https://github.com/lesshint/lesshint/blob/v6.3.6/lib/linters/README.md
+ // If you're unsure or could use some advice, come by https://sailsjs.com/support.
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ "singleLinePerSelector": false,
+ "singleLinePerProperty": false,
+ "zeroUnit": false,
+ "idSelector": false,
+ "propertyOrdering": false,
+ "spaceAroundBang": false,
+ "fileExtensions": [".less", ".css"],
+ "excludedFiles": ["vendor.less"],
+ "importPath": false,
+ "borderZero": false,
+ "hexLength": false,
+ "hexNotation": false,
+ "newlineAfterBlock": false,
+ "spaceBeforeBrace": {
+ "style": "one_space"
+ },
+ "spaceAfterPropertyName": false,
+ "spaceAfterPropertyColon": {
+ "enabled": true,
+ "style": "one_space"
+ },
+ "maxCharPerLine": false,
+ "emptyRule": false,
+ "importantRule": true,
+ "qualifyingElement": false
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ // ^^ This last one is only disabled because the lesshint parser seems to have
+ // a hard time distinguishing between things like `div.bar` and `&.bar`.
+ // In this case, the ampersand has a distinct meaning, and it does not refer
+ // to an element. (It's referring to the case where that class is matched at
+ // the parent level, rather than talking about a descendant.)
+ // https://github.com/lesshint/lesshint/blob/v6.3.6/lib/linters/README.md#qualifyingelement
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+}
diff --git a/ee/bulk-operations-dashboard/.npmrc b/ee/bulk-operations-dashboard/.npmrc
new file mode 100644
index 0000000000..43601ce8db
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.npmrc
@@ -0,0 +1,11 @@
+######################
+# ╔╗╔╔═╗╔╦╗┬─┐┌─┐ #
+# ║║║╠═╝║║║├┬┘│ #
+# o╝╚╝╩ ╩ ╩┴└─└─┘ #
+######################
+
+# Hide NPM log output unless it is related to an error of some kind:
+loglevel=error
+
+# Make "npm audit" an opt-in thing for subsequent installs within this app:
+audit=false
diff --git a/ee/bulk-operations-dashboard/.sailsrc b/ee/bulk-operations-dashboard/.sailsrc
new file mode 100644
index 0000000000..782d995af6
--- /dev/null
+++ b/ee/bulk-operations-dashboard/.sailsrc
@@ -0,0 +1,12 @@
+{
+ "hooks": {
+ "sockets": false
+ },
+ "generators": {
+ "modules": {}
+ },
+ "_generatedWith": {
+ "sails": "1.5.11",
+ "sails-generate": "2.0.11"
+ }
+}
diff --git a/ee/bulk-operations-dashboard/Dockerfile b/ee/bulk-operations-dashboard/Dockerfile
new file mode 100644
index 0000000000..c68b6f335a
--- /dev/null
+++ b/ee/bulk-operations-dashboard/Dockerfile
@@ -0,0 +1,26 @@
+# Use the official Node.js 14 image as a base
+FROM node:20@sha256:e06aae17c40c7a6b5296ca6f942a02e6737ae61bbbf3e2158624bb0f887991b5
+
+# Set the working directory in the container
+WORKDIR /usr/src/app
+
+# Copy the package.json
+COPY package.json ./
+
+# Install vulnerability dashboard dependencies
+RUN npm install
+
+# Copy the vulnerability dashboard into the container
+COPY . .
+
+# Copy the entrypoint script into the container
+COPY entrypoint.sh /usr/src/app/entrypoint.sh
+
+# Make sure the entrypoint script is executable
+RUN chmod +x /usr/src/app/entrypoint.sh
+
+# Expose the port the vulnerability dashboard runs on
+EXPOSE 1337
+
+# Set the entrypoint script as the entry point
+ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
diff --git a/ee/bulk-operations-dashboard/Gruntfile.js b/ee/bulk-operations-dashboard/Gruntfile.js
new file mode 100644
index 0000000000..e3b2847338
--- /dev/null
+++ b/ee/bulk-operations-dashboard/Gruntfile.js
@@ -0,0 +1,23 @@
+/**
+ * Gruntfile
+ *
+ * This Node script is executed when you run `grunt`-- and also when
+ * you run `sails lift` (provided the grunt hook is installed and
+ * hasn't been disabled).
+ *
+ * WARNING:
+ * Unless you know what you're doing, you shouldn't change this file.
+ * Check out the `tasks/` directory instead.
+ *
+ * For more information see:
+ * https://sailsjs.com/anatomy/Gruntfile.js
+ */
+module.exports = function(grunt) {
+
+ var loadGruntTasks = require('sails-hook-grunt/accessible/load-grunt-tasks');
+
+ // Load Grunt task configurations (from `tasks/config/`) and Grunt
+ // task registrations (from `tasks/register/`).
+ loadGruntTasks(__dirname, grunt);
+
+};
diff --git a/ee/bulk-operations-dashboard/README.md b/ee/bulk-operations-dashboard/README.md
new file mode 100644
index 0000000000..678396b623
--- /dev/null
+++ b/ee/bulk-operations-dashboard/README.md
@@ -0,0 +1,49 @@
+# Bulk operations dashboard
+
+
+A dashboard to easily manage profiles and scripts across multiple teams on a Fleet instance.
+
+
+## Dependencies
+
+- A datastore, this app was built using Postgres, but you can use a database of your choice.
+
+- A Redis database - For session storage.
+
+
+## Configuration
+
+This app has two required custom configuration values:
+
+- `sails.config.custom.fleetBaseUrl`: The full URL of your Fleet instance. (e.g., https://fleet.example.com)
+
+- `sails.config.custom.fleetApiToken`: An API token for an API-only user on your Fleet instance.
+
+
+
+## Running the bulk operations dashboard with Docker.
+
+To run a local bulk operations dashboard with docker, you can follow these instructions.
+
+1. Clone this repo
+2. Update the following ENV variables `ee/bulk-operations-dashboard/docker-compose.yml` file:
+
+ 1. `sails_custom__fleetBaseUrl`: The full URL of your Fleet instance. (e.g., https://fleet.example.com)
+
+ 2. `sails_custom__fleetApiToken`: An API token for an API-only user on your Fleet instance.
+
+ >You can read about how to create an API-only user and get it's token [here](https://fleetdm.com/docs/using-fleet/fleetctl-cli#create-api-only-user)
+
+3. Open the `ee/bulk-operations-dashboard/` folder in your terminal.
+
+4. Run `docker compose up --build` to build the bulk operations dashboard's Docker image.
+
+ > The first time the bulk operations dashboard starts it will Initalize the database aby running the `config/bootstrap.js` script before the server starts.
+
+5. Once the container is done building, the bulk operations dashboard will be available at http://localhost:1337
+
+ > You can login with the default admin login:
+ >
+ >- Email address: `admin@example.com`
+ >
+ >- Password: `abc123`
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/logout.js b/ee/bulk-operations-dashboard/api/controllers/account/logout.js
new file mode 100644
index 0000000000..61e50d8c19
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/logout.js
@@ -0,0 +1,55 @@
+module.exports = {
+
+
+ friendlyName: 'Logout',
+
+
+ description: 'Log out of this app.',
+
+
+ extendedDescription:
+`This action deletes the \`req.session.userId\` key from the session of the requesting user agent.
+Actual garbage collection of session data depends on this app's session store, and
+potentially also on the [TTL configuration](https://sailsjs.com/docs/reference/configuration/sails-config-session)
+you provided for it.
+
+Note that this action does not check to see whether or not the requesting user was
+actually logged in. (If they weren't, then this action is just a no-op.)`,
+
+
+ exits: {
+
+ success: {
+ description: 'The requesting user agent has been successfully logged out.'
+ },
+
+ redirect: {
+ description: 'The requesting user agent looks to be a web browser.',
+ extendedDescription: 'After logging out from a web browser, the user is redirected away.',
+ responseType: 'redirect'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // Clear the `userId` property from this session.
+ delete this.req.session.userId;
+
+ // Broadcast a message that we can display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ // Then finish up, sending an appropriate response.
+ // > Under the covers, this persists the now-logged-out session back
+ // > to the underlying session store.
+ if (!this.req.wantsJSON) {
+ throw {redirect: '/login'};
+ }
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/update-billing-card.js b/ee/bulk-operations-dashboard/api/controllers/account/update-billing-card.js
new file mode 100644
index 0000000000..7cbeac5bab
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/update-billing-card.js
@@ -0,0 +1,79 @@
+module.exports = {
+
+
+ friendlyName: 'Update billing card',
+
+
+ description: 'Update the credit card for the logged-in user.',
+
+
+ inputs: {
+
+ stripeToken: {
+ type: 'string',
+ example: 'tok_199k3qEXw14QdSnRwmsK99MH',
+ description: 'The single-use Stripe Checkout token identifier representing the user\'s payment source (i.e. credit card.)',
+ extendedDescription: 'Omit this (or use "") to remove this user\'s payment source.',
+ whereToGet: {
+ description: 'This Stripe.js token is provided to the front-end (client-side) code after completing a Stripe Checkout or Stripe Elements flow.'
+ }
+ },
+
+ billingCardLast4: {
+ type: 'string',
+ example: '4242',
+ description: 'Omit if removing card info.',
+ whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
+ },
+
+ billingCardBrand: {
+ type: 'string',
+ example: 'visa',
+ description: 'Omit if removing card info.',
+ whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
+ },
+
+ billingCardExpMonth: {
+ type: 'string',
+ example: '08',
+ description: 'Omit if removing card info.',
+ whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
+ },
+
+ billingCardExpYear: {
+ type: 'string',
+ example: '2023',
+ description: 'Omit if removing card info.',
+ whereToGet: { description: 'Credit card info is provided by Stripe after completing the checkout flow.' }
+ },
+
+ },
+
+
+ fn: async function ({stripeToken, billingCardLast4, billingCardBrand, billingCardExpMonth, billingCardExpYear}) {
+
+ // Add, update, or remove the default payment source for the logged-in user's
+ // customer entry in Stripe.
+ var stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
+ stripeCustomerId: this.req.me.stripeCustomerId,
+ token: stripeToken || '',
+ }).timeout(5000).retry();
+
+ // Update (or clear) the card info we have stored for this user in our database.
+ // > Remember, never store complete card numbers-- only the last 4 digits + expiration!
+ // > Storing (or even receiving) complete, unencrypted card numbers would require PCI
+ // > compliance in the U.S.
+ await User.updateOne({ id: this.req.me.id })
+ .set({
+ stripeCustomerId,
+ hasBillingCard: stripeToken ? true : false,
+ billingCardBrand: stripeToken ? billingCardBrand : '',
+ billingCardLast4: stripeToken ? billingCardLast4 : '',
+ billingCardExpMonth: stripeToken ? billingCardExpMonth : '',
+ billingCardExpYear: stripeToken ? billingCardExpYear : ''
+ });
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/update-password.js b/ee/bulk-operations-dashboard/api/controllers/account/update-password.js
new file mode 100644
index 0000000000..00a53a38d6
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/update-password.js
@@ -0,0 +1,35 @@
+module.exports = {
+
+
+ friendlyName: 'Update password',
+
+
+ description: 'Update the password for the logged-in user.',
+
+
+ inputs: {
+
+ password: {
+ description: 'The new, unencrypted password.',
+ example: 'abc123v2',
+ required: true
+ }
+
+ },
+
+
+ fn: async function ({password}) {
+
+ // Hash the new password.
+ var hashed = await sails.helpers.passwords.hashPassword(password);
+
+ // Update the record for the logged-in user.
+ await User.updateOne({ id: this.req.me.id })
+ .set({
+ password: hashed
+ });
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/update-profile.js b/ee/bulk-operations-dashboard/api/controllers/account/update-profile.js
new file mode 100644
index 0000000000..afc5fd1b54
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/update-profile.js
@@ -0,0 +1,160 @@
+module.exports = {
+
+
+ friendlyName: 'Update profile',
+
+
+ description: 'Update the profile for the logged-in user.',
+
+
+ inputs: {
+
+ fullName: {
+ type: 'string'
+ },
+
+ emailAddress: {
+ type: 'string'
+ },
+
+ },
+
+
+ exits: {
+
+ emailAlreadyInUse: {
+ statusCode: 409,
+ description: 'The provided email address is already in use.',
+ },
+
+ },
+
+
+ fn: async function ({fullName, emailAddress}) {
+
+ var newEmailAddress = emailAddress;
+ if (newEmailAddress !== undefined) {
+ newEmailAddress = newEmailAddress.toLowerCase();
+ }
+
+ // Determine if this request wants to change the current user's email address,
+ // revert her pending email address change, modify her pending email address
+ // change, or if the email address won't be affected at all.
+ var desiredEmailEffect;// ('change-immediately', 'begin-change', 'cancel-pending-change', 'modify-pending-change', or '')
+ if (
+ newEmailAddress === undefined ||
+ (this.req.me.emailStatus !== 'change-requested' && newEmailAddress === this.req.me.emailAddress) ||
+ (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailChangeCandidate)
+ ) {
+ desiredEmailEffect = '';
+ } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress === this.req.me.emailAddress) {
+ desiredEmailEffect = 'cancel-pending-change';
+ } else if (this.req.me.emailStatus === 'change-requested' && newEmailAddress !== this.req.me.emailAddress) {
+ desiredEmailEffect = 'modify-pending-change';
+ } else if (!sails.config.custom.verifyEmailAddresses || this.req.me.emailStatus === 'unconfirmed') {
+ desiredEmailEffect = 'change-immediately';
+ } else {
+ desiredEmailEffect = 'begin-change';
+ }
+
+
+ // If the email address is changing, make sure it is not already being used.
+ if (_.contains(['begin-change', 'change-immediately', 'modify-pending-change'], desiredEmailEffect)) {
+ let conflictingUser = await User.findOne({
+ or: [
+ { emailAddress: newEmailAddress },
+ { emailChangeCandidate: newEmailAddress }
+ ]
+ });
+ if (conflictingUser) {
+ throw 'emailAlreadyInUse';
+ }
+ }
+
+
+ // Start building the values to set in the db.
+ // (We always set the fullName if provided.)
+ var valuesToSet = {
+ fullName,
+ };
+
+ switch (desiredEmailEffect) {
+
+ // Change now
+ case 'change-immediately':
+ _.extend(valuesToSet, {
+ emailAddress: newEmailAddress,
+ emailChangeCandidate: '',
+ emailProofToken: '',
+ emailProofTokenExpiresAt: 0,
+ emailStatus: this.req.me.emailStatus === 'unconfirmed' ? 'unconfirmed' : 'confirmed'
+ });
+ break;
+
+ // Begin new email change, or modify a pending email change
+ case 'begin-change':
+ case 'modify-pending-change':
+ _.extend(valuesToSet, {
+ emailChangeCandidate: newEmailAddress,
+ emailProofToken: await sails.helpers.strings.random('url-friendly'),
+ emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL,
+ emailStatus: 'change-requested'
+ });
+ break;
+
+ // Cancel pending email change
+ case 'cancel-pending-change':
+ _.extend(valuesToSet, {
+ emailChangeCandidate: '',
+ emailProofToken: '',
+ emailProofTokenExpiresAt: 0,
+ emailStatus: 'confirmed'
+ });
+ break;
+
+ // Otherwise, do nothing re: email
+ }
+
+ // Save to the db
+ await User.updateOne({id: this.req.me.id })
+ .set(valuesToSet);
+
+ // If this is an immediate change, and billing features are enabled,
+ // then also update the billing email for this user's linked customer entry
+ // in the Stripe API to make sure they receive email receipts.
+ // > Note: If there was not already a Stripe customer entry for this user,
+ // > then one will be set up implicitly, so we'll need to persist it to our
+ // > database. (This could happen if Stripe credentials were not configured
+ // > at the time this user was originally created.)
+ if(desiredEmailEffect === 'change-immediately' && sails.config.custom.enableBillingFeatures) {
+ let didNotAlreadyHaveCustomerId = (! this.req.me.stripeCustomerId);
+ let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
+ stripeCustomerId: this.req.me.stripeCustomerId,
+ emailAddress: newEmailAddress
+ }).timeout(5000).retry();
+ if (didNotAlreadyHaveCustomerId){
+ await User.updateOne({ id: this.req.me.id })
+ .set({
+ stripeCustomerId
+ });
+ }
+ }
+
+ // If an email address change was requested, and re-confirmation is required,
+ // send the "confirm account" email.
+ if (desiredEmailEffect === 'begin-change' || desiredEmailEffect === 'modify-pending-change') {
+ await sails.helpers.sendTemplateEmail.with({
+ to: newEmailAddress,
+ subject: 'Your account has been updated',
+ template: 'email-verify-new-email',
+ templateData: {
+ fullName: fullName||this.req.me.fullName,
+ token: valuesToSet.emailProofToken
+ }
+ });
+ }
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/view-account-overview.js b/ee/bulk-operations-dashboard/api/controllers/account/view-account-overview.js
new file mode 100644
index 0000000000..f5841f9981
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/view-account-overview.js
@@ -0,0 +1,30 @@
+module.exports = {
+
+
+ friendlyName: 'View account overview',
+
+
+ description: 'Display "Account Overview" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/account/account-overview',
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // If billing features are enabled, include our configured Stripe.js
+ // public key in the view locals. Otherwise, leave it as undefined.
+ return {
+ stripePublishableKey: sails.config.custom.enableBillingFeatures? sails.config.custom.stripePublishableKey : undefined,
+ };
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/view-edit-password.js b/ee/bulk-operations-dashboard/api/controllers/account/view-edit-password.js
new file mode 100644
index 0000000000..208a1d6a3f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/view-edit-password.js
@@ -0,0 +1,26 @@
+module.exports = {
+
+
+ friendlyName: 'View edit password',
+
+
+ description: 'Display "Edit password" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/account/edit-password'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/account/view-edit-profile.js b/ee/bulk-operations-dashboard/api/controllers/account/view-edit-profile.js
new file mode 100644
index 0000000000..baea0f7c20
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/account/view-edit-profile.js
@@ -0,0 +1,26 @@
+module.exports = {
+
+
+ friendlyName: 'View edit profile',
+
+
+ description: 'Display "Edit profile" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/account/edit-profile',
+ }
+
+ },
+
+
+ fn: async function () {
+
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/confirm-email.js b/ee/bulk-operations-dashboard/api/controllers/entrance/confirm-email.js
new file mode 100644
index 0000000000..01afe04cd9
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/confirm-email.js
@@ -0,0 +1,160 @@
+module.exports = {
+
+
+ friendlyName: 'Confirm email',
+
+
+ description:
+`Confirm a new user's email address, or an existing user's request for an email address change,
+then redirect to either a special landing page (for newly-signed up users), or the account page
+(for existing users who just changed their email address).`,
+
+
+ inputs: {
+
+ token: {
+ description: 'The confirmation token from the email.',
+ example: '4-32fad81jdaf$329'
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'Email address confirmed and requesting user logged in.'
+ },
+
+ redirect: {
+ description: 'Email address confirmed and requesting user logged in. Since this looks like a browser, redirecting...',
+ responseType: 'redirect'
+ },
+
+ invalidOrExpiredToken: {
+ responseType: 'expired',
+ description: 'The provided token is expired, invalid, or already used up.',
+ },
+
+ emailAddressNoLongerAvailable: {
+ statusCode: 409,
+ viewTemplatePath: '500',
+ description: 'The email address is no longer available.',
+ extendedDescription: 'This is an edge case that is not always anticipated by websites and APIs. Since it is pretty rare, the 500 server error page is used as a simple catch-all. If this becomes important in the future, this could easily be expanded into a custom error page or resolution flow. But for context: this behavior of showing the 500 server error page mimics how popular apps like Slack behave under the same circumstances.',
+ }
+
+ },
+
+
+ fn: async function ({token}) {
+
+ // If no token was provided, this is automatically invalid.
+ if (!token) {
+ throw 'invalidOrExpiredToken';
+ }
+
+ // Get the user with the matching email token.
+ var user = await User.findOne({ emailProofToken: token });
+
+ // If no such user exists, or their token is expired, bail.
+ if (!user || user.emailProofTokenExpiresAt <= Date.now()) {
+ throw 'invalidOrExpiredToken';
+ }
+
+ if (user.emailStatus === 'unconfirmed') {
+ // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦╦═╗╔═╗╔╦╗ ╔╦╗╦╔╦╗╔═╗ ╦ ╦╔═╗╔═╗╦═╗ ┌─┐┌┬┐┌─┐┬┬
+ // │ │ ││││├┤ │├┬┘││││││││ ┬ ╠╣ ║╠╦╝╚═╗ ║───║ ║║║║║╣ ║ ║╚═╗║╣ ╠╦╝ ├┤ │││├─┤││
+ // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚ ╩╩╚═╚═╝ ╩ ╩ ╩╩ ╩╚═╝ ╚═╝╚═╝╚═╝╩╚═ └─┘┴ ┴┴ ┴┴┴─┘
+ // If this is a new user confirming their email for the first time,
+ // then just update the state of their user record in the database,
+ // store their user id in the session (just in case they aren't logged
+ // in already), and then redirect them to the "email confirmed" page.
+ await User.updateOne({ id: user.id }).set({
+ emailStatus: 'confirmed',
+ emailProofToken: '',
+ emailProofTokenExpiresAt: 0
+ });
+ this.req.session.userId = user.id;
+
+ // In case there was an existing session, broadcast a message that we can
+ // display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ if (this.req.wantsJSON) {
+ return;
+ } else {
+ throw { redirect: '/email/confirmed' };
+ }
+
+ } else if (user.emailStatus === 'change-requested') {
+ // ┌─┐┌─┐┌┐┌┌─┐┬┬─┐┌┬┐┬┌┐┌┌─┐ ╔═╗╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ ┌─┐┌┬┐┌─┐┬┬
+ // │ │ ││││├┤ │├┬┘││││││││ ┬ ║ ╠═╣╠═╣║║║║ ╦║╣ ║║ ├┤ │││├─┤││
+ // └─┘└─┘┘└┘└ ┴┴└─┴ ┴┴┘└┘└─┘ ╚═╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ └─┘┴ ┴┴ ┴┴┴─┘
+ if (!user.emailChangeCandidate){
+ throw new Error(`Consistency violation: Could not update Stripe customer because this user record's emailChangeCandidate ("${user.emailChangeCandidate}") is missing. (This should never happen.)`);
+ }
+
+ // Last line of defense: since email change candidates are not protected
+ // by a uniqueness constraint in the database, it's important that we make
+ // sure no one else managed to grab this email in the mean time since we
+ // last checked its availability. (This is a relatively rare edge case--
+ // see exit description.)
+ if (await User.count({ emailAddress: user.emailChangeCandidate }) > 0) {
+ throw 'emailAddressNoLongerAvailable';
+ }
+
+ // If billing features are enabled, also update the billing email for this
+ // user's linked customer entry in the Stripe API to make sure they receive
+ // email receipts.
+ // > Note: If there was not already a Stripe customer entry for this user,
+ // > then one will be set up implicitly, so we'll need to persist it to our
+ // > database. (This could happen if Stripe credentials were not configured
+ // > at the time this user was originally created.)
+ if(sails.config.custom.enableBillingFeatures) {
+ let didNotAlreadyHaveCustomerId = (! user.stripeCustomerId);
+ let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
+ stripeCustomerId: user.stripeCustomerId,
+ emailAddress: user.emailChangeCandidate
+ }).timeout(5000).retry();
+ if (didNotAlreadyHaveCustomerId){
+ await User.updateOne({ id: user.id }).set({
+ stripeCustomerId
+ });
+ }
+ }
+
+ // Finally update the user in the database, store their id in the session
+ // (just in case they aren't logged in already), then redirect them to
+ // their "my account" page so they can see their updated email address.
+ await User.updateOne({ id: user.id })
+ .set({
+ emailStatus: 'confirmed',
+ emailProofToken: '',
+ emailProofTokenExpiresAt: 0,
+ emailAddress: user.emailChangeCandidate,
+ emailChangeCandidate: '',
+ });
+ this.req.session.userId = user.id;
+
+ // In case there was an existing session, broadcast a message that we can
+ // display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ if (this.req.wantsJSON) {
+ return;
+ } else {
+ throw { redirect: '/account' };
+ }
+
+ } else {
+ throw new Error(`Consistency violation: User ${user.id} has an email proof token, but somehow also has an emailStatus of "${user.emailStatus}"! (This should never happen.)`);
+ }
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/login.js b/ee/bulk-operations-dashboard/api/controllers/entrance/login.js
new file mode 100644
index 0000000000..a95aabf84c
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/login.js
@@ -0,0 +1,119 @@
+module.exports = {
+
+
+ friendlyName: 'Login',
+
+
+ description: 'Log in using the provided email and password combination.',
+
+
+ extendedDescription:
+`This action attempts to look up the user record in the database with the
+specified email address. Then, if such a user exists, it uses
+bcrypt to compare the hashed password from the database with the provided
+password attempt.`,
+
+
+ inputs: {
+
+ emailAddress: {
+ description: 'The email to try in this attempt, e.g. "irl@example.com".',
+ type: 'string',
+ required: true
+ },
+
+ password: {
+ description: 'The unencrypted password to try in this attempt, e.g. "passwordlol".',
+ type: 'string',
+ required: true
+ },
+
+ rememberMe: {
+ description: 'Whether to extend the lifetime of the user\'s session.',
+ extendedDescription:
+`Note that this is NOT SUPPORTED when using virtual requests (e.g. sending
+requests over WebSockets instead of HTTP).`,
+ type: 'boolean'
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'The requesting user agent has been successfully logged in.',
+ extendedDescription:
+`Under the covers, this stores the id of the logged-in user in the session
+as the \`userId\` key. The next time this user agent sends a request, assuming
+it includes a cookie (like a web browser), Sails will automatically make this
+user id available as req.session.userId in the corresponding action. (Also note
+that, thanks to the included "custom" hook, when a relevant request is received
+from a logged-in user, that user's entire record from the database will be fetched
+and exposed as \`req.me\`.)`
+ },
+
+ badCombo: {
+ description: `The provided email and password combination does not
+ match any user in the database.`,
+ responseType: 'unauthorized'
+ // ^This uses the custom `unauthorized` response located in `api/responses/unauthorized.js`.
+ // To customize the generic "unauthorized" response across this entire app, change that file
+ // (see api/responses/unauthorized).
+ //
+ // To customize the response for _only this_ action, replace `responseType` with
+ // something else. For example, you might set `statusCode: 498` and change the
+ // implementation below accordingly (see http://sailsjs.com/docs/concepts/controllers).
+ }
+
+ },
+
+
+ fn: async function ({emailAddress, password, rememberMe}) {
+
+ // Look up by the email address.
+ // (note that we lowercase it to ensure the lookup is always case-insensitive,
+ // regardless of which database we're using)
+ var userRecord = await User.findOne({
+ emailAddress: emailAddress.toLowerCase(),
+ });
+
+ // If there was no matching user, respond thru the "badCombo" exit.
+ if(!userRecord) {
+ throw 'badCombo';
+ }
+
+ // If the password doesn't match, then also exit thru "badCombo".
+ await sails.helpers.passwords.checkPassword(password, userRecord.password)
+ .intercept('incorrect', 'badCombo');
+
+ // If "Remember Me" was enabled, then keep the session alive for
+ // a longer amount of time. (This causes an updated "Set Cookie"
+ // response header to be sent as the result of this request -- thus
+ // we must be dealing with a traditional HTTP request in order for
+ // this to work.)
+ if (rememberMe) {
+ if (this.req.isSocket) {
+ sails.log.warn(
+ 'Received `rememberMe: true` from a virtual request, but it was ignored\n'+
+ 'because a browser\'s session cookie cannot be reset over sockets.\n'+
+ 'Please use a traditional HTTP request instead.'
+ );
+ } else {
+ this.req.session.cookie.maxAge = sails.config.custom.rememberMeCookieMaxAge;
+ }
+ }//fi
+
+ // Modify the active session instance.
+ // (This will be persisted when the response is sent.)
+ this.req.session.userId = userRecord.id;
+
+ // In case there was an existing session (e.g. if we allow users to go to the login page
+ // when they're already logged in), broadcast a message that we can display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ }
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/send-password-recovery-email.js b/ee/bulk-operations-dashboard/api/controllers/entrance/send-password-recovery-email.js
new file mode 100644
index 0000000000..21d8f0efa2
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/send-password-recovery-email.js
@@ -0,0 +1,66 @@
+module.exports = {
+
+
+ friendlyName: 'Send password recovery email',
+
+
+ description: 'Send a password recovery notification to the user with the specified email address.',
+
+
+ inputs: {
+
+ emailAddress: {
+ description: 'The email address of the alleged user who wants to recover their password.',
+ example: 'rydahl@example.com',
+ type: 'string',
+ required: true
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'The email address might have matched a user in the database. (If so, a recovery email was sent.)'
+ },
+
+ },
+
+
+ fn: async function ({emailAddress}) {
+
+ // Find the record for this user.
+ // (Even if no such user exists, pretend it worked to discourage sniffing.)
+ var userRecord = await User.findOne({ emailAddress });
+ if (!userRecord) {
+ return;
+ }//•
+
+ // Come up with a pseudorandom, probabilistically-unique token for use
+ // in our password recovery email.
+ var token = await sails.helpers.strings.random('url-friendly');
+
+ // Store the token on the user record
+ // (This allows us to look up the user when the link from the email is clicked.)
+ await User.updateOne({ id: userRecord.id })
+ .set({
+ passwordResetToken: token,
+ passwordResetTokenExpiresAt: Date.now() + sails.config.custom.passwordResetTokenTTL,
+ });
+
+ // Send recovery email
+ await sails.helpers.sendTemplateEmail.with({
+ to: emailAddress,
+ subject: 'Password reset instructions',
+ template: 'email-reset-password',
+ templateData: {
+ fullName: userRecord.fullName,
+ token: token
+ }
+ });
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/signup.js b/ee/bulk-operations-dashboard/api/controllers/entrance/signup.js
new file mode 100644
index 0000000000..e1fd133c59
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/signup.js
@@ -0,0 +1,127 @@
+module.exports = {
+
+
+ friendlyName: 'Signup',
+
+
+ description: 'Sign up for a new user account.',
+
+
+ extendedDescription:
+`This creates a new user record in the database, signs in the requesting user agent
+by modifying its [session](https://sailsjs.com/documentation/concepts/sessions), and
+(if emailing with Mailgun is enabled) sends an account verification email.
+
+If a verification email is sent, the new user's account is put in an "unconfirmed" state
+until they confirm they are using a legitimate email address (by clicking the link in
+the account verification message.)`,
+
+
+ inputs: {
+
+ emailAddress: {
+ required: true,
+ type: 'string',
+ isEmail: true,
+ description: 'The email address for the new account, e.g. m@example.com.',
+ extendedDescription: 'Must be a valid email address.',
+ },
+
+ password: {
+ required: true,
+ type: 'string',
+ maxLength: 200,
+ example: 'passwordlol',
+ description: 'The unencrypted password to use for the new account.'
+ },
+
+ fullName: {
+ required: true,
+ type: 'string',
+ example: 'Frida Kahlo de Rivera',
+ description: 'The user\'s full name.',
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'New user account was created successfully.'
+ },
+
+ invalid: {
+ responseType: 'badRequest',
+ description: 'The provided fullName, password and/or email address are invalid.',
+ extendedDescription: 'If this request was sent from a graphical user interface, the request '+
+ 'parameters should have been validated/coerced _before_ they were sent.'
+ },
+
+ emailAlreadyInUse: {
+ statusCode: 409,
+ description: 'The provided email address is already in use.',
+ },
+
+ },
+
+
+ fn: async function ({emailAddress, password, fullName}) {
+
+ var newEmailAddress = emailAddress.toLowerCase();
+
+ // Build up data for the new user record and save it to the database.
+ // (Also use `fetch` to retrieve the new ID so that we can use it below.)
+ var newUserRecord = await User.create(_.extend({
+ fullName,
+ emailAddress: newEmailAddress,
+ password: await sails.helpers.passwords.hashPassword(password),
+ tosAcceptedByIp: this.req.ip
+ }, sails.config.custom.verifyEmailAddresses? {
+ emailProofToken: await sails.helpers.strings.random('url-friendly'),
+ emailProofTokenExpiresAt: Date.now() + sails.config.custom.emailProofTokenTTL,
+ emailStatus: 'unconfirmed'
+ }:{}))
+ .intercept('E_UNIQUE', 'emailAlreadyInUse')
+ .intercept({name: 'UsageError'}, 'invalid')
+ .fetch();
+
+ // If billing feaures are enabled, save a new customer entry in the Stripe API.
+ // Then persist the Stripe customer id in the database.
+ if (sails.config.custom.enableBillingFeatures) {
+ let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
+ emailAddress: newEmailAddress
+ }).timeout(5000).retry();
+ await User.updateOne({id: newUserRecord.id})
+ .set({
+ stripeCustomerId
+ });
+ }
+
+ // Store the user's new id in their session.
+ this.req.session.userId = newUserRecord.id;
+
+ // In case there was an existing session (e.g. if we allow users to go to the signup page
+ // when they're already logged in), broadcast a message that we can display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ if (sails.config.custom.verifyEmailAddresses) {
+ // Send "confirm account" email
+ await sails.helpers.sendTemplateEmail.with({
+ to: newEmailAddress,
+ subject: 'Please confirm your account',
+ template: 'email-verify-account',
+ templateData: {
+ fullName,
+ token: newUserRecord.emailProofToken
+ }
+ });
+ } else {
+ sails.log.info('Skipping new account email verification... (since `verifyEmailAddresses` is disabled)');
+ }
+
+ }
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/update-password-and-login.js b/ee/bulk-operations-dashboard/api/controllers/entrance/update-password-and-login.js
new file mode 100644
index 0000000000..51a75b8746
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/update-password-and-login.js
@@ -0,0 +1,80 @@
+module.exports = {
+
+
+ friendlyName: 'Update password and login',
+
+
+ description: 'Finish the password recovery flow by setting the new password and '+
+ 'logging in the requesting user, based on the authenticity of their token.',
+
+
+ inputs: {
+
+ password: {
+ description: 'The new, unencrypted password.',
+ example: 'abc123v2',
+ required: true
+ },
+
+ token: {
+ description: 'The password token that was generated by the `sendPasswordRecoveryEmail` endpoint.',
+ example: 'gwa8gs8hgw9h2g9hg29hgwh9asdgh9q34$$$$$asdgasdggds',
+ required: true
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'Password successfully updated, and requesting user agent is now logged in.'
+ },
+
+ invalidToken: {
+ description: 'The provided password token is invalid, expired, or has already been used.',
+ responseType: 'expired'
+ }
+
+ },
+
+
+ fn: async function ({password, token}) {
+
+ if(!token) {
+ throw 'invalidToken';
+ }
+
+ // Look up the user with this reset token.
+ var userRecord = await User.findOne({ passwordResetToken: token });
+
+ // If no such user exists, or their token is expired, bail.
+ if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) {
+ throw 'invalidToken';
+ }
+
+ // Hash the new password.
+ var hashed = await sails.helpers.passwords.hashPassword(password);
+
+ // Store the user's new password and clear their reset token so it can't be used again.
+ await User.updateOne({ id: userRecord.id })
+ .set({
+ password: hashed,
+ passwordResetToken: '',
+ passwordResetTokenExpiresAt: 0
+ });
+
+ // Log the user in.
+ // (This will be persisted when the response is sent.)
+ this.req.session.userId = userRecord.id;
+
+ // In case there was an existing session, broadcast a message that we can
+ // display in other open tabs.
+ if (sails.hooks.sockets) {
+ await sails.helpers.broadcastSessionChange(this.req);
+ }
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/view-confirmed-email.js b/ee/bulk-operations-dashboard/api/controllers/entrance/view-confirmed-email.js
new file mode 100644
index 0000000000..0602f10e99
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/view-confirmed-email.js
@@ -0,0 +1,27 @@
+module.exports = {
+
+
+ friendlyName: 'View confirmed email',
+
+
+ description: 'Display "Confirmed email" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/entrance/confirmed-email'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // Respond with view.
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/view-forgot-password.js b/ee/bulk-operations-dashboard/api/controllers/entrance/view-forgot-password.js
new file mode 100644
index 0000000000..e6b5404c9f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/view-forgot-password.js
@@ -0,0 +1,36 @@
+module.exports = {
+
+
+ friendlyName: 'View forgot password',
+
+
+ description: 'Display "Forgot password" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/entrance/forgot-password',
+ },
+
+ redirect: {
+ description: 'The requesting user is already logged in.',
+ extendedDescription: 'Logged-in users should change their password in "Account settings."',
+ responseType: 'redirect',
+ }
+
+ },
+
+
+ fn: async function () {
+
+ if (this.req.me) {
+ throw {redirect: '/'};
+ }
+
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/view-login.js b/ee/bulk-operations-dashboard/api/controllers/entrance/view-login.js
new file mode 100644
index 0000000000..1d1c590b53
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/view-login.js
@@ -0,0 +1,35 @@
+module.exports = {
+
+
+ friendlyName: 'View login',
+
+
+ description: 'Display "Login" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/entrance/login',
+ },
+
+ redirect: {
+ description: 'The requesting user is already logged in.',
+ responseType: 'redirect'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ if (this.req.me) {
+ throw {redirect: '/'};
+ }
+
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/view-new-password.js b/ee/bulk-operations-dashboard/api/controllers/entrance/view-new-password.js
new file mode 100644
index 0000000000..4532fa5fef
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/view-new-password.js
@@ -0,0 +1,57 @@
+module.exports = {
+
+
+ friendlyName: 'View new password',
+
+
+ description: 'Display "New password" page.',
+
+
+ inputs: {
+
+ token: {
+ description: 'The password reset token from the email.',
+ example: '4-32fad81jdaf$329'
+ }
+
+ },
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/entrance/new-password'
+ },
+
+ invalidOrExpiredToken: {
+ responseType: 'expired',
+ description: 'The provided token is expired, invalid, or has already been used.',
+ }
+
+ },
+
+
+ fn: async function ({token}) {
+
+ // If password reset token is missing, display an error page explaining that the link is bad.
+ if (!token) {
+ sails.log.warn('Attempting to view new password (recovery) page, but no reset password token included in request! Displaying error page...');
+ throw 'invalidOrExpiredToken';
+ }//•
+
+ // Look up the user with this reset token.
+ var userRecord = await User.findOne({ passwordResetToken: token });
+ // If no such user exists, or their token is expired, display an error page explaining that the link is bad.
+ if (!userRecord || userRecord.passwordResetTokenExpiresAt <= Date.now()) {
+ throw 'invalidOrExpiredToken';
+ }
+
+ // Grab token and include it in view locals
+ return {
+ token,
+ };
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/entrance/view-signup.js b/ee/bulk-operations-dashboard/api/controllers/entrance/view-signup.js
new file mode 100644
index 0000000000..be43753770
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/entrance/view-signup.js
@@ -0,0 +1,35 @@
+module.exports = {
+
+
+ friendlyName: 'View signup',
+
+
+ description: 'Display "Signup" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/entrance/signup',
+ },
+
+ redirect: {
+ description: 'The requesting user is already logged in.',
+ responseType: 'redirect'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ if (this.req.me) {
+ throw {redirect: '/'};
+ }
+
+ return {};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/get-profiles.js b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js
new file mode 100644
index 0000000000..7326fea9ae
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/get-profiles.js
@@ -0,0 +1,113 @@
+module.exports = {
+
+
+ friendlyName: 'Get profiles',
+
+
+ description: 'Builds and returns an array of deployed configuration profiles on the Fleet instance and undeployed profiles stored in the dashboard\'s datastore.',
+
+ exits: {
+ success: {
+ outputType: [{}],
+ }
+ },
+
+
+ fn: async function () {
+ // Get all teams on the Fleet instance.
+ let teamsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/teams',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+
+ let allTeams = teamsResponseData.teams;
+
+ let teams = [];
+ for(let team of allTeams) {
+ teams.push({
+ fleetApid: team.id,
+ teamName: team.name,
+ });
+ }
+ // Add the "team" for hosts with no team
+ teams.push({
+ fleetApid: 0,
+ teamName: 'No team',
+ });
+
+
+ let allProfiles = [];
+ let teamApids = _.pluck(allTeams, 'id');
+ // Get all of the configuration profiles on the Fleet instance.
+ for(let teamApid of teamApids){
+ let configurationProfilesResponseData = await sails.helpers.http.get.with({
+ url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`,
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let profilesForThisTeam = configurationProfilesResponseData.profiles;
+ allProfiles = allProfiles.concat(profilesForThisTeam);
+ }
+
+ // Add the configurations profiles that are assigned to the "no team" team.
+ let noTeamConfigurationProfilesResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/configuration_profiles',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles;
+ allProfiles = allProfiles.concat(profilesForThisTeam);
+
+ // console.log(allProfiles);
+
+ let profilesOnThisFleetInstance = [];
+ // Group configuration profiles by their identifier.
+ let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier');
+ for(let profileIdentifier in allProfilesByIdentifier) {
+ // Iterate through the arrays of profiles with the same unique identifier.
+ let teamsForThisProfile = [];
+ // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles.
+ for(let profile of allProfilesByIdentifier[profileIdentifier]){
+ let informationAboutThisProfile = {
+ uuid: profile.profile_uuid,
+ fleetApid: profile.team_id,
+ teamName: _.find(teams, {fleetApid: profile.team_id}).teamName,
+ };
+ teamsForThisProfile.push(informationAboutThisProfile);
+ }
+ let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration.
+ let profileInformation = {
+ name: profile.name,
+ identifier: profileIdentifier,
+ platform: profile.platform,
+ createdAt: new Date(profile.created_at).getTime(),
+ teams: teamsForThisProfile
+ };
+ profilesOnThisFleetInstance.push(profileInformation);
+ }
+ // Get the undeployed profiles from the app's database.
+ let undeployedProfiles = await UndeployedProfile.find();
+ profilesOnThisFleetInstance = _.union(profilesOnThisFleetInstance, undeployedProfiles);
+
+ // Sort profiles by their name.
+ profilesOnThisFleetInstance = _.sortByOrder(profilesOnThisFleetInstance, 'name', 'asc');
+
+ return profilesOnThisFleetInstance;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/get-scripts.js b/ee/bulk-operations-dashboard/api/controllers/get-scripts.js
new file mode 100644
index 0000000000..1900b9b700
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/get-scripts.js
@@ -0,0 +1,120 @@
+module.exports = {
+
+
+ friendlyName: 'Get scripts',
+
+
+ description: 'Builds and returns an array of deployed scripts on the Fleet instance and undeployed scripts stored in the dashboard\'s datastore',
+
+
+ exits: {
+ success: {
+ outputType: [{}],
+ }
+ },
+
+
+ fn: async function () {
+ // Get all teams on the Fleet instance.
+ let teamsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/teams',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+
+ let allTeams = teamsResponseData.teams;
+
+ let teamApids = _.pluck(allTeams, 'id');
+ let teams = [];
+ for(let team of allTeams) {
+ teams.push({
+ fleetApid: team.id,
+ teamName: team.name,
+ });
+ }
+ // Add the "team" for hosts with no team
+ teams.push({
+ fleetApid: 0,
+ teamName: 'No team',
+ });
+
+
+ let allScripts = [];
+ // Get all of the scripts on a Fleet instance.
+ for(let teamApid of teamApids){
+ let scriptsResponseData = await sails.helpers.http.get.with({
+ url: `/api/v1/fleet/scripts?team_id=${teamApid}`,
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let scriptsForThisTeam = scriptsResponseData.scripts;
+ if(scriptsForThisTeam !== null) {
+ allScripts = allScripts.concat(scriptsForThisTeam);
+ }
+ }
+
+ // Grab all of the configuration scripts on the Fleet instance.
+ let noTeamConfigurationScriptsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/scripts',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let scriptsForThisTeam = noTeamConfigurationScriptsResponseData.scripts;
+
+ if(scriptsForThisTeam !== null){
+ allScripts = allScripts.concat(scriptsForThisTeam);
+ }
+
+ // If there are no scripts on the Fleet instance, return an empty array and the teams information.
+ if(allScripts === [ null ]){
+ return {scripts: [], teams};
+ }
+ let scriptsOnThisFleetInstance = [];
+
+ let allScriptsByIdentifier = _.groupBy(allScripts, 'name');
+ for(let scriptIdentifier in allScriptsByIdentifier) {
+ if(scriptIdentifier === null){
+ continue;
+ }
+ let teamsForThisProfile = [];
+ for(let script of allScriptsByIdentifier[scriptIdentifier]){
+ let informationAboutThisScript = {
+ scriptFleetApid: script.id,
+ fleetApid: script.team_id ? script.team_id : 0,
+ teamName: script.team_id ? _.find(teams, {fleetApid: script.team_id}).teamName : 'No team',
+ };
+ teamsForThisProfile.push(informationAboutThisScript);
+ }
+ let script = allScriptsByIdentifier[scriptIdentifier][0];// Grab the first script returned in the api repsonse to build our script configuration.
+ let scriptInformation = {
+ name: script.name,
+ identifier: scriptIdentifier,
+ platform: _.endsWith(script.name, 'sh') ? 'macOS & Linux' : 'Windows',
+ createdAt: new Date(script.created_at).getTime(),
+ teams: teamsForThisProfile
+ };
+ scriptsOnThisFleetInstance.push(scriptInformation);
+ }
+ // Get the undeployed scripts from the app's database.
+ let undeployedScripts = await UndeployedScript.find();
+ scriptsOnThisFleetInstance = _.union(scriptsOnThisFleetInstance, undeployedScripts);
+ // Sort scripts by their name.
+ scriptsOnThisFleetInstance = _.sortByOrder(scriptsOnThisFleetInstance, 'name', 'asc');
+ return scriptsOnThisFleetInstance;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/legal/view-privacy.js b/ee/bulk-operations-dashboard/api/controllers/legal/view-privacy.js
new file mode 100644
index 0000000000..6960be873f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/legal/view-privacy.js
@@ -0,0 +1,27 @@
+module.exports = {
+
+
+ friendlyName: 'View privacy',
+
+
+ description: 'Display "Privacy policy" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/legal/privacy'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/legal/view-terms.js b/ee/bulk-operations-dashboard/api/controllers/legal/view-terms.js
new file mode 100644
index 0000000000..643f04408a
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/legal/view-terms.js
@@ -0,0 +1,27 @@
+module.exports = {
+
+
+ friendlyName: 'View terms',
+
+
+ description: 'Display "Legal terms" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/legal/terms'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/delete-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/delete-profile.js
new file mode 100644
index 0000000000..3a97405869
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/profiles/delete-profile.js
@@ -0,0 +1,46 @@
+module.exports = {
+
+
+ friendlyName: 'Delete profile',
+
+
+ description: '',
+
+
+ inputs: {
+ profile: {
+ type: {},
+ description: 'The configuration profile that will be deleted.',
+ required: true,
+ }
+ },
+
+
+ exits: {
+
+ },
+
+
+ fn: async function ({profile}) {
+ // If the provided profile does not have a teams array and has an ID, it is an undeployed profile that will be deleted.
+ if(profile.id && !profile.teams){
+ await UndeployedProfile.destroy({id: profile.id});
+ } else {// Otherwise, this is a deployed profile, and we'll use information from the teams array to remove the profile.
+ for(let team of profile.teams){
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles/${team.uuid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ }
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/download-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/download-profile.js
new file mode 100644
index 0000000000..ebbf569d52
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/profiles/download-profile.js
@@ -0,0 +1,78 @@
+module.exports = {
+
+
+ friendlyName: 'Download profile',
+
+
+ description: 'Download profile file (returning a stream).',
+
+
+ inputs: {
+ id: {
+ type: 'number',
+ description: 'The database ID of the undeployed profile to download.'
+ },
+ uuid: {
+ type: 'string',
+ description: 'The uuid of a profile on a team.'
+ },
+ },
+
+
+ exits: {
+ success: {
+ outputFriendlyName: 'File',
+ outputDescription: 'The streaming bytes of the file.',
+ outputType: 'ref'
+ },
+
+ notFound: {
+ description: 'No profile exists with the specified ID or UUID.',
+ responseType: 'notFound'
+ },
+ },
+
+
+ fn: async function ({id, uuid}) {
+ if(!uuid && !id){
+ return this.res.badRequest();
+ }
+ let datePrefix = new Date().toISOString();
+ datePrefix = datePrefix.split('T')[0] +'_';
+ let profileContents;
+ let filename;
+ let download;
+ if(id){
+ let profileToDownload = await UndeployedProfile.findOne({id: id});
+
+ filename = datePrefix + profileToDownload.name + profileToDownload.profileType;
+ profileContents = profileToDownload.profileContents;
+ if(profileToDownload.profileType === '.mobileconfig'){
+ this.res.type('application/x-apple-aspen-config');
+ } else {
+ this.res.type('application/octet-stream');
+ }
+ download = profileContents;
+ } else {
+ let profileDownloadResponse = await sails.helpers.http.sendHttpRequest.with({
+ method: 'GET',
+ url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/configuration_profiles/${uuid}?alt=media`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ });
+ let contentDispositionHeader = profileDownloadResponse.headers['content-disposition'];
+ let filenameMatch = contentDispositionHeader.replace(/^attachment;filename="(.+?)"$/, '$1');
+ filename = filenameMatch;
+ let contentType = profileDownloadResponse.headers['content-type'];
+ download = profileDownloadResponse.body;
+ this.res.type(contentType);
+ }
+ this.res.attachment(filename);
+ // All done.
+ return download;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js
new file mode 100644
index 0000000000..bede01b44f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/profiles/edit-profile.js
@@ -0,0 +1,204 @@
+module.exports = {
+
+
+ friendlyName: 'Edit profile',
+
+
+ description: 'Edits the teams a profile is assigned to and/or replaces the file on the Fleet instance if the new file\'s profile identifier matches',
+
+ files: ['newProfile'],
+
+ inputs: {
+ profile: {
+ type: {},
+ description: 'The configuration profile that is being editted',
+ required: true,
+ },
+ newTeamIds: {
+ type: ['string'],
+ description: 'An array of teams that this profile will be deployed on or Undefined if the profile is being removed from a team.'
+ },
+ newProfile: {
+ type: 'ref',
+ description: 'A file that will be replacing the profile.'
+ },
+ },
+
+
+ exits: {
+ payloadIdentifierDoesNotMatch: {
+ statusCode: 409,
+ description: 'The new profiles bundle indentifer does not match the existing profile',
+ }
+ },
+
+
+
+ fn: async function ({profile, newTeamIds, newProfile}) {
+ if(newProfile.isNoop){
+ newProfile.noMoreFiles();
+ newProfile = undefined;
+ }
+ // ╔═╗╔═╗╔╦╗ ╔═╗╦═╗╔═╗╔═╗╦╦ ╔═╗
+ // ║ ╦║╣ ║ ╠═╝╠╦╝║ ║╠╣ ║║ ║╣
+ // ╚═╝╚═╝ ╩ ╩ ╩╚═╚═╝╚ ╩╩═╝╚═╝
+ let profileContents; // The raw text contents of a profile file.
+ let filename;
+ let extension;
+ // If there is not a new profile, and the profile is deployed (has teams array === deployed), download the profile to be able to add it to other teams.
+ if(!newProfile && profile.teams){
+ // console.log('Existing deployed profile!');
+ let profileUuid = profile.teams[0].uuid;
+ let profileDownloadResponse = await sails.helpers.http.sendHttpRequest.with({
+ method: 'GET',
+ url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/configuration_profiles/${profileUuid}?alt=media`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ });
+ let contentDispositionHeader = profileDownloadResponse.headers['content-disposition'];
+ let filenameMatch = contentDispositionHeader.match(/filename="(.+?)"/);
+ filename = filenameMatch[1];
+ extension = '.'+filename.split('.').pop();
+ profileContents = profileDownloadResponse.body;
+ } else if(newProfile) {// Otherwise, if there is a new profile file uploaded, check that the payload identifier maches the existing profile on the Fleet instance.
+ // console.log('Replacing an existing(/undeployed) profile!');
+ let file = await sails.reservoir(newProfile);
+ profileContents = file[0].contentBytes;
+ let profileFileName = file[0].name;
+ filename = profileFileName.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.[^/.]+$/, '');
+ extension = '.'+profileFileName.split('.').pop();
+ let profilePlatform = 'darwin';
+ if(_.endsWith(profileFileName, '.xml')) {
+ profilePlatform = 'windows';
+ }
+ if(newTeamIds && profile.teams && profilePlatform === 'darwin'){
+ let existingProfileInfo = await sails.helpers.http.get.with({
+ url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/configuration_profiles/${profile.teams[0].uuid}?alt=media`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ });
+ let newProfileBundleIdentifier = profileContents.match(/PayloadIdentifier<\/key>\s*(.*?)<\/string>/)[1];
+ let existingProfileBundleIdentifier = existingProfileInfo.match(/PayloadIdentifier<\/key>\s*(.*?)<\/string>/)[1];
+ // Note: We're using the _.startsWith method to check that the identifier is the same. The identifiers returned by the Fleet instance are
+ if(existingProfileBundleIdentifier !== newProfileBundleIdentifier){
+ throw 'payloadIdentifierDoesNotMatch';
+ }
+ }
+ } else if (!newProfile && !profile.teams){// Undeployed profiles are stored in the app's database.
+ // console.log('editing an undeployed profile!');
+ profileContents = profile.profileContents;
+ filename = profile.name;
+ extension = profile.profileType;
+ }
+ // ╔═╗╔═╗╔═╗╦╔═╗╔╗╔ ╔═╗╦═╗╔═╗╔═╗╦╦ ╔═╗
+ // ╠═╣╚═╗╚═╗║║ ╦║║║ ╠═╝╠╦╝║ ║╠╣ ║║ ║╣
+ // ╩ ╩╚═╝╚═╝╩╚═╝╝╚╝ ╩ ╩╚═╚═╝╚ ╩╩═╝╚═╝
+ if(!newProfile){
+ // If we're changing the teams for an existing profile, we'll remove this profile from any team not included in the newTeamIds array.
+ let currentProfileTeamIds = _.pluck(profile.teams, 'fleetApid');
+ let addedTeams = _.difference(newTeamIds, currentProfileTeamIds);
+ let removedTeams = _.difference(currentProfileTeamIds, newTeamIds);
+ let removedTeamsInfo = _.filter(profile.teams, (team)=>{
+ return removedTeams.includes(team.fleetApid);
+ });
+ for(let team of removedTeamsInfo){
+ // console.log(`removing ${profile.name} from team id ${team.teamName}`);
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles/${team.uuid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ for(let teamApid of addedTeams){
+ // console.log(`Adding ${profile.name} to team id ${teamApid}`);
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`,
+ enctype: 'multipart/form-data',
+ body: {
+ team_id: teamApid,// eslint-disable-line camelcase
+ profile: {
+ options: {
+ filename: filename + extension,
+ contentType: 'application/octet-stream'
+ },
+ value: profileContents,
+ }
+ },
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ });
+ }// After every added team
+ } else {
+ if(profile.teams) {
+ // If there is a new profile uploaded, we will need to delete the old profiles, and add the new profile.
+ for(let team of profile.teams) {
+ // console.log(`removing ${profile.name} from team id ${team.teamName}`);
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles/${team.uuid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ }
+ for(let teamApid of newTeamIds){
+ // console.log(`Adding ${profile.name} to team id ${teamApid}`);
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`,
+ enctype: 'multipart/form-data',
+ body: {
+ team_id: teamApid,// eslint-disable-line camelcase
+ profile: {
+ options: {
+ filename: filename + extension,
+ contentType: 'application/octet-stream'
+ },
+ value: profileContents,
+ }
+ },
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ });
+ }// After every added team
+
+ }
+ // If this profile has an ID, then it is a database record, and we will delete it if it has been deployed to a team.
+ if(profile.id && newTeamIds.length > 0){
+ // console.log('Undeployed profile has been deployed. deleting DB record!');
+ await UndeployedProfile.destroy({id: profile.id});
+ } else if(!profile.id && newTeamIds.length === 0){
+ // If this is not a database record of a profile, and the profile is being undeployed from all teams, we'll create a databse record for it.
+ // console.log('Creating database record for a (now) undeployed profile!');
+ await UndeployedProfile.create({
+ name: profile.name,
+ platform: extension === '.xml' ? 'windows' : 'darwin',
+ profileContents,
+ profileType: extension,
+ });
+ } else if(profile.id && newProfile){
+ // If there is a new profile that is replacing a database record, update the profileContents in the database.
+ // console.log('Updating existing undeployed profile!');
+ await UndeployedProfile.updateOne({id: profile.id}).set({
+ profileContents,
+ });
+ }
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js
new file mode 100644
index 0000000000..19c847bf62
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/profiles/upload-profile.js
@@ -0,0 +1,120 @@
+module.exports = {
+
+
+ friendlyName: 'Upload profile',
+
+
+ description: '',
+
+ files: ['newProfile'],
+
+ inputs: {
+ newProfile: {
+ type: 'ref',
+ description: 'An Upstream with an incoming file upload.',
+ required: true,
+ },
+ teams: {
+ type: ['string'],
+ description: 'An array of team IDs that this profile will be added to'
+ }
+ },
+
+
+ exits: {
+ success: {
+ outputDescription: 'The new profile has been uploaded',
+ outputType: {},
+ },
+
+ noFileAttached: {
+ description: 'No file was attached.',
+ responseType: 'badRequest'
+ },
+
+ tooBig: {
+ description: 'The file is too big.',
+ responseType: 'badRequest'
+ },
+
+ },
+
+
+ fn: async function ({newProfile, teams}) {
+ let util = require('util');
+ let profile = await sails.reservoir(newProfile)
+ .intercept('E_EXCEEDS_UPLOAD_LIMIT', 'tooBig')
+ .intercept((err)=>new Error('The configuration profile upload failed. '+util.inspect(err)));
+ if(!profile) {
+ throw 'noFileAttached';
+ }
+ let profileContents = profile[0].contentBytes;
+ let profileFileName = profile[0].name;
+ let datelessExtensionlessFilename = profileFileName.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.[^/.]+$/, '');
+ let extension = '.'+profileFileName.split('.').pop();
+ let profilePlatform = 'darwin';
+ if(_.endsWith(profileFileName, '.xml')) {
+ profilePlatform = 'windows';
+ }
+
+ let profileToReturn;
+ let newProfileInfo = {
+ name: datelessExtensionlessFilename,
+ platform: profilePlatform,
+ profileType: extension,
+ createdAt: Date.now(),
+ };
+ if(!teams) {
+ newProfileInfo.profileContents = profileContents;
+ profileToReturn = await UndeployedProfile.create(newProfileInfo).fetch();
+ } else {
+ let newTeams = [];
+ for(let teamApid of teams){
+ let newProfileResponse = await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`,
+ enctype: 'multipart/form-data',
+ body: {
+ team_id: teamApid,// eslint-disable-line camelcase
+ profile: {
+ options: {
+ filename: profileFileName,
+ contentType: 'application/octet-stream'
+ },
+ value: profileContents,
+ }
+ },
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ });
+ let parsedJsonResponse = JSON.parse(newProfileResponse.body);
+ let uuidForThisProfile = parsedJsonResponse.profile_uuid;
+ // send a request to the Fleet instance to get the bundleId of the new profile.
+ await sails.helpers.http.get.with({
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/configuration_profiles/${uuidForThisProfile}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ newTeams.push({
+ fleetApid: teamApid,
+ uuid: JSON.parse(newProfileResponse.body).profile_uuid
+ });
+ }
+ newProfileInfo.teams = newTeams;
+ profileToReturn = newProfileInfo;
+ }
+
+
+
+
+ // All done.
+ return profileToReturn;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js
new file mode 100644
index 0000000000..9ebe807932
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/profiles/view-profiles.js
@@ -0,0 +1,114 @@
+module.exports = {
+
+
+ friendlyName: 'View profiles',
+
+
+ description: 'Display "Profiles" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/profiles'
+ }
+
+ },
+
+
+ fn: async function () {
+
+ // Get all teams on the Fleet instance.
+ let teamsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/teams',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+
+ let allTeams = teamsResponseData.teams;
+
+ let teams = [];
+ for(let team of allTeams) {
+ teams.push({
+ fleetApid: team.id,
+ teamName: team.name,
+ });
+ }
+ // Add the "team" for hosts with no team
+ teams.push({
+ fleetApid: 0,
+ teamName: 'No team',
+ });
+
+
+ let allProfiles = [];
+ let teamApids = _.pluck(allTeams, 'id');
+ // Get all of the configuration profiles on the Fleet instance.
+ for(let teamApid of teamApids){
+ let configurationProfilesResponseData = await sails.helpers.http.get.with({
+ url: `/api/v1/fleet/configuration_profiles?team_id=${teamApid}`,
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let profilesForThisTeam = configurationProfilesResponseData.profiles;
+ allProfiles = allProfiles.concat(profilesForThisTeam);
+ }
+ // Add the configurations profiles that are assigned to the "no team" team.
+ let noTeamConfigurationProfilesResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/configuration_profiles',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let profilesForThisTeam = noTeamConfigurationProfilesResponseData.profiles;
+ allProfiles = allProfiles.concat(profilesForThisTeam);
+ let profilesInformation = [];
+ // Group configuration profiles by their identifier.
+ let allProfilesByIdentifier = _.groupBy(allProfiles, 'identifier');
+ for(let profileIdentifier in allProfilesByIdentifier) {
+ // Iterate through the arrays of profiles with the same unique identifier.
+ let teamsForThisProfile = [];
+ // Add the profile's UUID and information about the team this profile is assigned to the teams array for profiles.
+ for(let profile of allProfilesByIdentifier[profileIdentifier]) {
+ let informationAboutThisProfile = {
+ uuid: profile.profile_uuid,
+ fleetApid: profile.team_id,
+ teamName: _.find(teams, {fleetApid: profile.team_id}).teamName,
+ };
+ teamsForThisProfile.push(informationAboutThisProfile);
+ }
+ let profile = allProfilesByIdentifier[profileIdentifier][0];// Grab the first profile returned in the api repsonse to build our profile configuration.
+ let profileInformation = {
+ name: profile.name,
+ identifier: profileIdentifier,
+ platform: profile.platform,
+ createdAt: new Date(profile.created_at).getTime(),
+ teams: teamsForThisProfile
+ };
+ profilesInformation.push(profileInformation);
+ }
+ // Get the undeployed profiles from the app's database.
+ let undeployedProfiles = await UndeployedProfile.find();
+ profilesInformation = _.union(profilesInformation, undeployedProfiles);
+
+ // Sort profiles by their name.
+ profilesInformation = _.sortByOrder(profilesInformation, 'name', 'asc');
+
+ // Respond with view.
+ return {profiles: profilesInformation, teams};
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/scripts/delete-script.js b/ee/bulk-operations-dashboard/api/controllers/scripts/delete-script.js
new file mode 100644
index 0000000000..f7c38c1664
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/scripts/delete-script.js
@@ -0,0 +1,46 @@
+module.exports = {
+
+
+ friendlyName: 'Delete script',
+
+
+ description: '',
+
+
+ inputs: {
+ script: {
+ type: {},
+ description: 'The script that will be deleted.',
+ required: true,
+ }
+ },
+
+
+ exits: {
+
+ },
+
+
+ fn: async function ({script}) {
+ // If the provided script does not have a teams array and has an ID, it is an undeployed script that will be deleted.
+ if(script.id && !script.teams){
+ await UndeployedScript.destroy({id: script.id});
+ } else {
+ for(let teamScript of script.teams){
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/scripts/${teamScript.scriptFleetApid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ }
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/scripts/download-script.js b/ee/bulk-operations-dashboard/api/controllers/scripts/download-script.js
new file mode 100644
index 0000000000..4c9b7bc809
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/scripts/download-script.js
@@ -0,0 +1,76 @@
+module.exports = {
+
+
+ friendlyName: 'Download script',
+
+
+ description: 'Download script file (returning a stream).',
+
+
+ inputs: {
+ fleetApid: {
+ type: 'number',
+ description: 'The Fleet API ID of the script to download.',
+ },
+ id: {
+ type: 'number',
+ description: 'The database ID of the undeployed script to download'
+ }
+ },
+
+
+ exits: {
+ success: {
+ outputFriendlyName: 'File',
+ outputDescription: 'The streaming bytes of the file.',
+ outputType: 'ref'
+ },
+
+ notFound: {
+ description: 'No script exists with the specified ID or UUID.',
+ responseType: 'notFound'
+ },
+ },
+
+
+ fn: async function ({fleetApid, id}) {
+ if(!fleetApid && !id){
+ return this.res.badRequest();
+ }
+ let filename;
+ let download;
+ if(id){
+ let datePrefix = new Date().toISOString();
+ datePrefix = datePrefix.split('T')[0] +'_';
+ let scriptToDownload = await UndeployedScript.findOne({id: id});
+ filename = datePrefix + scriptToDownload.name;
+ let scriptContents = scriptToDownload.scriptContents;
+ if(scriptToDownload.scriptType === '.sh'){
+ this.res.type('application/x-apple-aspen-config');
+ } else {
+ this.res.type('application/octet-stream');
+ }
+ download = scriptContents;
+ } else {
+ let scriptDownloadResponse = await sails.helpers.http.sendHttpRequest.with({
+ method: 'GET',
+ url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/scripts/${fleetApid}?alt=media`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ });
+ let contentDispositionHeader = scriptDownloadResponse.headers['content-disposition'];
+ let filenameMatch = contentDispositionHeader.replace(/^attachment;filename="(.+?)"$/, '$1');
+ filename = filenameMatch;
+ let contentType = scriptDownloadResponse.headers['content-type'];
+ download = scriptDownloadResponse.body;
+ this.res.type(contentType);
+ }
+ this.res.attachment(filename);
+ // All done.
+ return download;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/scripts/edit-script.js b/ee/bulk-operations-dashboard/api/controllers/scripts/edit-script.js
new file mode 100644
index 0000000000..ec8d30f182
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/scripts/edit-script.js
@@ -0,0 +1,197 @@
+module.exports = {
+
+
+ friendlyName: 'Edit script',
+
+
+ description: '',
+
+ files: ['newScript'],
+
+ inputs: {
+ script: {
+ type: {},
+ description: 'The script that is being editted',
+ required: true,
+ },
+ newTeamIds: {
+ type: ['ref'],
+ description: 'An array of teams that this script will be added to.'
+ },
+ newScript: {
+ type: 'ref',
+ description: 'A file that will be replacing the script.'
+ },
+ },
+
+
+ exits: {
+ scriptNameDoesNotMatch: {
+ description: 'The provided replacement script\'s filename does not match the name of the script on the Fleet instance.',
+ statusCode: 400,
+ },
+ },
+
+
+ fn: async function ({script, newTeamIds, newScript}) {
+ if(newScript.isNoop){
+ newScript.noMoreFiles();
+ newScript = undefined;
+ }
+ let scriptContents; // The raw text contents of a script file.
+ let filename;
+ let extension;
+ // If there is not a new script, and the script is deployed (has teams array === deployed), download the script to be able to add it to other teams.
+ if(!newScript && script.teams){
+ let scriptFleetApid = script.teams[0].scriptFleetApid;
+ let scriptDownloadResponse = await sails.helpers.http.sendHttpRequest.with({
+ method: 'GET',
+ url: `${sails.config.custom.fleetBaseUrl}/api/v1/fleet/scripts/${scriptFleetApid}?alt=media`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ });
+ let contentDispositionHeader = scriptDownloadResponse.headers['content-disposition'];
+ let filenameMatch = contentDispositionHeader.match(/filename="(.+?)"/);
+ filename = filenameMatch[1];
+ extension = '.'+filename.split('.').pop();
+ filename = filename.replace(/^\d{4}-\d{2}-\d{2}[_|\s]?/, '');
+ scriptContents = scriptDownloadResponse.body;
+ } else if(newScript) {
+ let file = await sails.reservoir(newScript);
+ scriptContents = file[0].contentBytes;
+ let scriptFilename = file[0].name;
+ filename = scriptFilename.replace(/^\d{4}-\d{2}-\d{2}[_|\s]?/, '').replace(/\.[^/.]+$/, '');
+ extension = '.'+scriptFilename.split('.').pop();
+ if(script.name !== filename+extension){
+ throw 'scriptNameDoesNotMatch';
+ }
+ } else if (!newScript && !script.teams){// Undeployed profiles are stored in the app's database.
+ // console.log('editing an undeployed profile!');
+ scriptContents = script.scriptContents;
+ filename = script.name;
+ extension = script.scriptType;
+ }
+
+ if(!newScript){
+ let currentScriptTeamIds = _.pluck(script.teams, 'fleetApid');
+ let addedTeams = _.difference(newTeamIds, currentScriptTeamIds);
+ let removedTeams = _.difference(currentScriptTeamIds, newTeamIds);
+ let removedTeamsInfo = _.filter(script.teams, (team)=>{
+ return removedTeams.includes(team.fleetApid);
+ });
+ for(let script of removedTeamsInfo){
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/scripts/${script.scriptFleetApid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ for(let teamApid of addedTeams){
+ // Build a request body for the team.
+ let requestBodyForThisTeam = {
+ script: {
+ options: {
+ filename: filename,
+ contentType: 'application/octet-stream'
+ },
+ value: scriptContents,
+ }
+ };
+ let addScriptUrl;
+ // If the script is being added to the "no team" team, then we need to include the team ID of the no team team in the request URL
+ if(Number(teamApid) === 0){
+ addScriptUrl = `/api/v1/fleet/scripts?team_id=${teamApid}`;
+ } else {
+ // Otherwise, the team_id needs to be included in the request's formData.
+ addScriptUrl = `/api/v1/fleet/scripts`;
+ requestBodyForThisTeam.team_id = Number(teamApid);// eslint-disable-line camelcase
+ }
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: addScriptUrl,
+ enctype: 'multipart/form-data',
+ body: requestBodyForThisTeam,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ });
+ }
+ } else {
+ if(script.teams) {
+ for(let scriptId of script.teams){
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/scripts/${scriptId.scriptFleetApid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
+ }
+ }
+ for(let teamApid of newTeamIds){
+ // Build a request body for the team.
+ let requestBodyForThisTeam = {
+ script: {
+ options: {
+ filename: filename,
+ contentType: 'application/octet-stream'
+ },
+ value: scriptContents,
+ }
+ };
+ let addScriptUrl;
+ // If the script is being added to the "no team" team, then we need to include the team ID of the no team team in the request URL
+ if(Number(teamApid) === 0){
+ addScriptUrl = `/api/v1/fleet/scripts?team_id=${teamApid}`;
+ } else {
+ // Otherwise, the team_id needs to be included in the request's formData.
+ addScriptUrl = `/api/v1/fleet/scripts`;
+ requestBodyForThisTeam.team_id = Number(teamApid);// eslint-disable-line camelcase
+ }
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: addScriptUrl,
+ enctype: 'multipart/form-data',
+ body: requestBodyForThisTeam,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ });
+ }
+ }
+
+ // If this profile has an ID, then it is a database record, and we will delete it if it has been deployed to a team.
+ if(script.id && newTeamIds.length > 0){
+ // console.log('Undeployed script has been deployed. deleting DB record!');
+ await UndeployedScript.destroy({id: script.id});
+ } else if(!script.id && newTeamIds.length === 0){
+ // If this is not a database record of a script, and the script is being undeployed from all teams, we'll create a databse record for it.
+ // console.log('Creating database record for a (now) undeployed script!');
+ await UndeployedScript.create({
+ name: script.name,
+ platform: extension === '.ps1' ? 'Windows' : 'macOS & Linux',
+ scriptContents,
+ scriptType: extension,
+ });
+ } else if(script.id && newScript){
+ // If there is a new script that is replacing a database record, update the scriptContents in the database.
+ // console.log('Updating existing undeployed script!');
+ await UndeployedScript.updateOne({id: script.id}).set({
+ scriptContents,
+ });
+ }
+
+ // All done.
+ return;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/scripts/upload-script.js b/ee/bulk-operations-dashboard/api/controllers/scripts/upload-script.js
new file mode 100644
index 0000000000..55265f2fa2
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/scripts/upload-script.js
@@ -0,0 +1,118 @@
+module.exports = {
+
+
+ friendlyName: 'Upload script',
+
+
+ description: 'Uploads a script to the connected Fleet instance',
+
+ files: ['newScript'],
+
+ inputs: {
+
+ newScript: {
+ type: 'ref',
+ description: 'An Upstream with an incoming file upload.',
+ required: true,
+ },
+
+ teams: {
+ type: ['string'],
+ description: 'An array of team IDs that this profile will be added to'
+ }
+ },
+
+
+ exits: {
+ success: {
+ outputDescription: 'The new script has been uploaded',
+ outputType: {},
+ },
+
+ scriptWithThisNameAlreadyExists: {
+ description: 'A script with this name already exists on the Fleet Instance',
+ statusCode: 409,
+ },
+
+ noFileAttached: {
+ description: 'No file was attached.',
+ responseType: 'badRequest'
+ },
+
+ tooBig: {
+ description: 'The file is too big.',
+ responseType: 'badRequest'
+ },
+ },
+
+
+ fn: async function ({newScript, teams}) {
+
+ let util = require('util');
+ let script = await sails.reservoir(newScript)
+ .intercept('E_EXCEEDS_UPLOAD_LIMIT', 'tooBig')
+ .intercept((err)=>new Error('The script upload failed. '+util.inspect(err)));
+ if(!script) {
+ throw 'noFileAttached';
+ }
+ // Get the file contents and filename.
+ let scriptContents = script[0].contentBytes;
+ let scriptFilename = script[0].name;
+ // Strip out any automatically added date prefixes from uploaded scripts.
+ let datelessExtensionlessFilename = scriptFilename.replace(/^\d{4}-\d{2}-\d{2}\s/, '').replace(/\.[^/.]+$/, '');
+ let extension = '.'+scriptFilename.split('.').pop();
+ // Build a dictonary of information about this script to return to the scripts page.
+ let newScriptInfo = {
+ name: datelessExtensionlessFilename,
+ platform: _.endsWith(scriptFilename, '.ps1') ? 'Windows' : 'macOS & Linux',
+ scriptType: extension,
+ createdAt: Date.now()
+ };
+ if(!teams) {
+ newScriptInfo.scriptContents = scriptContents;
+ await UndeployedScript.create(newScriptInfo).fetch();
+ } else {
+ // Send a request to add the script for every team ID in the array of teams.
+ for(let teamApid of teams){
+ // Build a request body for the team.
+ let requestBodyForThisTeam = {
+ script: {
+ options: {
+ filename: datelessExtensionlessFilename + extension,
+ contentType: 'application/octet-stream'
+ },
+ value: scriptContents,
+ }
+ };
+ let addScriptUrl;
+ // If the script is being added to the "no team" team, then we need to include the team ID of the no team team in the request URL
+ if(Number(teamApid) === 0){
+ addScriptUrl = `/api/v1/fleet/scripts?team_id=${teamApid}`;
+ } else {
+ // Otherwise, the team_id needs to be included in the request's formData.
+ addScriptUrl = `/api/v1/fleet/scripts`;
+ requestBodyForThisTeam.team_id = Number(teamApid);// eslint-disable-line camelcase
+ }
+ // Send a PSOT request to add the script.
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'POST',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url:addScriptUrl,
+ enctype: 'multipart/form-data',
+ body: requestBodyForThisTeam,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ },
+ })
+ .intercept({raw: {statusCode: 409}}, ()=>{
+ return 'scriptWithThisNameAlreadyExists';
+ });
+ }
+ }
+ // All done.
+ return newScriptInfo;
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/controllers/scripts/view-scripts.js b/ee/bulk-operations-dashboard/api/controllers/scripts/view-scripts.js
new file mode 100644
index 0000000000..02fabebdd2
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/controllers/scripts/view-scripts.js
@@ -0,0 +1,124 @@
+module.exports = {
+
+
+ friendlyName: 'View scripts',
+
+
+ description: 'Display "Scripts" page.',
+
+
+ exits: {
+
+ success: {
+ viewTemplatePath: 'pages/scripts'
+ }
+
+ },
+
+
+ fn: async function () {
+ let teamsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/teams',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+
+ let allTeams = teamsResponseData.teams;
+
+ let teamApids = _.pluck(allTeams, 'id');
+ let teams = [];
+ for(let team of allTeams) {
+ teams.push({
+ fleetApid: team.id,
+ teamName: team.name,
+ });
+ }
+ // Add the "team" for hosts with no team
+ teams.push({
+ fleetApid: 0,
+ teamName: 'No team',
+ });
+
+ let allScripts = [];
+
+ for(let teamApid of teamApids){
+ let scriptsResponseData = await sails.helpers.http.get.with({
+ url: `/api/v1/fleet/scripts?team_id=${teamApid}`,
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let scriptsForThisTeam = scriptsResponseData.scripts;
+ if(scriptsForThisTeam !== null) {
+ allScripts = allScripts.concat(scriptsForThisTeam);
+ }
+ }
+
+ // Grab all of the configuration scripts on the Fleet instance.
+ let noTeamConfigurationScriptsResponseData = await sails.helpers.http.get.with({
+ url: '/api/v1/fleet/scripts',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ }
+ })
+ .timeout(120000)
+ .retry(['requestFailed', {name: 'TimeoutError'}]);
+ let scriptsForThisTeam = noTeamConfigurationScriptsResponseData.scripts;
+
+ if(scriptsForThisTeam !== null){
+ allScripts = allScripts.concat(scriptsForThisTeam);
+ }
+
+ if(allScripts === [ null ]){
+ return {scripts: [], teams};
+ }
+ let scriptsOnThisFleetInstance = [];
+
+ let allScriptsByIdentifier = _.groupBy(allScripts, 'name');
+ for(let scriptIdentifier in allScriptsByIdentifier) {
+ if(scriptIdentifier === null){
+ continue;
+ }
+ let teamsForThisProfile = [];
+ // console.log(teamsForThisProfile);
+ // let platforms = _.uniq(_.pluck(allScriptsByIdentifier[scriptIdentifier], 'platform'));
+ for(let script of allScriptsByIdentifier[scriptIdentifier]){
+ let informationAboutThisScript = {
+ scriptFleetApid: script.id,
+ fleetApid: script.team_id ? script.team_id : 0,
+ teamName: script.team_id ? _.find(teams, {fleetApid: script.team_id}).teamName : 'No team',
+ };
+ teamsForThisProfile.push(informationAboutThisScript);
+ }
+ let script = allScriptsByIdentifier[scriptIdentifier][0];// Grab the first script returned in the api repsonse to build our script configuration.
+ let scriptInformation = {
+ name: script.name,
+ identifier: scriptIdentifier,
+ platform: _.endsWith(script.name, 'sh') ? 'macOS & Linux' : 'Windows',
+ createdAt: new Date(script.created_at).getTime(),
+ teams: teamsForThisProfile
+ };
+ scriptsOnThisFleetInstance.push(scriptInformation);
+ }
+ // Get the undeployed scripts from the app's database.
+ let undeployedScripts = await UndeployedScript.find();
+ scriptsOnThisFleetInstance = _.union(scriptsOnThisFleetInstance, undeployedScripts);
+
+ // Sort the scripts by name.
+ scriptsOnThisFleetInstance = _.sortByOrder(scriptsOnThisFleetInstance, 'name', 'asc');
+ // Respond with view.
+ return {scripts: scriptsOnThisFleetInstance, teams};
+
+
+ }
+
+
+};
diff --git a/ee/bulk-operations-dashboard/api/helpers/broadcast-session-change.js b/ee/bulk-operations-dashboard/api/helpers/broadcast-session-change.js
new file mode 100644
index 0000000000..f4aa836b3c
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/helpers/broadcast-session-change.js
@@ -0,0 +1,45 @@
+module.exports = {
+
+
+ friendlyName: 'Broadcast session change',
+
+
+ description: 'Broadcast a socket notification indicating a change in login status.',
+
+
+ inputs: {
+
+ req: {
+ type: 'ref',
+ required: true,
+ },
+
+ },
+
+
+ exits: {
+
+ success: {
+ description: 'All done.',
+ },
+
+ },
+
+
+ fn: async function ({ req }) {
+
+ // If there's no sessionID, we don't need to broadcase a message about the old session.
+ if(!req.sessionID) {
+ return;
+ }
+
+ let roomName = `session${_.deburr(req.sessionID)}`;
+ let messageText = `You have signed out or signed into a different session in another tab or window. Reload the page to refresh your session.`;
+ sails.sockets.broadcast(roomName, 'session', { notificationText: messageText }, req);
+
+
+ }
+
+
+};
+
diff --git a/ee/bulk-operations-dashboard/api/helpers/redact-user.js b/ee/bulk-operations-dashboard/api/helpers/redact-user.js
new file mode 100644
index 0000000000..28d9a7c44f
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/helpers/redact-user.js
@@ -0,0 +1,33 @@
+module.exports = {
+
+
+ friendlyName: 'Redact user',
+
+
+ description: 'Destructively remove properties from the provided User record to prepare it for publication.',
+
+
+ sync: true,
+
+
+ inputs: {
+
+ user: {
+ type: 'ref',
+ readOnly: false
+ }
+
+ },
+
+
+ fn: function ({ user }) {
+ for (let [attrName, attrDef] of Object.entries(User.attributes)) {
+ if (attrDef.protect) {
+ delete user[attrName];
+ }//fi
+ }//∞
+ }
+
+
+};
+
diff --git a/ee/bulk-operations-dashboard/api/helpers/send-template-email.js b/ee/bulk-operations-dashboard/api/helpers/send-template-email.js
new file mode 100644
index 0000000000..f03b447462
--- /dev/null
+++ b/ee/bulk-operations-dashboard/api/helpers/send-template-email.js
@@ -0,0 +1,282 @@
+module.exports = {
+
+
+ friendlyName: 'Send template email',
+
+
+ description: 'Send an email using a template.',
+
+
+ extendedDescription: 'To ease testing and development, if the provided "to" email address ends in "@example.com", '+
+ 'then the email message will be written to the terminal instead of actually being sent.'+
+ '(Thanks [@simonratner](https://github.com/simonratner)!)',
+
+
+ inputs: {
+
+
+ template: {
+ description: 'The relative path to an EJS template within our `views/emails/` folder -- WITHOUT the file extension.',
+ extendedDescription: 'Use strings like "foo" or "foo/bar", but NEVER "foo/bar.ejs" or "/foo/bar". For example, '+
+ '"internal/email-contact-form" would send an email using the "views/emails/internal/email-contact-form.ejs" template.',
+ example: 'email-reset-password',
+ type: 'string',
+ required: true
+ },
+
+ templateData: {
+ description: 'A dictionary of data which will be accessible in the EJS template.',
+ extendedDescription: 'Each key will be a local variable accessible in the template. For instance, if you supply '+
+ 'a dictionary with a \`friends\` key, and \`friends\` is an array like \`[{name:"Chandra"}, {name:"Mary"}]\`),'+
+ 'then you will be able to access \`friends\` from the template:\n'+
+ '\`\`\`\n'+
+ '