diff --git a/.github/actions/r2-upload/action.yml b/.github/actions/r2-upload/action.yml
new file mode 100644
index 0000000000..60fb7e0786
--- /dev/null
+++ b/.github/actions/r2-upload/action.yml
@@ -0,0 +1,34 @@
+name: R2 upload
+description: Upload a file to R2
+# Schema: https://json.schemastore.org/github-action.json
+
+# This action expects the following env vars to be set:
+# - R2_ENDPOINT: The endpoint of the R2 instance to upload to
+# - R2_ACCESS_KEY_ID: The access key ID to use for R2
+# - R2_ACCESS_KEY_SECRET: The access key secret to use for R2
+# - R2_BUCKET: The bucket to upload to
+
+inputs:
+ filename:
+ # Future improvement: accept array of filenames as JSON string, and loop over it like in https://www.starkandwayne.com/blog/bash-for-loop-over-json-array-using-jq/index.html
+ description: 'Name of the file to upload'
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Upload file to R2
+ shell: bash
+ run: |
+ sudo ./.github/scripts/rclone-install.sh
+ mkdir -p ~/.config/rclone
+ echo "[r2]
+ type = s3
+ provider = Cloudflare
+ region = auto
+ no_check_bucket = true
+ access_key_id = $R2_ACCESS_KEY_ID
+ secret_access_key = $R2_ACCESS_KEY_SECRET
+ endpoint = $R2_ENDPOINT
+ " > ~/.config/rclone/rclone.conf
+ rclone copy --verbose ${{ inputs.filename }} r2:${R2_BUCKET}/
diff --git a/.github/scripts/rclone-install.sh b/.github/scripts/rclone-install.sh
new file mode 100755
index 0000000000..da6a276253
--- /dev/null
+++ b/.github/scripts/rclone-install.sh
@@ -0,0 +1,212 @@
+#!/usr/bin/env bash
+
+# This script is a modified version of MIT licensed script from https://github.com/rclone/rclone/blob/v1.66.0/docs/content/install.sh
+# The script is used to install rclone on a GitHub Actions runner machine.
+# We use a specific version of rclone for stability/security reasons.
+download_version="v1.66.0"
+
+# error codes
+# 0 - exited without problems
+# 1 - parameters not supported were used or some unexpected error occurred
+# 2 - OS not supported by this script
+# 0 (was 3) - installed version of rclone is up to date
+# 4 - supported unzip tools are not available
+
+set -e
+
+#when adding a tool to the list make sure to also add its corresponding command further in the script
+unzip_tools_list=('unzip' '7z' 'busybox')
+
+usage() { echo "Usage: sudo -v ; curl https://rclone.org/install.sh | sudo bash [-s beta]" 1>&2; exit 1; }
+
+#check for beta flag
+if [ -n "$1" ] && [ "$1" != "beta" ]; then
+ usage
+fi
+
+if [ -n "$1" ]; then
+ install_beta="beta "
+fi
+
+
+#create tmp directory and move to it with macOS compatibility fallback
+tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'rclone-install.XXXXXXXXXX')
+cd "$tmp_dir"
+
+
+#make sure unzip tool is available and choose one to work with
+set +e
+for tool in ${unzip_tools_list[*]}; do
+ trash=$(hash "$tool" 2>>errors)
+ if [ "$?" -eq 0 ]; then
+ unzip_tool="$tool"
+ break
+ fi
+done
+set -e
+
+# exit if no unzip tools available
+if [ -z "$unzip_tool" ]; then
+ printf "\nNone of the supported tools for extracting zip archives (${unzip_tools_list[*]}) were found. "
+ printf "Please install one of them and try again.\n\n"
+ exit 4
+fi
+
+# Make sure we don't create a root owned .config/rclone directory #2127
+export XDG_CONFIG_HOME=config
+
+#check installed version of rclone to determine if update is necessary
+version=$(rclone --version 2>>errors | head -n 1)
+if [ -z "$install_beta" ]; then
+ current_version=$(curl -fsS https://downloads.rclone.org/version.txt)
+else
+ current_version=download_version
+ # current_version=$(curl -fsS https://beta.rclone.org/version.txt)
+fi
+
+if [ "$version" = "$current_version" ]; then
+ printf "\nThe latest ${install_beta}version of rclone ${version} is already installed.\n\n"
+ exit 0 # originally 3
+fi
+
+
+#detect the platform
+OS="$(uname)"
+case $OS in
+ Linux)
+ OS='linux'
+ ;;
+ FreeBSD)
+ OS='freebsd'
+ ;;
+ NetBSD)
+ OS='netbsd'
+ ;;
+ OpenBSD)
+ OS='openbsd'
+ ;;
+ Darwin)
+ OS='osx'
+ binTgtDir=/usr/local/bin
+ man1TgtDir=/usr/local/share/man/man1
+ ;;
+ SunOS)
+ OS='solaris'
+ echo 'OS not supported'
+ exit 2
+ ;;
+ *)
+ echo 'OS not supported'
+ exit 2
+ ;;
+esac
+
+OS_type="$(uname -m)"
+case "$OS_type" in
+ x86_64|amd64)
+ OS_type='amd64'
+ ;;
+ i?86|x86)
+ OS_type='386'
+ ;;
+ aarch64|arm64)
+ OS_type='arm64'
+ ;;
+ armv7*)
+ OS_type='arm-v7'
+ ;;
+ armv6*)
+ OS_type='arm-v6'
+ ;;
+ arm*)
+ OS_type='arm'
+ ;;
+ *)
+ echo 'OS type not supported'
+ exit 2
+ ;;
+esac
+
+
+#download and unzip
+if [ -z "$install_beta" ]; then
+ download_link="https://downloads.rclone.org/${download_version}/rclone-${download_version}-${OS}-${OS_type}.zip"
+ rclone_zip="rclone-${download_version}-${OS}-${OS_type}.zip"
+ # download_link="https://downloads.rclone.org/rclone-current-${OS}-${OS_type}.zip"
+ # rclone_zip="rclone-current-${OS}-${OS_type}.zip"
+else
+ download_link="https://beta.rclone.org/rclone-beta-latest-${OS}-${OS_type}.zip"
+ rclone_zip="rclone-beta-latest-${OS}-${OS_type}.zip"
+fi
+
+curl -OfsS "$download_link"
+unzip_dir="tmp_unzip_dir_for_rclone"
+# there should be an entry in this switch for each element of unzip_tools_list
+case "$unzip_tool" in
+ 'unzip')
+ unzip -a "$rclone_zip" -d "$unzip_dir"
+ ;;
+ '7z')
+ 7z x "$rclone_zip" "-o$unzip_dir"
+ ;;
+ 'busybox')
+ mkdir -p "$unzip_dir"
+ busybox unzip "$rclone_zip" -d "$unzip_dir"
+ ;;
+esac
+
+cd $unzip_dir/*
+
+#mounting rclone to environment
+
+case "$OS" in
+ 'linux')
+ #binary
+ cp rclone /usr/bin/rclone.new
+ chmod 755 /usr/bin/rclone.new
+ chown root:root /usr/bin/rclone.new
+ mv /usr/bin/rclone.new /usr/bin/rclone
+ #manual
+# if ! [ -x "$(command -v mandb)" ]; then
+# echo 'mandb not found. The rclone man docs will not be installed.'
+# else
+# mkdir -p /usr/local/share/man/man1
+# cp rclone.1 /usr/local/share/man/man1/
+# mandb
+# fi
+ ;;
+ 'freebsd'|'openbsd'|'netbsd')
+ #binary
+ cp rclone /usr/bin/rclone.new
+ chown root:wheel /usr/bin/rclone.new
+ mv /usr/bin/rclone.new /usr/bin/rclone
+ #manual
+ mkdir -p /usr/local/man/man1
+ cp rclone.1 /usr/local/man/man1/
+ makewhatis
+ ;;
+ 'osx')
+ #binary
+ mkdir -m 0555 -p ${binTgtDir}
+ cp rclone ${binTgtDir}/rclone.new
+ mv ${binTgtDir}/rclone.new ${binTgtDir}/rclone
+ chmod a=x ${binTgtDir}/rclone
+ #manual
+# mkdir -m 0555 -p ${man1TgtDir}
+# cp rclone.1 ${man1TgtDir}
+# chmod a=r ${man1TgtDir}/rclone.1
+ ;;
+ *)
+ echo 'OS not supported'
+ exit 2
+esac
+
+#update version variable post install
+version=$(rclone --version 2>>errors | head -n 1)
+
+#cleanup
+rm -rf "$tmp_dir"
+
+printf "\n${version} has successfully installed."
+printf '\nNow run "rclone config" for setup. Check https://rclone.org/docs/ for more details.\n\n'
+exit 0
diff --git a/.github/workflows/code-sign-windows.yml b/.github/workflows/code-sign-windows.yml
new file mode 100644
index 0000000000..41a9666677
--- /dev/null
+++ b/.github/workflows/code-sign-windows.yml
@@ -0,0 +1,97 @@
+name: Code sign Windows binaries with DigiCert KeyLocker KSP
+
+on:
+ workflow_call:
+ inputs:
+ filename:
+ description: 'The name of the file to sign'
+ required: true
+ type: string
+ download_name:
+ description: 'The name of the artifact to download'
+ required: false
+ default: 'unsigned-windows'
+ type: string
+ upload_name:
+ description: 'The name of the artifact to upload'
+ required: false
+ default: 'signed-windows'
+ type: string
+ secrets:
+ DIGICERT_KEYLOCKER_CERTIFICATE:
+ required: true
+ DIGICERT_KEYLOCKER_PASSWORD:
+ required: true
+ DIGICERT_KEYLOCKER_HOST_URL:
+ required: true
+ DIGICERT_API_KEY:
+ required: true
+ DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT:
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ code-sign-windows:
+ runs-on: windows-2022
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Download unsigned artifact
+ uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
+ with:
+ name: ${{ inputs.download_name }}
+
+ - name: Setup certificate
+ run: |
+ echo "${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE }}" | base64 --decode > /d/Certificate_pkcs12.p12
+ openssl pkcs12 -in /d/Certificate_pkcs12.p12 -nodes -passin pass:${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }} | openssl x509 -noout -subject
+ shell: bash
+
+ - name: Set variables
+ id: variables
+ run: |
+ echo "SM_HOST=${{ secrets.DIGICERT_KEYLOCKER_HOST_URL }}" >> "$GITHUB_ENV"
+ echo "SM_API_KEY=${{ secrets.DIGICERT_API_KEY }}" >> "$GITHUB_ENV"
+ echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
+ echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }}" >> "$GITHUB_ENV"
+ echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
+ echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
+ echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH
+ shell: bash
+
+ - name: Download Keylocker KSP on windows
+ run: |
+ curl https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" --fail-with-body -o Keylockertools-windows-x64.msi
+ shell: cmd
+
+ - name: Install Keylocker KSP on windows
+ run: |
+ msiexec /i Keylockertools-windows-x64.msi /quiet /qn
+ smksp_registrar.exe list
+ smctl.exe keypair ls
+ C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
+ shell: cmd
+
+ - name: Certificates Sync
+ run: |
+ smctl windows certsync
+ shell: cmd
+
+ - name: Sign using Windows signtool
+ # Debug logs are at: %USERPROFILE%\.signingmanager\logs\smksp.log
+ run: |
+ signtool.exe sign /v /debug /sha1 ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT }} /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ${{ inputs.filename }}
+ copy unsigned.exe signed.exe
+ signtool.exe verify /v /pa ${{ inputs.filename }}
+ shell: cmd
+
+ - name: Upload signed artifact
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
+ with:
+ name: ${{ inputs.upload_name }}
+ path: ${{ inputs.filename }}
diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml
index 1840d48b90..5c03cc3f1f 100644
--- a/.github/workflows/generate-desktop-targets.yml
+++ b/.github/workflows/generate-desktop-targets.yml
@@ -84,7 +84,7 @@ jobs:
make desktop-app-tar-gz
- name: Upload desktop.app.tar.gz
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
name: desktop.app.tar.gz
path: desktop.app.tar.gz
@@ -112,11 +112,24 @@ jobs:
make desktop-windows
- name: Upload fleet-desktop.exe
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
- name: fleet-desktop.exe
+ name: unsigned-windows
path: fleet-desktop.exe
+ code-sign-windows:
+ needs: desktop-windows
+ uses: ./.github/workflows/code-sign-windows.yml
+ with:
+ filename: fleet-desktop.exe
+ upload_name: fleet-desktop.exe
+ secrets:
+ DIGICERT_KEYLOCKER_CERTIFICATE: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE }}
+ DIGICERT_KEYLOCKER_PASSWORD: ${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }}
+ DIGICERT_KEYLOCKER_HOST_URL: ${{ secrets.DIGICERT_KEYLOCKER_HOST_URL }}
+ DIGICERT_API_KEY: ${{ secrets.DIGICERT_API_KEY }}
+ DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT }}
+
desktop-linux:
runs-on: ubuntu-latest
steps:
@@ -140,7 +153,7 @@ jobs:
make desktop-linux
- name: Upload desktop.tar.gz
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
name: desktop.tar.gz
path: desktop.tar.gz
diff --git a/.github/workflows/goreleaser-orbit.yaml b/.github/workflows/goreleaser-orbit.yaml
index 2f1eb3905b..1ce2386dd9 100644
--- a/.github/workflows/goreleaser-orbit.yaml
+++ b/.github/workflows/goreleaser-orbit.yaml
@@ -3,7 +3,7 @@ name: GoReleaser Orbit
on:
push:
tags:
- - 'orbit-*'
+ - '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:
@@ -68,7 +68,7 @@ jobs:
CODESIGN_IDENTITY: 51049B247B25B3119FAE7E9C0CC4375A43E47237
- name: Upload
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
name: orbit-macos
path: dist/orbit-macos_darwin_all/orbit
@@ -101,7 +101,7 @@ jobs:
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux.yml # v1.20.0
- name: Upload
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
name: orbit-linux
path: dist/orbit_linux_amd64_v1/orbit
@@ -134,7 +134,20 @@ jobs:
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-windows.yml # v1.20.0
- name: Upload
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
+ uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # 4.3.3
with:
- name: orbit-windows
+ name: unsigned-windows
path: dist/orbit_windows_amd64_v1/orbit.exe
+
+ code-sign-windows:
+ needs: goreleaser-windows
+ uses: ./.github/workflows/code-sign-windows.yml
+ with:
+ filename: orbit.exe
+ upload_name: orbit-windows
+ secrets:
+ DIGICERT_KEYLOCKER_CERTIFICATE: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE }}
+ DIGICERT_KEYLOCKER_PASSWORD: ${{ secrets.DIGICERT_KEYLOCKER_PASSWORD }}
+ DIGICERT_KEYLOCKER_HOST_URL: ${{ secrets.DIGICERT_KEYLOCKER_HOST_URL }}
+ DIGICERT_API_KEY: ${{ secrets.DIGICERT_API_KEY }}
+ DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT: ${{ secrets.DIGICERT_KEYLOCKER_CERTIFICATE_FINGERPRINT }}
diff --git a/.github/workflows/release-fleetd-base.yml b/.github/workflows/release-fleetd-base.yml
new file mode 100644
index 0000000000..6d3a0e2e61
--- /dev/null
+++ b/.github/workflows/release-fleetd-base.yml
@@ -0,0 +1,177 @@
+name: Upload fleetd base to https://download.fleetdm.com
+
+on:
+ workflow_dispatch: # Manual
+ schedule:
+ - cron: '0 3 * * *' # Nightly 3AM UTC
+
+# 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
+
+env:
+ R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
+ R2_ACCESS_KEY_ID: ${{ secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_ID }} # Production: ${{ secrets.R2_DOWNLOAD_ACCESS_KEY_ID }} | Testing: ${{ secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_ID }}
+ R2_ACCESS_KEY_SECRET: ${{ secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_SECRET }} # Production: ${{ secrets.R2_DOWNLOAD_ACCESS_KEY_SECRET }} | Testing: ${{ secrets.R2_DOWNLOAD_TESTING_ACCESS_KEY_SECRET }}
+ R2_BUCKET: download-testing # Production: download | Testing: download-testing
+ BASE_URL: https://download-testing.fleetdm.com # Production: https://download.fleetdm.com | Testing: https://download-testing.fleetdm.com
+
+jobs:
+ check-for-fleetd-component-updates:
+ runs-on: ubuntu-latest
+ outputs:
+ update_needed: ${{ steps.check-for-fleetd-component-updates.outputs.update_needed }}
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ 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: Check for fleetd component updates
+ id: check-for-fleetd-component-updates
+ run: |
+ go run tools/tuf/status/tuf-status.go channel-version -channel stable --components orbit,desktop,osqueryd --format json > latest-meta.json
+ curl -O $BASE_URL/meta.json
+ if diff latest-meta.json meta.json >/dev/null 2>&1
+ then
+ echo "update_needed=false" >> $GITHUB_OUTPUT
+ else
+ echo "update_needed=true" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Upload latest meta.json artifact
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: latest-meta.json
+ path: latest-meta.json
+
+ update-fleetd-base-pkg:
+ needs: [check-for-fleetd-component-updates]
+ if: needs.check-for-fleetd-component-updates.outputs.update_needed == 'true'
+ runs-on: macos-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout code needed for R2 upload
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ sparse-checkout: |
+ .github/actions/r2-upload/action.yml
+ .github/scripts/rclone-install.sh
+ sparse-checkout-cone-mode: false
+
+ - name: Install fleetctl
+ run: npm install -g fleetctl
+
+ - name: Import package signing keys
+ env:
+ APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }}
+ APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ run: |
+ echo "$APPLE_INSTALLER_CERTIFICATE" | base64 --decode > certificate.p12
+ security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
+ security default-keychain -s build.keychain
+ security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
+ security import certificate.p12 -k build.keychain -P $APPLE_INSTALLER_CERTIFICATE_PASSWORD -T /usr/bin/productsign
+ security set-key-partition-list -S apple-tool:,apple:,productsign: -s -k $KEYCHAIN_PASSWORD build.keychain
+ security find-identity -vv
+ rm certificate.p12
+
+ - name: Build PKG, sign, and notarize
+ env:
+ AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
+ AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ PACKAGE_SIGNING_IDENTITY_SHA1: D52080FD1F0941DE31346F06DA0F08AED6FACBBF
+ run: |
+ fleetctl package --type pkg --fleet-desktop --use-system-configuration --sign-identity $PACKAGE_SIGNING_IDENTITY_SHA1 --notarize
+ mv fleet-osquery*.pkg fleetd-base.pkg
+
+ - name: Upload package
+ uses: ./.github/actions/r2-upload
+ with:
+ filename: fleetd-base.pkg
+
+ update-fleetd-base-msi:
+ needs: [check-for-fleetd-component-updates]
+ if: needs.check-for-fleetd-component-updates.outputs.update_needed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout code needed for R2 upload
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ sparse-checkout: |
+ .github/actions/r2-upload/action.yml
+ .github/scripts/rclone-install.sh
+ sparse-checkout-cone-mode: false
+
+ - name: Install fleetctl
+ run: npm install -g fleetctl
+
+ - name: Build MSI
+ run: |
+ fleetctl package --type msi --fleet-desktop --fleet-url dummy --enroll-secret dummy
+ mv fleet-osquery*.msi fleetd-base.msi
+
+ - name: Upload package
+ uses: ./.github/actions/r2-upload
+ with:
+ filename: fleetd-base.msi
+
+ update-meta-json:
+ needs: [update-fleetd-base-pkg, update-fleetd-base-msi]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Checkout code needed for R2 upload
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ sparse-checkout: |
+ .github/actions/r2-upload/action.yml
+ .github/scripts/rclone-install.sh
+ sparse-checkout-cone-mode: false
+
+ - name: Download latest-meta.json artifact
+ uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+ with:
+ name: latest-meta.json
+
+ - name: Rename latest-meta.json to meta.json
+ run: mv latest-meta.json meta.json
+
+ - name: Upload meta.json
+ uses: ./.github/actions/r2-upload
+ with:
+ filename: meta.json
diff --git a/.github/workflows/test-packaging.yml b/.github/workflows/test-packaging.yml
index 428544fcb0..7190314cb9 100644
--- a/.github/workflows/test-packaging.yml
+++ b/.github/workflows/test-packaging.yml
@@ -42,38 +42,37 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest, macos-latest]
+ # note: in order to test both the wix and the docker flow for msi
+ # packages, this worker needs to run on an x86_64 architecture.
+ # `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:
- # Docker needs to be installed manually on macOS.
- # From https://github.com/docker/for-mac/issues/2359#issuecomment-943131345
- # FIXME: lock Docker version to 4.10.0 as newer versions fail to initialize
- name: Harden Runner
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
with:
egress-policy: audit
- - name: Install Docker
- timeout-minutes: 20
- if: matrix.os == 'macos-latest'
- run: |
- curl -L https://raw.githubusercontent.com/Homebrew/homebrew-cask/c65030146a5cf2070c2499b6c68e2c3495c99731/Casks/docker.rb > docker.rb
- brew install --cask docker.rb
- sudo /Applications/Docker.app/Contents/MacOS/Docker --unattended --install-privileged-components
- open -a /Applications/Docker.app --args --unattended --accept-license
- echo "Waiting for Docker to start up..."
- while ! /Applications/Docker.app/Contents/Resources/bin/docker info &>/dev/null; do
- sleep 1;
- done
- echo "Docker is ready."
-
- 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: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
@@ -83,7 +82,7 @@ jobs:
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Install wine and wix
- if: matrix.os == 'macos-latest'
+ 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
@@ -124,5 +123,5 @@ jobs:
run: ./build/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- name: Build MSI (using local Wix)
- if: matrix.os == 'macos-latest'
+ 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/CHANGELOG.md b/CHANGELOG.md
index 0ad5bc1acb..2d5d86e114 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,58 @@
+## Fleet 4.49.1 (Apr 26, 2024)
+
+### Bug fixes
+
+* Fixed a bug that prevented the Fleet server from starting if Windows MDM was configured but Apple MDM wasn't.
+
+## Fleet 4.49.0 (Apr 24, 2024)
+
+### Endpoint operations
+
+- Added integration with Google Calendar for policy compliance events.
+- Added new API endpoints to add/remove manual labels to/from a host.
+- Updated the `POST /api/v1/fleet/labels` and `PATCH /api/v1/fleet/labels/{id}` endpoints to support creation and update of manual labels.
+- Implemented changes in `fleetctl gitops` for batch processing queries and policies.
+- Enabled setting host status webhook at the team level via REST API and fleetctl apply/gitops.
+
+### Device management (MDM)
+
+- Added API functionality for creating DDM declarations, both individually and as a batch.
+- Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed.
+- Updated `fleetctl run-script` to include new `--team` and `--script-name` flags.
+- Displayed disk encryption status in macOS as "verifying" while verifying the escrowed key.
+- Added the `enable_release_device_manually` configuration setting for teams and no team, which controls the automatic release of a macOS DEP-enrolled device.
+
+### Vulnerability management
+
+- Ignored Valve Corporation's Steam client's vulnerabilities on Windows and macOS due to retrieval challenges of the true version.
+- Updated the GET fleet/os_versions and GET fleet/os_versions/[id] to restrict team users from accessing os versions on hosts from other teams.
+
+### Bug fixes and improvements
+
+- Upgraded Golang version to 1.21.7.
+- Added a minimum supported node version in the `package.json`.
+- Made block_id mismatch errors more informative as 400s instead of 500s.
+- Added Windows MDM support to the `osquery-perf` host-simulation command.
+- Updated calendar events automations to not show error validation on enabling the feature.
+- Migrated MDM-related endpoints to new paths while maintaining support for old endpoints indefinitely.
+- Added a missing database index to the MDM Windows enrollments table to improve performance at scale.
+- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
+- Fixed a bug where Microsoft Edge was not reporting vulnerabilities.
+- Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration.
+- Fixed the error message to indicate if a conflict on uploading an Apple profile was caused by the profile's name or its identifier.
+- Fixed license checks to allow migration and restoring DEP devices during trial.
+- Fixed a 500 error in MySQL 8 and when DB user has insufficient privileges for `fleetctl debug db-locks` and `fleetctl debug db-innodb-status`.
+- Fixed a bug where values not derived from "actual" fleetd-chrome tables were not being displayed correctly.
+- Fixed a bug where values were not being rendered in host-specific query reports.
+- Fixed an issue with automatic release of the device after setup when a DDM profile is pending.
+- Fixed UI issues: alignment bugs, padding around empty states, tooltip rendering, and incorrect rendering of the global Host status expiry settings page.
+- Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500 error.
+- Fixed an issue where a bad request response from a 3rd party MDM solution would result in a 500 error in Fleet during MDM migration.
+- Fixed a bug where updating policy name could result in multiple policies with the same name in a team.
+- Fixed potential server panic when events are created with calendar integration, but then global calendar integration is disabled.
+- Fixed fleetctl gitops dry-run validation issues when enabling calendar integration for the first time.
+- Fixed a bug where all Windows MDM enrollments were detected as automatic.
+
## Fleet 4.48.3 (Apr 16, 2024)
### Bug fixes
diff --git a/CODEOWNERS b/CODEOWNERS
index b1a4d0f957..81aa6a12bf 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -54,9 +54,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
-/charts/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster
-/terraform/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster
+/infrastructure/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
+/charts/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
+/terraform/ @rfairburn @ksatter @lukeheath @edwardsb @pacamaster @georgekarrv
/it-and-security/ @noahtalerman @lukeheath
##############################################################################################
diff --git a/Dockerfile b/Dockerfile
index c05bdff5e5..fb9e439674 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1
+FROM alpine:3.19.1@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
LABEL maintainer="Fleet Developers"
RUN apk --update add ca-certificates
diff --git a/Makefile b/Makefile
index 226410def1..8547479853 100644
--- a/Makefile
+++ b/Makefile
@@ -325,7 +325,9 @@ changelog-orbit:
sh -c "git rm orbit/changes/*"
changelog-chrome:
- sh -c "find ee/fleetd-chrome/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md"
+ $(eval TODAY_DATE := $(shell date "+%b %d, %Y"))
+ @echo -e "## fleetd-chrome $(version) ($(TODAY_DATE))\n" > new-CHANGELOG.md
+ sh -c "find ee/fleetd-chrome/changes -type file | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' >> new-CHANGELOG.md"
sh -c "cat new-CHANGELOG.md ee/fleetd-chrome/CHANGELOG.md > tmp-CHANGELOG.md && rm new-CHANGELOG.md && mv tmp-CHANGELOG.md ee/fleetd-chrome/CHANGELOG.md"
sh -c "git rm ee/fleetd-chrome/changes/*"
diff --git a/articles/enhancing-fleets-vulnerability-management-with-vulncheck-integration.md b/articles/enhancing-fleets-vulnerability-management-with-vulncheck-integration.md
new file mode 100644
index 0000000000..ccb5d6eea9
--- /dev/null
+++ b/articles/enhancing-fleets-vulnerability-management-with-vulncheck-integration.md
@@ -0,0 +1,42 @@
+# Enhancing Fleet's vulnerability management with VulnCheck integration
+
+
+
+Staying ahead of threats is paramount. For Fleet users, this means having the right tools and ensuring they are optimized for performance and efficiency. We're excited to include VulnCheck for centralized Common Platform Enumeration (CPE) data.
+
+IT administrators and CISOs have long found managing CPE data across diverse environments challenging. The sheer volume of information and the need for accuracy and timeliness can strain resources and compromise security. Recognizing this, Fleet partnered with VulnCheck for a solution to streamline CPE data management and enhance security workflows.
+
+
+### CPE data
+
+Common Platform Enumeration is a vital component in identifying and mitigating vulnerabilities. CPEs provide a standardized nomenclature for describing and identifying the software applications and operating systems affected by specific vulnerabilities. With the National Vulnerability Database (NVD) sometimes lagging in attaching CPEs to recent vulnerabilities—some published over a month late—Fleet users face the dilemma of potential blind spots in their vulnerability detection mechanisms. Since Fleet relies on CPEs to match software inventory collected via osquery to CVEs, any delay or absence in CPE data from the NVD directly impacts Fleet's ability to accurately detect and address CVEs, posing significant implications for performance and security.
+
+
+### Introducing VulnCheck
+
+At its core, VulnCheck acts as a reliable bridge to the NIST National Vulnerability Database (NVD), ensuring persistent and accurate connections to the latest CVE data. Leveraging its high-performance API and downloadable CVE data, VulnCheck offers Fleet users unparalleled access to up-to-date vulnerability information. What sets VulnCheck apart is its integration of NVD++, a community-driven initiative to enhance the reliability and accessibility of NVD data. By tapping into NVD++, VulnCheck provides Fleet users with a comprehensive repository of CPE data, regardless of delays in the official NVD feed. This centralized approach to CPE data management streamlines the vulnerability detection process within Fleet, empowering IT administrators to identify and remediate security threats swiftly. The benefits of integrating VulnCheck into Fleet are manifold: enhanced accuracy in vulnerability detection, improved timeliness of threat response, and, ultimately, strengthened security posture for organizations of all sizes. This strategic partnership fortifies organizations' security posture and empowers administrators with the tools to manage and mitigate potential threats proactively.
+
+
+### Enhanced Data Reliability with VulnCheck
+
+The integration of VulnCheck significantly bolsters the reliability of the data Fleet uses for vulnerability management. Recently, the National Vulnerability Database (NVD) faced disruptions that affected its ability to enrich CVEs with crucial matching data, leading to potential vulnerabilities remaining undetected. This is where VulnCheck steps in with its NVD++ service. By enriching its feeds independently, VulnCheck offers a more consistent and reliable data source, ensuring that Fleet users do not experience gaps in vulnerability detection. This is especially critical when the NVD data pipeline faces lags or interruptions, as was seen with several CVEs pending analysis and lacking essential software matching data.
+
+
+### Streamlining Vulnerability Management
+
+The practical implications of integrating VulnCheck's enriched data are profound. For instance, the initial synchronization time for setting up vulnerability management in Fleet has been substantially reduced. Where it previously took about 17 minutes to load NVD data—a considerable delay for new users—this process is now more efficient thanks to enriched data from VulnCheck and pre-processing by Fleet. Moreover, the reliability of data fetch operations has improved with fewer retries needed, mitigating the impact of NVD API's occasional unavailability. This enhancement allows Fleet to offer more immediate value to new users, significantly shortening the time to useful data and enabling faster, more reliable vulnerability scanning.
+
+
+### Conclusion
+
+Staying ahead of threats with efficient tools and systems is not just an option but a necessity. Fleet and VulnCheck introduce a robust solution tailored to enhance the security frameworks of diverse organizations. This integration alleviates the strains of managing large volumes of security data by centralizing and streamlining CPE data management. It ensures that Fleet users can rely on up-to-date and accurate vulnerability information. Incorporating VulnCheck’s NVD++ service into Fleet’s vulnerability management process significantly advances our ability to offer timely and effective security responses, minimizing potential exposure to threats. As we continue to enhance our capabilities, Fleet remains committed to providing our users with the most reliable and efficient tools necessary to safeguard their digital environments against the ever-changing threat landscape.
+
+
+
+
+
+
+
+
+
+
diff --git a/articles/fleet-4.49.0.md b/articles/fleet-4.49.0.md
new file mode 100644
index 0000000000..bda46c13e2
--- /dev/null
+++ b/articles/fleet-4.49.0.md
@@ -0,0 +1,140 @@
+# Fleet 4.49.0 | VulnCheck's NVD++, device health API, `fleetd` data parsing.
+
+
+
+Fleet 4.49.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.49.0) or continue reading to get the highlights.
+For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs.
+
+## Highlights
+
+* Enhancing Fleet's vulnerability management with VulnCheck integration
+* Device health API includes critical policy and resolution data
+* `fleetd` data parsing expansion
+* Apply labels using UI or API
+* Resend configuration profiles
+
+
+
+### Enhancing Fleet's vulnerability management with VulnCheck integration
+
+Fleet is integrating VulnCheck to enhance our vulnerability management capabilities, ensuring our users can manage Common Platform Enumeration (CPE) data more effectively and securely. Utilizing VulnCheck's NVD++ service, Fleet will provide reliable, timely access to vulnerability data, overcoming delays and inconsistencies in the National Vulnerability Database (NVD). This integration improves the accuracy and timeliness of threat detection and streamlines the overall vulnerability management process, empowering IT administrators to identify and mitigate security threats swiftly. Learn more about how this enhancement strengthens Fleet's security framework in our latest blog post: [Enhancing Fleet's Vulnerability Management with VulnCheck Integration](https://fleetdm.com/announcements/enhancing-fleets-vulnerability-management-with-vulncheck-integration).
+
+
+### Device health API includes critical policy and resolution data
+
+Fleet has updated its device health API to include critical and policy resolution data, enhancing the utility of this API for specific workflow conditions where compliance verification is essential before proceeding. This update allows for real-time authentication checks to ensure a host complies with set policies, thereby supporting secure and compliant operational workflows. By integrating critical compliance data into the device health API, Fleet enables administrators to enforce and verify security policies efficiently, ensuring that only compliant devices proceed in sensitive or critical operations. This enhancement supports thorough compliance management and reinforces secure practices within IT environments, streamlining processes where policy adherence is crucial.
+
+
+### `fleetd` data parsing expansion
+
+Fleet's agent (`fleetd`) has expanded its data parsing capabilities by adding support for JSON, JSONL, XML, and INI file formats as tables. This functionality allows for more versatile data extraction and management, enabling users to convert these popular data formats directly into queryable tables. This capability is particularly useful for IT and security teams who need to analyze and monitor configuration and data files across various systems within their digital environments efficiently. By facilitating integration and manipulation of data from these diverse formats, Fleet helps ensure that teams can maintain better oversight and faster responsiveness when managing operational and security needs. This feature is a natural extension of Fleet's ongoing efforts to empower IT professionals with comprehensive tools for robust data handling and security management.
+
+
+### Apply labels using UI or API
+
+Fleet has expanded the flexibility of label management by enabling users to add labels manually through both the UI and API. This capability was previously available only via the CLI. This enhancement allows administrators to more conveniently categorize and manage hosts directly within the user interface or programmatically via the API, aligning with various operational workflows. By streamlining the label application process, Fleet makes it easier for teams to organize and access host data according to specific criteria, thereby improving operational efficiency and responsiveness. This update supports better integration and automation capabilities within IT environments, empowering users to maintain organized and effective device management practices.
+
+
+### Resend configuration profiles
+
+Fleet has introduced a new feature that allows users to resend a configuration profile to a host, which is crucial for maintaining current settings and certificates. This functionality is particularly beneficial in scenarios where renewing SCEP certificates, signing certificates need updating, or reapplication of existing configurations is required to ensure continuity and compliance. By enabling the reissuance of configuration profiles directly from the platform, Fleet supports continuous device management and security upkeep, facilitating a proactive approach to maintaining and securing digital environments. This feature enhances Fleet's utility for administrators by simplifying the management of device configurations.
+
+
+
+## Changes
+
+### Endpoint operations
+
+- Added integration with Google Calendar for policy compliance events.
+- Added new API endpoints to add/remove manual labels to/from a host.
+- Updated the `POST /api/v1/fleet/labels` and `PATCH /api/v1/fleet/labels/{id}` endpoints to support creation and update of manual labels.
+- Implemented changes in `fleetctl gitops` for batch processing queries and policies.
+- Enabled setting host status webhook at the team level via REST API and fleetctl apply/gitops.
+
+### Device management (MDM)
+
+- Added API functionality for creating DDM declarations, both individually and as a batch.
+- Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed.
+- Updated `fleetctl run-script` to include new `--team` and `--script-name` flags.
+- Displayed disk encryption status in macOS as "verifying" while verifying the escrowed key.
+- Added the `enable_release_device_manually` configuration setting for teams and no team, which controls the automatic release of a macOS DEP-enrolled device.
+- Updated the `POST /api/v1/fleet/hosts/:id/wipe` Fleet Premium API endpoint to support remote wiping a host.
+- Added the `enable_release_device_manually` configuration, which affects macOS automatic enrollment profile settings.
+
+### Vulnerability management
+
+- Ignored Valve Corporation's Steam client's vulnerabilities on Windows and macOS due to retrieval challenges of the true version.
+- Updated the GET fleet/os_versions and GET fleet/os_versions/[id] to restrict team users from accessing os versions on hosts from other teams.
+
+### Bug fixes and improvements
+
+- Upgraded Golang version to 1.21.7.
+- Added a minimum supported node version in the `package.json`.
+- Made block_id mismatch errors more informative as 400s instead of 500s.
+- Added Windows MDM support to the `osquery-perf` host-simulation command.
+- Updated calendar events automations to not show error validation on enabling the feature.
+- Migrated MDM-related endpoints to new paths while maintaining support for old endpoints indefinitely.
+- Added a missing database index to the MDM Windows enrollments table to improve performance at scale.
+- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
+- Fixed a bug where Microsoft Edge was not reporting vulnerabilities.
+- Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration.
+- Fixed the error message to indicate if a conflict on uploading an Apple profile was caused by the profile's name or its identifier.
+- Fixed license checks to allow migration and restoring DEP devices during trial.
+- Fixed a 500 error in MySQL 8 and when DB user has insufficient privileges for `fleetctl debug db-locks` and `fleetctl debug db-innodb-status`.
+- Fixed a bug where values not derived from "actual" fleetd-chrome tables were not being displayed correctly.
+- Fixed a bug where values were not being rendered in host-specific query reports.
+- Fixed an issue with automatic release of the device after setup when a DDM profile is pending.
+- Fixed UI issues: alignment bugs, padding around empty states, tooltip rendering, and incorrect rendering of the global Host status expiry settings page.
+- Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500 error.
+- Fixed an issue where a bad request response from a 3rd party MDM solution would result in a 500 error in Fleet during MDM migration.
+- Fixed a bug where updating policy name could result in multiple policies with the same name in a team.
+- Fixed potential server panic when events are created with calendar integration, but then global calendar integration is disabled.
+- Fixed fleetctl gitops dry-run validation issues when enabling calendar integration for the first time.
+- Fixed a bug where all Windows MDM enrollments were detected as automatic.
+
+## Fleet 4.48.3 (Apr 16, 2024)
+
+### Bug fixes
+
+* Updated calendar webhook to retry if it receives response 429 "Too Many Requests". Webhook request will retry for 30 minutes with a 1 minute max delay between retries.
+* Updated label endpoints and UI to prevent creating, updating, or deleting built-in labels.
+* Fixed edge cases of team ID being lost in various flows.
+* Fixed queries to correctly parse params for `GET` ...`policies/count`, `GET` ...`teams/:id/policies/count`, and `GET` ...`vulnerabilities`.
+* Fixed 'GET` ...`labels` to return `400` when the non-supported `query` url param was included in the request. Previous behavior was to silently ignore that param and return `200`.
+* Casted windows exit codes to signed integers to match windows interpreter.
+* Fixed a bug where some scripts got stuck in "upcoming" activity permanently.
+* Fixed a bug where the translate API returned "forbidden" instead of "bad request" for an empty JSON body.
+* Fixed an uncaught bug where "forbidden" would be returned for invalid payload type, which should also be a bad request.
+* Fixed an issue where applying Windows MDM profiles using `fleetctl apply` would cause Fleet to overwrite the reserved profile used to manage Windows OS updates.
+* Fixed a bug where we were not ignoreing leading and trailing whitespace when filtering Fleet entities by name.
+* Fixed a bug where query retrieving bitlocker info from windows server wouldn't return.
+* Fixed MDM migration starting when the device didn't have the right ADE JSON profile already assigned.
+
+## Fleet 4.48.2 (Apr 09, 2024)
+
+### Bug fixes
+
+* Fixed an issue with the `20240327115617_CreateTableNanoDDMRequests` database migration where it could fail if the database did not default to the `utf8mb4_unicode_ci` collation.
+* Fixed an issue with automatic release of the device after setup when a DDM profile is pending.
+
+## Fleet 4.48.1 (Apr 08, 2024)
+
+### Bug fixes
+
+- Made block_id mismatch errors more informative as 400s instead of 500s
+- Fixed a bug where values were not being rendered in host-specific query reports
+- Fixed potential server panic when events are created with calendar integration, but then global calendar integration is disabled
+
+
+
+
+## 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.49.0.
+
+
+
+
+
+
+
diff --git a/changes/12290-run-query-on-host b/changes/12290-run-query-on-host
deleted file mode 100644
index a2459e28e0..0000000000
--- a/changes/12290-run-query-on-host
+++ /dev/null
@@ -1 +0,0 @@
-- UI revamp: Run query on an online host
diff --git a/changes/12292-policies-filter-by-platform b/changes/12292-policies-filter-by-platform
deleted file mode 100644
index dbc31fab33..0000000000
--- a/changes/12292-policies-filter-by-platform
+++ /dev/null
@@ -1 +0,0 @@
-* Add filters by platform to select a new policy modal
\ No newline at end of file
diff --git a/changes/15565-windows-automatic-enrollment b/changes/15565-windows-automatic-enrollment
deleted file mode 100644
index a89e709468..0000000000
--- a/changes/15565-windows-automatic-enrollment
+++ /dev/null
@@ -1 +0,0 @@
-- Fix a bug where all Windows MDM enrollments were detected as automatic
diff --git a/changes/16120-add-windows-mdm-support-to-osquery-perf b/changes/16120-add-windows-mdm-support-to-osquery-perf
deleted file mode 100644
index a8ebd32ce7..0000000000
--- a/changes/16120-add-windows-mdm-support-to-osquery-perf
+++ /dev/null
@@ -1,2 +0,0 @@
-* Added Windows MDM support to the `osquery-perf` host-simulation command.
-* Added a missing database index to the MDM Windows enrollments table that will improve performance at scale.
diff --git a/changes/16205-health-failing-counts b/changes/16205-health-failing-counts
deleted file mode 100644
index df792a3fa6..0000000000
--- a/changes/16205-health-failing-counts
+++ /dev/null
@@ -1 +0,0 @@
-- The Host Health API now includes failing policy counts
\ No newline at end of file
diff --git a/changes/16260-recategorize-mdm-api-endpoints b/changes/16260-recategorize-mdm-api-endpoints
deleted file mode 100644
index cdc03d0933..0000000000
--- a/changes/16260-recategorize-mdm-api-endpoints
+++ /dev/null
@@ -1 +0,0 @@
-* Migrate MDM-related endpoints to new paths, deprecating (but still supporting indefinitely) the old endpoints.
diff --git a/changes/16345-disabled-checkbox-tooltip b/changes/16345-disabled-checkbox-tooltip
deleted file mode 100644
index 5e83ded1e1..0000000000
--- a/changes/16345-disabled-checkbox-tooltip
+++ /dev/null
@@ -1 +0,0 @@
-- UI fix: users can see a tooltip on a disabled checkbox
diff --git a/changes/16500-policy-pass-fail-percentage b/changes/16500-policy-pass-fail-percentage
deleted file mode 100644
index bc93d8227f..0000000000
--- a/changes/16500-policy-pass-fail-percentage
+++ /dev/null
@@ -1 +0,0 @@
-* When a live policy run finishes, display the percentages of passing and failing hosts to the user.
diff --git a/changes/16562-deadlock b/changes/16562-deadlock
new file mode 100644
index 0000000000..16675fd8c5
--- /dev/null
+++ b/changes/16562-deadlock
@@ -0,0 +1 @@
+Updated MySQL host_operating_system insert statement to reduce table lock time and optimize performance for the common case.
diff --git a/changes/16562-sql-deadlock b/changes/16562-sql-deadlock
deleted file mode 100644
index c4c725e435..0000000000
--- a/changes/16562-sql-deadlock
+++ /dev/null
@@ -1 +0,0 @@
-Reduced the number of 'Deadlock found' errors seen by the server when multiple hosts share the same UUID
diff --git a/changes/16562-sql-deadlock copy b/changes/16562-sql-deadlock copy
deleted file mode 100644
index c4c725e435..0000000000
--- a/changes/16562-sql-deadlock copy
+++ /dev/null
@@ -1 +0,0 @@
-Reduced the number of 'Deadlock found' errors seen by the server when multiple hosts share the same UUID
diff --git a/changes/16661-current-instance-checks b/changes/16661-current-instance-checks
deleted file mode 100644
index 9d03a9ca3a..0000000000
--- a/changes/16661-current-instance-checks
+++ /dev/null
@@ -1 +0,0 @@
-vulnerabilities.current_instance_checks=no is now an alias for vulnerabilities.disable_schedule=true
diff --git a/changes/16767-updating-host-labels b/changes/16767-updating-host-labels
deleted file mode 100644
index 32c1e635cc..0000000000
--- a/changes/16767-updating-host-labels
+++ /dev/null
@@ -1 +0,0 @@
-* Added endpoints to add/remove manual labels to/from a host. `POST /api/v1/fleet/hosts/:id/labels` and `DELETE /api/v1/fleet/hosts/:id/labels`.
diff --git a/changes/16817-ms-edge-vuln b/changes/16817-ms-edge-vuln
deleted file mode 100644
index 56b7664368..0000000000
--- a/changes/16817-ms-edge-vuln
+++ /dev/null
@@ -1 +0,0 @@
-- Fixed issue where microsoft edge was not reporting vulnerabilities
\ No newline at end of file
diff --git a/changes/16951-improve-carve-request-timeout-error-code b/changes/16951-improve-carve-request-timeout-error-code
deleted file mode 100644
index c23c1bb466..0000000000
--- a/changes/16951-improve-carve-request-timeout-error-code
+++ /dev/null
@@ -1 +0,0 @@
-* Made block_id mismatch errors more informative as 400s instead of 500s.
diff --git a/changes/16989-delete-activities b/changes/16989-delete-activities
new file mode 100644
index 0000000000..b90414df7e
--- /dev/null
+++ b/changes/16989-delete-activities
@@ -0,0 +1 @@
+- Added flag to enable deletion of old activities and associated data in cleanup cron job (`activity_expiry_settings.activity_expiry_enabled` and `activity_expiry_settings.activity_expiry_window`). The cleanup cron job deletes up to 5000 expired activities on each hourly run (thus, up to ~120,000 expired activities are cleaned up a day).
diff --git a/changes/16989-ui-to-delete-old-activities b/changes/16989-ui-to-delete-old-activities
new file mode 100644
index 0000000000..6897b0212a
--- /dev/null
+++ b/changes/16989-ui-to-delete-old-activities
@@ -0,0 +1 @@
+- Add advanced setting to set expiry window for activity log
diff --git a/changes/17003-ingest-vscode_extensions b/changes/17003-ingest-vscode_extensions
deleted file mode 100644
index a8ffcdf7ef..0000000000
--- a/changes/17003-ingest-vscode_extensions
+++ /dev/null
@@ -1 +0,0 @@
-* Visual Studio extensions added to Fleet's software inventory.
diff --git a/changes/17018-reset-query-report b/changes/17018-reset-query-report
deleted file mode 100644
index 444fac3f8b..0000000000
--- a/changes/17018-reset-query-report
+++ /dev/null
@@ -1 +0,0 @@
-- Query report is reset when there is a change to the selected platform or selected minimum osquery version
diff --git a/changes/17061-homebrew-python b/changes/17061-homebrew-python
deleted file mode 100644
index bf76e59e02..0000000000
--- a/changes/17061-homebrew-python
+++ /dev/null
@@ -1 +0,0 @@
-Fixing false negative vulnerabilities on macOS Homebrew python packages.
diff --git a/changes/17065-null-smtp_settings b/changes/17065-null-smtp_settings
deleted file mode 100644
index b37de25553..0000000000
--- a/changes/17065-null-smtp_settings
+++ /dev/null
@@ -1 +0,0 @@
-- Fix a bug where `null` or excluded `smtp_settings` caused a UI 500.
diff --git a/changes/17208-hover-states b/changes/17208-hover-states
deleted file mode 100644
index 5ae0c7f17a..0000000000
--- a/changes/17208-hover-states
+++ /dev/null
@@ -1 +0,0 @@
-Fleet UI: Add hover states to clickable elements
diff --git a/changes/17230-fleet-in-your-calendar b/changes/17230-fleet-in-your-calendar
deleted file mode 100644
index 299239a074..0000000000
--- a/changes/17230-fleet-in-your-calendar
+++ /dev/null
@@ -1,5 +0,0 @@
-Added integration with Google Calendar.
-- Fleet admins can enable Google Calendar integration by using a Google service account with domain-wide delegation.
-- Calendar integration is enabled at the team level for specific team policies.
-- If the policy is failing, a calendar event will be put on the host user's calendar for the 3rd Tuesday of the month.
-- During the event, Fleet will fire a webhook. IT admins should use this webhook to trigger a script or MDM command that will remediate the issue.
diff --git a/changes/17264-batch-process-gitops b/changes/17264-batch-process-gitops
deleted file mode 100644
index cfa7ce9776..0000000000
--- a/changes/17264-batch-process-gitops
+++ /dev/null
@@ -1 +0,0 @@
-- `fleetctl gitops` now batch processes queries and policies
\ No newline at end of file
diff --git a/changes/17265-filter-alignment b/changes/17265-filter-alignment
deleted file mode 100644
index a27c775810..0000000000
--- a/changes/17265-filter-alignment
+++ /dev/null
@@ -1 +0,0 @@
-* Fix a small alignment bug
diff --git a/changes/17288-fix-sort-of-sql-results b/changes/17288-fix-sort-of-sql-results
deleted file mode 100644
index ededd089b4..0000000000
--- a/changes/17288-fix-sort-of-sql-results
+++ /dev/null
@@ -1 +0,0 @@
-* UI fix of sql result sort for both string and numerical columns on live query results, live policy results, and query report
\ No newline at end of file
diff --git a/changes/17308-script-content-cleanup b/changes/17308-script-content-cleanup
deleted file mode 100644
index c51a6933e3..0000000000
--- a/changes/17308-script-content-cleanup
+++ /dev/null
@@ -1,3 +0,0 @@
-- Adds a migration that removes the `script_contents` columns that aren't needed anymore due to the
- introduction of the `script_contents` table
-- Adds a cleanup cron job that will remove unused script contents periodically
\ No newline at end of file
diff --git a/changes/17313-add-env-from-secret-capability-to-helm b/changes/17313-add-env-from-secret-capability-to-helm
deleted file mode 100644
index 9f052ff03a..0000000000
--- a/changes/17313-add-env-from-secret-capability-to-helm
+++ /dev/null
@@ -1 +0,0 @@
-- add env from secret/cm capability to helm charts
\ No newline at end of file
diff --git a/changes/17347-team-user-os-version-restrict b/changes/17347-team-user-os-version-restrict
deleted file mode 100644
index 49d0bb6a6e..0000000000
--- a/changes/17347-team-user-os-version-restrict
+++ /dev/null
@@ -1 +0,0 @@
-For GET fleet/os_versions and GET fleet/os_versions/[id], team users no longer have access to os versions on hosts from other teams.
diff --git a/changes/17360-better-url-email-validators b/changes/17360-better-url-email-validators
new file mode 100644
index 0000000000..079733c24e
--- /dev/null
+++ b/changes/17360-better-url-email-validators
@@ -0,0 +1 @@
+- UI: Improve URL and email validation
\ No newline at end of file
diff --git a/changes/17361-host-details-updates b/changes/17361-host-details-updates
deleted file mode 100644
index 7f3235f2af..0000000000
--- a/changes/17361-host-details-updates
+++ /dev/null
@@ -1 +0,0 @@
-- UI: Surface fleet desktop and orbit version to the host details page
diff --git a/changes/17362-orbit-and-desktop-version b/changes/17362-orbit-and-desktop-version
deleted file mode 100644
index c681b7644e..0000000000
--- a/changes/17362-orbit-and-desktop-version
+++ /dev/null
@@ -1 +0,0 @@
-In GET fleet/hosts/:id response, added orbit_version, fleet_desktop_version, and scripts_enabled fields.
diff --git a/changes/17401-add-enable-release-device-manually b/changes/17401-add-enable-release-device-manually
deleted file mode 100644
index 4fcda2283c..0000000000
--- a/changes/17401-add-enable-release-device-manually
+++ /dev/null
@@ -1,2 +0,0 @@
-* Added the `enable_release_device_manually` configuration setting for a team and no team. **Note** that the macOS automatic enrollment profile cannot set the `await_device_configured` option anymore, this setting is controlled by Fleet via the new `enable_release_device_manually` option.
-* Automatically release a macOS DEP-enrolled device after enrollment commands and profiles have been delivered, unless `enable_release_device_manually` is set to `true`.
diff --git a/changes/17404-mdm-custom-settings b/changes/17404-mdm-custom-settings
deleted file mode 100644
index 78b0506bd0..0000000000
--- a/changes/17404-mdm-custom-settings
+++ /dev/null
@@ -1 +0,0 @@
-- Adds API functionality for creating DDM declarations, both individually and as a batch.
\ No newline at end of file
diff --git a/changes/17418-macos-14-nudge b/changes/17418-macos-14-nudge
deleted file mode 100644
index cdf29816b9..0000000000
--- a/changes/17418-macos-14-nudge
+++ /dev/null
@@ -1 +0,0 @@
-* macOS 14 and higher no longer display nudge notifications
diff --git a/changes/17420-update-ddm-profile-os-updates b/changes/17420-update-ddm-profile-os-updates
deleted file mode 100644
index 54188ff7a2..0000000000
--- a/changes/17420-update-ddm-profile-os-updates
+++ /dev/null
@@ -1 +0,0 @@
-* Added creation or update of macOS DDM profile to enforce OS Updates settings whenever the settings are changed.
diff --git a/changes/17534-improve-error-states-org-settings b/changes/17534-improve-error-states-org-settings
deleted file mode 100644
index 6fdff36d01..0000000000
--- a/changes/17534-improve-error-states-org-settings
+++ /dev/null
@@ -1,2 +0,0 @@
-- Fix error state rendering on the global Host status expiry settings page, fix error state
- alignment for tooltip-wrapper field labels across organization settings.
diff --git a/changes/17557-ui-mdm-off-tooltip b/changes/17557-ui-mdm-off-tooltip
deleted file mode 100644
index c71a1dc8cb..0000000000
--- a/changes/17557-ui-mdm-off-tooltip
+++ /dev/null
@@ -1 +0,0 @@
-- Removed outdated tooltips from UI.
\ No newline at end of file
diff --git a/changes/17559-batch-set-duplicate-mdm b/changes/17559-batch-set-duplicate-mdm
deleted file mode 100644
index f037326fff..0000000000
--- a/changes/17559-batch-set-duplicate-mdm
+++ /dev/null
@@ -1 +0,0 @@
-- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
diff --git a/changes/17562-windows-server-2019-os-details b/changes/17562-windows-server-2019-os-details
deleted file mode 100644
index e3aa773a03..0000000000
--- a/changes/17562-windows-server-2019-os-details
+++ /dev/null
@@ -1 +0,0 @@
-- Fixed a bug where OS version information would not get detected on Windows Server 2019
diff --git a/changes/17563-windows-add b/changes/17563-windows-add
deleted file mode 100644
index 369ed30840..0000000000
--- a/changes/17563-windows-add
+++ /dev/null
@@ -1 +0,0 @@
-- Fixes an issue with Windows MDM profile processing where `
- Any enrolled hosts using this secret will not receive updates
- through Orbit including updates to agent options and command line
- flags.
+ Hosts that enrolled with this secret will not get updates to agent
+ options.
Follow this guide to{" "}
diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tests.tsx b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tests.tsx
new file mode 100644
index 0000000000..9f3ac57a5e
--- /dev/null
+++ b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tests.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+import { noop } from "lodash";
+import { render, screen } from "@testing-library/react";
+
+import createMockHost from "__mocks__/hostMock";
+import { IHost } from "interfaces/host";
+
+import TargetsInput from "./TargetsInput";
+import { ITargestInputHostTableConfig } from "./TargetsInputHostsTableConfig";
+
+describe("TargetsInput", () => {
+ it("renders the search table based on the custom configuration passed in", () => {
+ const testHosts: IHost[] = [
+ createMockHost({
+ display_name: "testHost",
+ public_ip: "123.456.789.0",
+ computer_name: "testName",
+ }),
+ ];
+
+ const testTableConfig: ITargestInputHostTableConfig[] = [
+ {
+ Header: "Name",
+ accessor: "display_name",
+ },
+ {
+ Header: "IP Address",
+ accessor: "public_ip",
+ },
+ ];
+
+ render(
+
/latest)
>
}
- onChange={handleInputChange}
+ onChange={onInputChange}
name="serverURL"
value={serverURL}
parseTarget
diff --git a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts
index 3e314baa83..ccf2aab3d4 100644
--- a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts
+++ b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts
@@ -20,30 +20,6 @@ export interface IFormField {
value: string | boolean | number;
}
-export interface IAppConfigFormErrors {
- metadata?: string | null;
- metadata_url?: string | null;
- entity_id?: string | null;
- idp_name?: string | null;
- server_url?: string | null;
- org_name?: string | null;
- org_logo_url?: string | null;
- org_logo_url_light_background?: string | null;
- org_support_url?: string | null;
- idp_image_url?: string | null;
- sender_address?: string | null;
- server?: string | null;
- server_port?: string | null;
- user_name?: string | null;
- password?: string | null;
- destination_url?: string | null;
- days_count?: string | null;
- host_percentage?: string | null;
- host_expiry_window?: string | null;
- agent_options?: string | null;
- transparency_url?: string | null;
-}
-
export const authMethodOptions = [
{ label: "Plain", value: "authmethod_plain" },
{ label: "Cram MD5", value: "authmethod_cram_md5" },
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx
index fa59ded99b..9ff1f29043 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage/AgentOptionsPage.tsx
@@ -142,8 +142,9 @@ const AgentOptionsPage = ({
return (
- Agent options configure the osquery agent. When you update agent
- options, they will be applied the next time a host checks in to Fleet.
+ Agent options configure Fleet's agent (fleetd). When you update
+ agent options, they will be applied the next time a host checks in to
+ Fleet.
Code based on work by [Kolide](https://github.com/kolide/launcher). Due to changes in macOS 12.3, the output of `profiles show -type enrollment` can only be generated once a day. If you are running this command with another tool, you should set the `PROFILES_SHOW_ENROLLMENT_CACHE_PATH` environment variable to the path you are caching this. The cache file should be `json` with the keys `dep_capable` and `rate_limited present`, both booleans representing whether the device is capable of DEP enrollment and whether the response from `profiles show -type enrollment` is being rate limited or not.
+notes: >-
+ - This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+
+ - Code based on work by [Kolide](https://github.com/kolide/launcher).
+
+ - Due to changes in macOS 12.3, the output of `profiles show -type enrollment` can only be generated once a day. If you are running this command with another tool, you should set the `PROFILES_SHOW_ENROLLMENT_CACHE_PATH` environment variable to the path you are caching this. The cache file should be `json` with the keys `dep_capable` and `rate_limited present`, both booleans representing whether the device is capable of DEP enrollment and whether the response from `profiles show -type enrollment` is being rate limited or not.
description: Information on the device's MDM enrollment.
platforms:
- darwin
diff --git a/schema/tables/mdm_bridge.yml b/schema/tables/mdm_bridge.yml
index 3656d7fba9..a247ae37e9 100644
--- a/schema/tables/mdm_bridge.yml
+++ b/schema/tables/mdm_bridge.yml
@@ -23,5 +23,5 @@ columns:
type: text
required: false
description: The full raw output of the MDM command execution.
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/munki_info.yml b/schema/tables/munki_info.yml
index 7c95b8d28e..b837daa4d7 100644
--- a/schema/tables/munki_info.yml
+++ b/schema/tables/munki_info.yml
@@ -1,5 +1,8 @@
name: munki_info
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).
+notes: >-
+ - This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+
+ - Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).
description: Information from the last [Munki](https://github.com/munki/munki) run.
platforms:
- darwin
diff --git a/schema/tables/munki_installs.yml b/schema/tables/munki_installs.yml
index 3f05dfc3fd..dfa49b6844 100644
--- a/schema/tables/munki_installs.yml
+++ b/schema/tables/munki_installs.yml
@@ -1,5 +1,8 @@
name: munki_installs
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).
+notes: >-
+ - This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+
+ - Code based on work by [macadmins/osquery-extension](https://github.com/macadmins/osquery-extension) and [Kolide](https://github.com/kolide/launcher).
description: Software packages and other items [Munki](https://github.com/munki/munki) is managing.
platforms:
- darwin
diff --git a/schema/tables/nvram_info.yml b/schema/tables/nvram_info.yml
index 7a37e987c7..321788bee4 100644
--- a/schema/tables/nvram_info.yml
+++ b/schema/tables/nvram_info.yml
@@ -9,5 +9,5 @@ columns:
description: >-
Apple Mobile File Integrity (AMFI) was first released in macOS 10.12. The daemon and service block attempts to run unsigned code. AMFI uses lanchd, code signatures, certificates, entitlements, and provisioning profiles to create a filtered entitlement dictionary for an app. AMFI is the macOS kernel module that enforces code-signing and library validation.
Note: AMFI cannot be disabled with SIP enabled, but a change attempt can be made that will appear successful, and report incorrectly as successful. If the AMFI audit fails, and the SIP audit passes, this is still an issue the admin should research.
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/orbit_info.yml b/schema/tables/orbit_info.yml
index 1dfa3a9ff0..cc0d923682 100644
--- a/schema/tables/orbit_info.yml
+++ b/schema/tables/orbit_info.yml
@@ -45,5 +45,5 @@ columns:
type: integer
required: false
description: 1 if running scripts is enabled, 0 if disabled.
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/parse_ini.yml b/schema/tables/parse_ini.yml
index 3d33596f39..e5f2719af7 100644
--- a/schema/tables/parse_ini.yml
+++ b/schema/tables/parse_ini.yml
@@ -1,5 +1,5 @@
name: parse_ini
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Parse a file as INI configuration.
platforms:
- darwin
diff --git a/schema/tables/parse_json.yml b/schema/tables/parse_json.yml
index 7a0e9a339a..fd1e62c526 100644
--- a/schema/tables/parse_json.yml
+++ b/schema/tables/parse_json.yml
@@ -1,5 +1,5 @@
name: parse_json
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Parses an entire file as JSON. See `parse_jsonl` where multiple JSON documents are supported.
platforms:
- darwin
diff --git a/schema/tables/parse_jsonl.yml b/schema/tables/parse_jsonl.yml
index 7aae0eef65..c2664437f7 100644
--- a/schema/tables/parse_jsonl.yml
+++ b/schema/tables/parse_jsonl.yml
@@ -1,5 +1,5 @@
name: parse_jsonl
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Parses each line of a file as a separate JSON document. See `parse_json` to treat an entire file as a single JSON document.
platforms:
- darwin
diff --git a/schema/tables/parse_xml.yml b/schema/tables/parse_xml.yml
index 21b3fe2a40..6f8ea31658 100644
--- a/schema/tables/parse_xml.yml
+++ b/schema/tables/parse_xml.yml
@@ -1,5 +1,5 @@
name: parse_xml
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Parses a file as an XML document.
platforms:
- darwin
diff --git a/schema/tables/pmset.yml b/schema/tables/pmset.yml
index 2e975c495f..e65779f26c 100644
--- a/schema/tables/pmset.yml
+++ b/schema/tables/pmset.yml
@@ -11,7 +11,5 @@ columns:
type: text
required: false
description: Result of the command in JSON format.
-notes: >-
- This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet.
- Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/puppet_info.yml b/schema/tables/puppet_info.yml
index 4e704fc4f4..81a22390b8 100644
--- a/schema/tables/puppet_info.yml
+++ b/schema/tables/puppet_info.yml
@@ -1,5 +1,5 @@
name: puppet_info
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Information on the last [Puppet](https://puppet.com/) run. This table uses data from the `last_run_report` that Puppet creates.
platforms:
- darwin
diff --git a/schema/tables/puppet_logs.yml b/schema/tables/puppet_logs.yml
index 9b84f426e7..2600f21539 100644
--- a/schema/tables/puppet_logs.yml
+++ b/schema/tables/puppet_logs.yml
@@ -1,5 +1,5 @@
name: puppet_logs
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: Outputs [Puppet](https://puppet.com/) logs from the last run.
platforms:
- darwin
diff --git a/schema/tables/puppet_state.yml b/schema/tables/puppet_state.yml
index ad8db96d8d..55bf80ff34 100644
--- a/schema/tables/puppet_state.yml
+++ b/schema/tables/puppet_state.yml
@@ -1,5 +1,5 @@
name: puppet_state
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
description: State of every resource [Puppet](https://puppet.com/) is managing. This table uses data from the `last_run_report` that Puppet creates.
platforms:
- darwin
diff --git a/schema/tables/pwd_policy.yml b/schema/tables/pwd_policy.yml
index f6c0fd920b..9f9dac97fc 100644
--- a/schema/tables/pwd_policy.yml
+++ b/schema/tables/pwd_policy.yml
@@ -1,7 +1,7 @@
name: pwd_policy
platforms:
- darwin
-description: Password Policiy (e.g max failed password attempts).
+description: Password Policy (e.g., max failed password attempts).
columns:
- name: max_failed_attempts
type: integer
@@ -28,7 +28,5 @@ columns:
required: false
description: >-
This parameter indicates the minimum number of mixed characters in a password.
-notes: >-
- This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet.
- Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/sntp_request.yml b/schema/tables/sntp_request.yml
index 8e8d68b09e..3a1b9acbd1 100644
--- a/schema/tables/sntp_request.yml
+++ b/schema/tables/sntp_request.yml
@@ -17,7 +17,5 @@ columns:
type: bigint
required: false
description: Offset between the host's time and the SNTP time in milliseconds.
-notes: >-
- This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet.
- Fleetd installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/software_update.yml b/schema/tables/software_update.yml
index 975d46a23c..9de2839c60 100644
--- a/schema/tables/software_update.yml
+++ b/schema/tables/software_update.yml
@@ -8,5 +8,5 @@ columns:
required: false
description: >-
If true, means one of the Apple softwares installed on this machine has a new available upgrade.
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/sudo_info.yml b/schema/tables/sudo_info.yml
index 074c2f5850..8ae828a515 100644
--- a/schema/tables/sudo_info.yml
+++ b/schema/tables/sudo_info.yml
@@ -7,5 +7,5 @@ columns:
type: text
required: false
description: A JSON document with the key value pairs parsed from `sudo -V` output.
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/schema/tables/user_login_settings.yml b/schema/tables/user_login_settings.yml
index cf3e0b1a03..02aaa35ec8 100644
--- a/schema/tables/user_login_settings.yml
+++ b/schema/tables/user_login_settings.yml
@@ -7,5 +7,5 @@ columns:
type: integer
required: false
description: whether password hint is enabled for any user. 1 means one or more users has a password hint set, 0 means no user has a password hint set
-notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
diff --git a/scripts/mdm/linux/linux-wipe.sh b/scripts/mdm/linux/linux-wipe.sh
index 69a78b1235..0c66494d88 100644
--- a/scripts/mdm/linux/linux-wipe.sh
+++ b/scripts/mdm/linux/linux-wipe.sh
@@ -38,9 +38,19 @@ wipe_system_files() {
done
}
-# Start the wiping process
-logout_users
-wipe_non_essential_data
-wipe_system_files
+wipe_all_files() {
+ sleep 10 # Give fleetd enough time to register the script as completed
+ wipe_non_essential_data
+ wipe_system_files
+}
-echo "Wiping process completed."
+if [ $1 == "wipe" ]; then
+ # We are in the detatched child process
+ wipe_all_files
+else
+ # We are in the parent shell, logout users and begin the detached
+ # wipe child process
+ logout_users
+ echo "Wiping, system will be unreachable"
+ nohup sh $0 wipe >/dev/null 2>/dev/null 0 {
+ deleteActivitiesQuery, args, err := sqlx.In(`DELETE FROM activities WHERE id IN (?);`, activityIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "build activities IN query")
+ }
+ if _, err := ds.writer(ctx).ExecContext(ctx, deleteActivitiesQuery, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired activities")
+ }
+ }
+
+ //
+ // `activities` and `queries` are not tied because the activity itself holds
+ // the query SQL so they don't need to be executed on the same transaction.
+ //
+ if err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
+ // Delete temporary queries (aka "not saved").
+ if _, err := tx.ExecContext(ctx,
+ `DELETE FROM queries
+ WHERE NOT saved AND created_at < DATE_SUB(NOW(), INTERVAL ? DAY)
+ LIMIT ?`,
+ expiredWindowDays, maxCount,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired non-saved queries")
+ }
+ // Delete distributed campaigns that reference unexisting query (removed in the previous query).
+ if _, err := tx.ExecContext(ctx,
+ `DELETE distributed_query_campaigns FROM distributed_query_campaigns
+ LEFT JOIN queries ON (distributed_query_campaigns.query_id=queries.id)
+ WHERE queries.id IS NULL`,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaigns")
+ }
+ // Delete distributed campaign targets that reference unexisting distributed campaign (removed in the previous query).
+ if _, err := tx.ExecContext(ctx,
+ `DELETE distributed_query_campaign_targets FROM distributed_query_campaign_targets
+ LEFT JOIN distributed_query_campaigns ON (distributed_query_campaign_targets.distributed_query_campaign_id=distributed_query_campaigns.id)
+ WHERE distributed_query_campaigns.id IS NULL`,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaign_targets")
+ }
+ return nil
+ }); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired distributed queries")
+ }
+ return nil
+}
diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go
index 8ffccc5b6e..ec4e2b8cc9 100644
--- a/server/datastore/mysql/activities_test.go
+++ b/server/datastore/mysql/activities_test.go
@@ -2,15 +2,18 @@ package mysql
import (
"context"
+ "database/sql"
"encoding/json"
"fmt"
"sort"
+ "strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
+ "github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -29,6 +32,8 @@ func TestActivity(t *testing.T) {
{"PaginationMetadata", testActivityPaginationMetadata},
{"ListHostUpcomingActivities", testListHostUpcomingActivities},
{"ListHostPastActivities", testListHostPastActivities},
+ {"CleanupActivitiesAndAssociatedData", testCleanupActivitiesAndAssociatedData},
+ {"CleanupActivitiesAndAssociatedDataBatch", testCleanupActivitiesAndAssociatedDataBatch},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -553,3 +558,239 @@ func testListHostPastActivities(t *testing.T, ds *Datastore) {
}
}
}
+
+func testCleanupActivitiesAndAssociatedData(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ user1 := &fleet.User{
+ Password: []byte("p4ssw0rd.123"),
+ Name: "user1",
+ Email: "user1@example.com",
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ }
+ user1, err := ds.NewUser(ctx, user1)
+ require.NoError(t, err)
+
+ // Nothing to delete.
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, 500, 1)
+ require.NoError(t, err)
+
+ nonSavedQuery1, err := ds.NewQuery(ctx, &fleet.Query{
+ Name: "nonSavedQuery1",
+ Saved: false,
+ Query: "SELECT 1;",
+ Logging: fleet.LoggingSnapshot,
+ })
+ require.NoError(t, err)
+ savedQuery1, err := ds.NewQuery(ctx, &fleet.Query{
+ Name: "savedQuery1",
+ Saved: true,
+ Query: "SELECT 2;",
+ Logging: fleet.LoggingSnapshot,
+ })
+ require.NoError(t, err)
+ distributedQueryCampaign1, err := ds.NewDistributedQueryCampaign(ctx, &fleet.DistributedQueryCampaign{
+ QueryID: nonSavedQuery1.ID,
+ Status: fleet.QueryComplete,
+ UserID: user1.ID,
+ })
+ require.NoError(t, err)
+ _, err = ds.NewDistributedQueryCampaignTarget(ctx, &fleet.DistributedQueryCampaignTarget{
+ DistributedQueryCampaignID: distributedQueryCampaign1.ID,
+ TargetID: 1,
+ Type: fleet.TargetHost,
+ })
+ require.NoError(t, err)
+ err = ds.NewActivity(ctx, user1, dummyActivity{
+ name: "other activity",
+ details: map[string]interface{}{"detail": 0, "foo": "zoo"},
+ })
+ require.NoError(t, err)
+ err = ds.NewActivity(ctx, user1, dummyActivity{
+ name: "live query",
+ details: map[string]interface{}{"detail": 1, "foo": "bar"},
+ })
+ require.NoError(t, err)
+ err = ds.NewActivity(ctx, user1, dummyActivity{
+ name: "some host activity",
+ details: map[string]interface{}{"detail": 0, "foo": "zoo"},
+ hostIDs: []uint{1},
+ })
+ require.NoError(t, err)
+ err = ds.NewActivity(ctx, user1, dummyActivity{
+ name: "some host activity 2",
+ details: map[string]interface{}{"detail": 0, "foo": "bar"},
+ hostIDs: []uint{2},
+ })
+ require.NoError(t, err)
+
+ // Nothing is deleted, as the activities and associated data is recent.
+ const maxCount = 500
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err := ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 4)
+ nonExpiredActivityID := activities[0].ID
+ expiredActivityID := activities[1].ID
+ nonExpiredHostActivityID := activities[2].ID
+ expiredHostActivityID := activities[3].ID
+ _, err = ds.Query(ctx, nonSavedQuery1.ID)
+ require.NoError(t, err)
+ _, err = ds.DistributedQueryCampaign(ctx, distributedQueryCampaign1.ID)
+ require.NoError(t, err)
+ targets, err := ds.DistributedQueryCampaignTargetIDs(ctx, distributedQueryCampaign1.ID)
+ require.NoError(t, err)
+ require.Len(t, targets.HostIDs, 1)
+
+ // Make some of the activity and associated data older.
+ _, err = ds.writer(context.Background()).Exec(`
+ UPDATE activities SET created_at = ? WHERE id = ? OR id = ?`,
+ time.Now().Add(-48*time.Hour), expiredActivityID, expiredHostActivityID,
+ )
+ require.NoError(t, err)
+ _, err = ds.writer(context.Background()).Exec(`
+ UPDATE queries SET created_at = ? WHERE id = ? OR id = ?`,
+ time.Now().Add(-48*time.Hour), nonSavedQuery1.ID, savedQuery1.ID,
+ )
+ require.NoError(t, err)
+
+ // Expired activity and associated data should be cleaned up.
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err = ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 3)
+ require.Equal(t, nonExpiredActivityID, activities[0].ID)
+ require.Equal(t, nonExpiredHostActivityID, activities[1].ID)
+ require.Equal(t, expiredHostActivityID, activities[2].ID)
+ _, err = ds.Query(ctx, nonSavedQuery1.ID)
+ require.ErrorIs(t, err, sql.ErrNoRows)
+ _, err = ds.DistributedQueryCampaign(ctx, distributedQueryCampaign1.ID)
+ require.ErrorIs(t, err, sql.ErrNoRows)
+ targets, err = ds.DistributedQueryCampaignTargetIDs(ctx, distributedQueryCampaign1.ID)
+ require.NoError(t, err)
+ require.Empty(t, targets.HostIDs)
+ require.Empty(t, targets.LabelIDs)
+ require.Empty(t, targets.TeamIDs)
+
+ // Saved query should not be cleaned up.
+ savedQuery1, err = ds.Query(ctx, savedQuery1.ID)
+ require.NoError(t, err)
+ require.NotNil(t, savedQuery1)
+}
+
+func testCleanupActivitiesAndAssociatedDataBatch(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ user1 := &fleet.User{
+ Password: []byte("p4ssw0rd.123"),
+ Name: "user1",
+ Email: "user1@example.com",
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ }
+ user1, err := ds.NewUser(ctx, user1)
+ require.NoError(t, err)
+
+ const maxCount = 500
+
+ // Create 1500 activities.
+ insertActivitiesStmt := `
+ INSERT INTO activities
+ (user_id, user_name, activity_type, details, user_email)
+ VALUES `
+ var insertActivitiesArgs []interface{}
+ for i := 0; i < 1500; i++ {
+ insertActivitiesArgs = append(insertActivitiesArgs,
+ user1.ID, user1.Name, "foobar", `{"foo": "bar"}`, user1.Email,
+ )
+ }
+ insertActivitiesStmt += strings.TrimSuffix(strings.Repeat("(?, ?, ?, ?, ?),", 1500), ",")
+ _, err = ds.writer(ctx).ExecContext(ctx, insertActivitiesStmt, insertActivitiesArgs...)
+ require.NoError(t, err)
+
+ // Create 1500 non-saved queries.
+ insertQueriesStmt := `
+ INSERT INTO queries
+ (name, description, query)
+ VALUES `
+ var insertQueriesArgs []interface{}
+ for i := 0; i < 1500; i++ {
+ insertQueriesArgs = append(insertQueriesArgs,
+ fmt.Sprintf("foobar%d", i), "foobar", "SELECT 1;",
+ )
+ }
+ insertQueriesStmt += strings.TrimSuffix(strings.Repeat("(?, ?, ?),", 1500), ",")
+ _, err = ds.writer(ctx).ExecContext(ctx, insertQueriesStmt, insertQueriesArgs...)
+ require.NoError(t, err)
+
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err := ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 1500)
+ var queriesLen int
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
+ })
+ require.Equal(t, 1500, queriesLen)
+
+ // Make 1250 activities as expired.
+ _, err = ds.writer(context.Background()).Exec(`
+ UPDATE activities SET created_at = ? WHERE id <= 1250`,
+ time.Now().Add(-48*time.Hour),
+ )
+ require.NoError(t, err)
+
+ // Make 1250 queries as expired.
+ _, err = ds.writer(context.Background()).Exec(`
+ UPDATE queries SET created_at = ? WHERE id <= 1250`,
+ time.Now().Add(-48*time.Hour),
+ )
+ require.NoError(t, err)
+
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err = ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 1000)
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
+ })
+ require.Equal(t, 1000, queriesLen)
+
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err = ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 500)
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
+ })
+ require.Equal(t, 500, queriesLen)
+
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err = ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 250)
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
+ })
+ require.Equal(t, 250, queriesLen)
+
+ err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
+ require.NoError(t, err)
+
+ activities, _, err = ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
+ require.NoError(t, err)
+ require.Len(t, activities, 250)
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
+ })
+ require.Equal(t, 250, queriesLen)
+}
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index f6253d2fd4..db2e0df8db 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -704,7 +704,7 @@ WHERE
return devices, nil
}
-func (ds *Datastore) IngestMDMAppleDeviceFromCheckin(ctx context.Context, mdmHost fleet.MDMAppleHostDetails) error {
+func (ds *Datastore) MDMAppleUpsertHost(ctx context.Context, mdmHost *fleet.Host) error {
appCfg, err := ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "ingest mdm apple host get app config")
@@ -717,20 +717,20 @@ func (ds *Datastore) IngestMDMAppleDeviceFromCheckin(ctx context.Context, mdmHos
func ingestMDMAppleDeviceFromCheckinDB(
ctx context.Context,
tx sqlx.ExtContext,
- mdmHost fleet.MDMAppleHostDetails,
+ mdmHost *fleet.Host,
logger log.Logger,
appCfg *fleet.AppConfig,
) error {
- if mdmHost.SerialNumber == "" {
+ if mdmHost.HardwareSerial == "" {
return ctxerr.New(ctx, "ingest mdm apple host from checkin expected device serial number but got empty string")
}
- if mdmHost.UDID == "" {
+ if mdmHost.UUID == "" {
return ctxerr.New(ctx, "ingest mdm apple host from checkin expected unique device id but got empty string")
}
// MDM is necessarily enabled if this gets called, always pass true for that
// parameter.
- matchID, _, err := matchHostDuringEnrollment(ctx, tx, mdmEnroll, true, "", mdmHost.UDID, mdmHost.SerialNumber)
+ matchID, _, err := matchHostDuringEnrollment(ctx, tx, mdmEnroll, true, "", mdmHost.UUID, mdmHost.HardwareSerial)
switch {
case errors.Is(err, sql.ErrNoRows):
return insertMDMAppleHostDB(ctx, tx, mdmHost, logger, appCfg)
@@ -747,7 +747,7 @@ func updateMDMAppleHostDB(
ctx context.Context,
tx sqlx.ExtContext,
hostID uint,
- mdmHost fleet.MDMAppleHostDetails,
+ mdmHost *fleet.Host,
appCfg *fleet.AppConfig,
) error {
updateStmt := `
@@ -763,13 +763,13 @@ func updateMDMAppleHostDB(
if _, err := tx.ExecContext(
ctx,
updateStmt,
- mdmHost.SerialNumber,
- mdmHost.UDID,
- mdmHost.Model,
+ mdmHost.HardwareSerial,
+ mdmHost.UUID,
+ mdmHost.HardwareModel,
"darwin",
1,
// Set osquery_host_id to the device UUID only if it is not already set.
- mdmHost.UDID,
+ mdmHost.UUID,
hostID,
); err != nil {
return ctxerr.Wrap(ctx, err, "update mdm apple host")
@@ -790,7 +790,7 @@ func updateMDMAppleHostDB(
func insertMDMAppleHostDB(
ctx context.Context,
tx sqlx.ExtContext,
- mdmHost fleet.MDMAppleHostDetails,
+ mdmHost *fleet.Host,
logger log.Logger,
appCfg *fleet.AppConfig,
) error {
@@ -809,13 +809,13 @@ func insertMDMAppleHostDB(
res, err := tx.ExecContext(
ctx,
insertStmt,
- mdmHost.SerialNumber,
- mdmHost.UDID,
- mdmHost.Model,
+ mdmHost.HardwareSerial,
+ mdmHost.UUID,
+ mdmHost.HardwareModel,
"darwin",
"2000-01-01 00:00:00",
"2000-01-01 00:00:00",
- mdmHost.UDID,
+ mdmHost.UUID,
1,
)
if err != nil {
@@ -829,17 +829,18 @@ func insertMDMAppleHostDB(
if id < 1 {
return ctxerr.Wrap(ctx, err, "ingest mdm apple host unexpected last insert id")
}
- host := fleet.Host{ID: uint(id), HardwareModel: mdmHost.Model, HardwareSerial: mdmHost.SerialNumber}
- if err := upsertMDMAppleHostDisplayNamesDB(ctx, tx, host); err != nil {
+ mdmHost.ID = uint(id)
+
+ if err := upsertMDMAppleHostDisplayNamesDB(ctx, tx, *mdmHost); err != nil {
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert display names")
}
- if err := upsertMDMAppleHostLabelMembershipDB(ctx, tx, logger, host); err != nil {
+ if err := upsertMDMAppleHostLabelMembershipDB(ctx, tx, logger, *mdmHost); err != nil {
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert label membership")
}
- if err := upsertMDMAppleHostMDMInfoDB(ctx, tx, appCfg.ServerSettings, false, host.ID); err != nil {
+ if err := upsertMDMAppleHostMDMInfoDB(ctx, tx, appCfg.ServerSettings, false, mdmHost.ID); err != nil {
return ctxerr.Wrap(ctx, err, "ingest mdm apple host upsert MDM info")
}
return nil
@@ -1125,28 +1126,58 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext
return nil
}
-func (ds *Datastore) deleteMDMAppleProfilesForHost(ctx context.Context, tx sqlx.ExtContext, uuid string) error {
- _, err := tx.ExecContext(ctx, `
- DELETE FROM host_mdm_apple_profiles
- WHERE host_uuid = ?`, uuid)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "removing all profiles from host")
+// deleteMDMOSCustomSettingsForHost deletes configuration profiles and
+// declarations for a host based on its platform.
+func (ds *Datastore) deleteMDMOSCustomSettingsForHost(ctx context.Context, tx sqlx.ExtContext, uuid, platform string) error {
+
+ tableMap := map[string][]string{
+ "darwin": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"},
+ "windows": {"host_mdm_windows_profiles"},
}
+
+ tables, ok := tableMap[platform]
+ if !ok {
+ return ctxerr.Errorf(ctx, "unsupported platform %s", platform)
+ }
+
+ for _, table := range tables {
+ _, err := tx.ExecContext(ctx, fmt.Sprintf(`
+ DELETE FROM %s
+ WHERE host_uuid = ?`, table), uuid)
+ if err != nil {
+ return ctxerr.Wrapf(ctx, err, "removing all %s from host %s", table, uuid)
+ }
+ }
+
return nil
}
-func (ds *Datastore) UpdateHostTablesOnMDMUnenroll(ctx context.Context, uuid string) error {
- return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
- var hostID uint
- row := tx.QueryRowxContext(ctx, `SELECT id FROM hosts WHERE uuid = ?`, uuid)
- err := row.Scan(&hostID)
+func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error {
+ return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ var host fleet.Host
+ err := sqlx.GetContext(
+ ctx, tx, &host,
+ `SELECT id, platform FROM hosts WHERE uuid = ? LIMIT 1`, uuid,
+ )
if err != nil {
- return ctxerr.Wrap(ctx, err, "getting host id from UUID")
+ return ctxerr.Wrap(ctx, err, "getting host info from UUID")
}
- // NOTE: set installed_from_dep = 0 so DEP host will not be counted as pending after it unenrolls.
+ if host.Platform != "darwin" && host.Platform != "windows" {
+ return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform)
+ }
+
+ // NOTE: set installed_from_dep = 0 so DEP host will not be
+ // counted as pending after it unenrolls.
_, err = tx.ExecContext(ctx, `
- UPDATE host_mdm SET enrolled = 0, installed_from_dep = 0, server_url = '', mdm_id = NULL WHERE host_id = ?`, hostID)
+ UPDATE host_mdm
+ SET
+ enrolled = 0,
+ installed_from_dep = 0,
+ server_url = '',
+ mdm_id = NULL
+ WHERE
+ host_id = ?`, host.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "clearing host_mdm for host")
}
@@ -1155,18 +1186,16 @@ func (ds *Datastore) UpdateHostTablesOnMDMUnenroll(ctx context.Context, uuid str
// host manually, the device won't Acknowledge any more requests (eg:
// to delete profiles) and profiles are automatically removed on
// unenrollment.
- if err := ds.deleteMDMAppleProfilesForHost(ctx, tx, uuid); err != nil {
+ if err := ds.deleteMDMOSCustomSettingsForHost(ctx, tx, uuid, host.Platform); err != nil {
return ctxerr.Wrap(ctx, err, "deleting profiles for host")
}
- _, err = tx.ExecContext(ctx, `
- DELETE FROM host_disk_encryption_keys
- WHERE host_id = ?`, hostID)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "removing all profiles from host")
- }
+ // NOTE: intentionally keeping disk encryption keys and bootstrap
+ // package information.
- return nil
+ // request a refetch to update any eventually consistent stale information.
+ err = updateHostRefetchRequestedDB(ctx, tx, host.ID, true)
+ return ctxerr.Wrap(ctx, err, "setting host refetch requested")
})
}
@@ -3368,29 +3397,52 @@ WHERE
return nil
}
-func (ds *Datastore) ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error {
+func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
- // it's okay if we didn't update any rows, `nano_enrollments` entries
- // are created on `TokenUpdate`, and this function is called on
- // `Authenticate` to make sure we start on a clean state if a host is
- // re-enrolling.
- _, err := tx.ExecContext(ctx, `UPDATE nano_enrollments SET token_update_tally = 0 WHERE id = ?`, hostUUID)
+ var host fleet.Host
+ err := sqlx.GetContext(
+ ctx, tx, &host,
+ `SELECT id, platform FROM hosts WHERE uuid = ? LIMIT 1`, hostUUID,
+ )
if err != nil {
- return ctxerr.Wrap(ctx, err, "resetting nano_enrollments")
+ return ctxerr.Wrap(ctx, err, "getting host info from UUID")
+ }
+
+ if host.Platform != "darwin" && host.Platform != "windows" {
+ return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform)
}
// Deleting profiles from this table will cause all profiles to
// be re-delivered on the next cron run.
- if err := ds.deleteMDMAppleProfilesForHost(ctx, tx, hostUUID); err != nil {
+ if err := ds.deleteMDMOSCustomSettingsForHost(ctx, tx, hostUUID, host.Platform); err != nil {
return ctxerr.Wrap(ctx, err, "resetting profiles status")
}
- // Deleting the matching entry on this table will cause
- // the aggregate report to show this host as 'pending' to
- // install the bootstrap package.
- _, err = tx.ExecContext(ctx, `DELETE FROM host_mdm_apple_bootstrap_packages WHERE host_uuid = ?`, hostUUID)
+ // Delete any stored disk encryption keys. This covers cases
+ // where hosts re-enroll without sending a CheckOut message
+ // first, for example:
+ //
+ // - IT admin wiping the host locally
+ // - Host restoring from a back-up
+ //
+ // This also means that somebody running `sudo profiles renew
+ // --type enrollment` will report disk encryption as "pending"
+ // for a short period of time.
+ _, err = tx.ExecContext(ctx, `
+ DELETE FROM host_disk_encryption_keys
+ WHERE host_id = ?`, host.ID)
if err != nil {
- return ctxerr.Wrap(ctx, err, "resetting host_mdm_apple_bootstrap_packages")
+ return ctxerr.Wrap(ctx, err, "resetting disk encryption key information for host")
+ }
+
+ if host.Platform == "darwin" {
+ // Deleting the matching entry on this table will cause
+ // the aggregate report to show this host as 'pending' to
+ // install the bootstrap package.
+ _, err = tx.ExecContext(ctx, `DELETE FROM host_mdm_apple_bootstrap_packages WHERE host_uuid = ?`, hostUUID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "resetting host_mdm_apple_bootstrap_packages")
+ }
}
return nil
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index 7a8a6daad1..43ec3a7812 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -66,7 +66,7 @@ func TestMDMApple(t *testing.T) {
{"TestMDMAppleDefaultSetupAssistant", testMDMAppleDefaultSetupAssistant},
{"TestSetVerifiedMacOSProfiles", testSetVerifiedMacOSProfiles},
{"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash},
- {"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment},
+ {"TestMDMAppleResetEnrollment", testMDMAppleResetEnrollment},
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
{"LockUnlockWipeMacOS", testLockUnlockWipeMacOS},
{"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown},
@@ -746,9 +746,9 @@ func testIngestMDMAppleHostAlreadyExistsInFleet(t *testing.T, ds *Datastore) {
require.Equal(t, testSerial, hosts[0].HardwareSerial)
require.Equal(t, testUUID, hosts[0].UUID)
- err = ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -784,9 +784,9 @@ func testIngestMDMNonDarwinHostAlreadyExistsInFleet(t *testing.T, ds *Datastore)
require.Equal(t, testSerial, hosts[0].HardwareSerial)
require.Equal(t, testUUID, hosts[0].UUID)
- err = ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -825,9 +825,9 @@ func testIngestMDMAppleIngestAfterDEPSync(t *testing.T, ds *Datastore) {
checkMDMHostRelatedTables(t, ds, hosts[0].ID, testSerial, testModel)
// now simulate the initial MDM checkin by that same host
- err = ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -844,10 +844,10 @@ func testIngestMDMAppleCheckinBeforeDEPSync(t *testing.T, ds *Datastore) {
testModel := "MacBook Pro"
// ingest host on initial mdm checkin
- err := ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
- Model: testModel,
+ err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
+ HardwareModel: testModel,
})
require.NoError(t, err)
@@ -875,9 +875,9 @@ func testIngestMDMAppleCheckinMultipleIngest(t *testing.T, ds *Datastore) {
testSerial := "test-serial"
testUUID := "test-uuid"
- err := ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -886,9 +886,9 @@ func testIngestMDMAppleCheckinMultipleIngest(t *testing.T, ds *Datastore) {
require.Equal(t, testUUID, hosts[0].UUID)
// duplicate Authenticate request has no effect
- err = ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -901,9 +901,9 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
ctx := context.Background()
testSerial := "test-serial"
testUUID := "test-uuid"
- err := ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{
- UDID: testUUID,
- SerialNumber: testSerial,
+ err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: testUUID,
+ HardwareSerial: testSerial,
})
require.NoError(t, err)
@@ -946,7 +946,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, 1, count)
- err = ds.UpdateHostTablesOnMDMUnenroll(ctx, testUUID)
+ err = ds.MDMTurnOff(ctx, testUUID)
require.NoError(t, err)
err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &count, `SELECT COUNT(*) FROM host_mdm WHERE host_id = ?`, testUUID)
@@ -957,8 +957,8 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Empty(t, hostProfs)
key, err = ds.GetHostDiskEncryptionKey(ctx, hostID)
- require.ErrorIs(t, err, sql.ErrNoRows)
- require.Nil(t, key)
+ require.NoError(t, err)
+ require.NotNil(t, key)
}
func expectAppleProfiles(
@@ -2202,7 +2202,7 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
// hosts[6] deletes all its profiles
tx, err := ds.writer(ctx).BeginTxx(ctx, nil)
require.NoError(t, err)
- require.NoError(t, ds.deleteMDMAppleProfilesForHost(ctx, tx, hosts[6].UUID))
+ require.NoError(t, ds.deleteMDMOSCustomSettingsForHost(ctx, tx, hosts[6].UUID, "darwin"))
require.NoError(t, tx.Commit())
pendingHosts := append(hosts[2:6:6], hosts[7:]...)
res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) // get summary for profiles with no team
@@ -2534,7 +2534,7 @@ func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) {
tx, err := ds.writer(ctx).BeginTxx(ctx, nil)
require.NoError(t, err)
- require.NoError(t, ds.deleteMDMAppleProfilesForHost(ctx, tx, h.UUID))
+ require.NoError(t, ds.deleteMDMOSCustomSettingsForHost(ctx, tx, h.UUID, "darwin"))
require.NoError(t, tx.Commit())
require.NoError(t, err)
gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, h.UUID)
@@ -4215,7 +4215,7 @@ func TestHostDEPAssignments(t *testing.T) {
require.True(t, *h.DEPAssignedToFleet)
// simulate MDM unenroll
- require.NoError(t, ds.UpdateHostTablesOnMDMUnenroll(ctx, depUUID))
+ require.NoError(t, ds.MDMTurnOff(ctx, depUUID))
// host MDM row is set to defaults on unenrollment
getHostResp, err = ds.Host(ctx, testHost.ID)
@@ -4295,7 +4295,7 @@ func TestHostDEPAssignments(t *testing.T) {
manualOrbitNodeKey := "manual-orbit-node-key"
manualDeviceToken := "manual-device-token"
- err = ds.IngestMDMAppleDeviceFromCheckin(ctx, fleet.MDMAppleHostDetails{SerialNumber: manualSerial, UDID: manualUUID})
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{HardwareSerial: manualSerial, UUID: manualUUID})
require.NoError(t, err)
var manualHostID uint
@@ -4413,7 +4413,7 @@ func testMDMAppleConfigProfileHash(t *testing.T, ds *Datastore) {
}
}
-func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) {
+func testMDMAppleResetEnrollment(t *testing.T, ds *Datastore) {
ctx := context.Background()
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host1-name",
@@ -4427,7 +4427,7 @@ func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) {
// try with a host that doesn't have a matching entry
// in nano_enrollments
- err = ds.ResetMDMAppleEnrollment(ctx, host.UUID)
+ err = ds.MDMResetEnrollment(ctx, host.UUID)
require.NoError(t, err)
// add a matching entry in the nano table
@@ -4484,13 +4484,9 @@ func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) {
require.EqualValues(t, 1, sum.Installed)
// reset the enrollment
- err = ds.ResetMDMAppleEnrollment(ctx, host.UUID)
+ err = ds.MDMResetEnrollment(ctx, host.UUID)
require.NoError(t, err)
- enrollment, err = ds.GetNanoMDMEnrollment(ctx, host.UUID)
- require.NoError(t, err)
- require.Zero(t, enrollment.TokenUpdateTally)
-
gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, host.UUID)
require.NoError(t, err)
require.Empty(t, gotProfs)
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 9c799edf59..56474197b4 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -2680,13 +2680,13 @@ func (ds *Datastore) HostByIdentifier(ctx context.Context, identifier string) (*
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
COALESCE(hd.gigs_total_disk_space, 0) as gigs_total_disk_space,
COALESCE(hst.seen_time, h.created_at) AS seen_time,
- COALESCE(hu.software_updated_at, h.created_at) AS software_updated_at
- ` + hostMDMSelect + `
+ COALESCE(hu.software_updated_at, h.created_at) AS software_updated_at
+ ` + hostMDMSelect + `
FROM hosts h
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
- LEFT JOIN host_updates hu ON (h.id = hu.host_id)
+ LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
- ` + hostMDMJoin + `
+ ` + hostMDMJoin + `
WHERE ? IN (h.hostname, h.osquery_host_id, h.node_key, h.uuid, h.hardware_serial)
LIMIT 1
`
@@ -3715,9 +3715,11 @@ func (ds *Datastore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.
err := sqlx.GetContext(
ctx, ds.reader(ctx), &orbit, `
SELECT
- scripts_enabled
+ version,
+ desktop_version,
+ scripts_enabled
FROM
- host_orbit_info
+ host_orbit_info
WHERE host_id = ?`, hostID,
)
if err != nil {
@@ -4343,12 +4345,15 @@ func (ds *Datastore) UpdateHostOsqueryIntervals(ctx context.Context, id uint, in
// UpdateHostRefetchRequested updates a host's refetch requested field.
func (ds *Datastore) UpdateHostRefetchRequested(ctx context.Context, id uint, value bool) error {
+ return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ return updateHostRefetchRequestedDB(ctx, tx, id, value)
+ })
+}
+
+func updateHostRefetchRequestedDB(ctx context.Context, tx sqlx.ExtContext, id uint, value bool) error {
sqlStatement := `UPDATE hosts SET refetch_requested = ? WHERE id = ?`
- _, err := ds.writer(ctx).ExecContext(ctx, sqlStatement, value, id)
- if err != nil {
- return ctxerr.Wrapf(ctx, err, "update host %d refetch_requested", id)
- }
- return nil
+ _, err := tx.ExecContext(ctx, sqlStatement, value, id)
+ return ctxerr.Wrapf(ctx, err, "update host %d refetch_requested", id)
}
// UpdateHostRefetchCriticalQueriesUntil updates a host's refetch critical queries until field.
@@ -5037,7 +5042,7 @@ func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *str
stmt := `
SELECT
h.id,
- h.team_id,
+ h.team_id,
h.osquery_host_id,
h.node_key,
h.hostname,
@@ -5048,7 +5053,7 @@ func (ds *Datastore) loadHostLite(ctx context.Context, id *uint, identifier *str
COALESCE(hst.seen_time, h.created_at) AS seen_time
FROM hosts h
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
- %s
+ %s
LIMIT 1
`
var (
diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go
index 2ace200a05..691db6fecf 100644
--- a/server/datastore/mysql/mdm.go
+++ b/server/datastore/mysql/mdm.go
@@ -307,11 +307,22 @@ ORDER BY
return labels, nil
}
+func (ds *Datastore) BulkSetPendingMDMHostProfiles(
+ ctx context.Context,
+ hostIDs, teamIDs []uint,
+ profileUUIDs, hostUUIDs []string,
+) error {
+ return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ return ds.bulkSetPendingMDMHostProfilesDB(ctx, tx, hostIDs, teamIDs, profileUUIDs, hostUUIDs)
+ })
+}
+
// Note that team ID 0 is used for profiles that apply to hosts in no team
// (i.e. pass 0 in that case as part of the teamIDs slice). Only one of the
// slice arguments can have values.
-func (ds *Datastore) BulkSetPendingMDMHostProfiles(
+func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
ctx context.Context,
+ tx sqlx.ExtContext,
hostIDs, teamIDs []uint,
profileUUIDs, hostUUIDs []string,
) error {
@@ -431,60 +442,58 @@ WHERE
}
- return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
- // TODO: this could be optimized to avoid querying for platform when
- // profileIDs or profileUUIDs are provided.
- if len(hosts) == 0 && !hasAppleDecls {
- uuidStmt, args, err := sqlx.In(uuidStmt, args...)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs")
- }
- if err := sqlx.SelectContext(ctx, tx, &hosts, uuidStmt, args...); err != nil {
- return ctxerr.Wrap(ctx, err, "execute query to load host UUIDs")
- }
+ // TODO: this could be optimized to avoid querying for platform when
+ // profileIDs or profileUUIDs are provided.
+ if len(hosts) == 0 && !hasAppleDecls {
+ uuidStmt, args, err := sqlx.In(uuidStmt, args...)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs")
}
+ if err := sqlx.SelectContext(ctx, tx, &hosts, uuidStmt, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "execute query to load host UUIDs")
+ }
+ }
- var macHosts []string
- var winHosts []string
- for _, h := range hosts {
- switch h.Platform {
- case "darwin":
- macHosts = append(macHosts, h.UUID)
- case "windows":
- winHosts = append(winHosts, h.UUID)
- default:
- level.Debug(ds.logger).Log(
- "msg", "tried to set profile status for a host with unsupported platform",
- "platform", h.Platform,
- "host_uuid", h.UUID,
- )
- }
+ var macHosts []string
+ var winHosts []string
+ for _, h := range hosts {
+ switch h.Platform {
+ case "darwin":
+ macHosts = append(macHosts, h.UUID)
+ case "windows":
+ winHosts = append(winHosts, h.UUID)
+ default:
+ level.Debug(ds.logger).Log(
+ "msg", "tried to set profile status for a host with unsupported platform",
+ "platform", h.Platform,
+ "host_uuid", h.UUID,
+ )
}
+ }
- if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, macHosts); err != nil {
- return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
- }
+ if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, macHosts); err != nil {
+ return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
+ }
- if err := ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts); err != nil {
- return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
- }
+ if err := ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts); err != nil {
+ return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
+ }
- const defaultBatchSize = 1000
- batchSize := defaultBatchSize
- if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
- batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
- }
- // TODO(roberto): this method currently sets the state of all
- // declarations for all hosts. I don't see an immediate concern
- // (and my hunch is that we could even do the same for
- // profiles) but this could be optimized to use only a provided
- // set of host uuids.
- if _, err := mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil); err != nil {
- return ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
- }
+ const defaultBatchSize = 1000
+ batchSize := defaultBatchSize
+ if ds.testUpsertMDMDesiredProfilesBatchSize > 0 {
+ batchSize = ds.testUpsertMDMDesiredProfilesBatchSize
+ }
+ // TODO(roberto): this method currently sets the state of all
+ // declarations for all hosts. I don't see an immediate concern
+ // (and my hunch is that we could even do the same for
+ // profiles) but this could be optimized to use only a provided
+ // set of host uuids.
+ if _, err := mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil); err != nil {
+ return ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
+ }
- return nil
- })
+ return nil
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
diff --git a/server/datastore/mysql/mdm_test.go b/server/datastore/mysql/mdm_test.go
index 3f4988d13e..a1de72e56e 100644
--- a/server/datastore/mysql/mdm_test.go
+++ b/server/datastore/mysql/mdm_test.go
@@ -38,7 +38,10 @@ func TestMDMShared(t *testing.T) {
{"TestBulkSetPendingMDMHostProfiles", testBulkSetPendingMDMHostProfiles},
{"TestBulkSetPendingMDMHostProfilesBatch2", testBulkSetPendingMDMHostProfilesBatch2},
{"TestBulkSetPendingMDMHostProfilesBatch3", testBulkSetPendingMDMHostProfilesBatch3},
- {"TestGetHostMDMProfilesExpectedForVerification", testGetHostMDMProfilesExpectedForVerification},
+ {
+ "TestGetHostMDMProfilesExpectedForVerification",
+ testGetHostMDMProfilesExpectedForVerification,
+ },
{"TestBatchSetProfileLabelAssociations", testBatchSetProfileLabelAssociations},
{"TestBatchSetProfilesTransactionError", testBatchSetMDMProfilesTransactionError},
{"TestMDMEULA", testMDMEULA},
@@ -87,9 +90,16 @@ func testMDMCommands(t *testing.T, ds *Datastore) {
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
require.NoError(t, err)
- err = ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, windowsEnrollment.HostUUID, windowsEnrollment.MDMDeviceID)
+ err = ds.UpdateMDMWindowsEnrollmentsHostUUID(
+ ctx,
+ windowsEnrollment.HostUUID,
+ windowsEnrollment.MDMDeviceID,
+ )
require.NoError(t, err)
- windowsEnrollment, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, windowsEnrollment.MDMDeviceID)
+ windowsEnrollment, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(
+ ctx,
+ windowsEnrollment.MDMDeviceID,
+ )
require.NoError(t, err)
// enroll a macOS device
@@ -104,7 +114,11 @@ func testMDMCommands(t *testing.T, ds *Datastore) {
nanoEnroll(t, ds, macH, false)
// no commands => no results
- cmds, err = ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
+ cmds, err = ds.ListMDMCommands(
+ ctx,
+ fleet.TeamFilter{User: test.UserAdmin},
+ &fleet.MDMCommandListOptions{},
+ )
require.NoError(t, err)
require.Empty(t, cmds)
@@ -118,7 +132,11 @@ func testMDMCommands(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// we get one result
- cmds, err = ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
+ cmds, err = ds.ListMDMCommands(
+ ctx,
+ fleet.TeamFilter{User: test.UserAdmin},
+ &fleet.MDMCommandListOptions{},
+ )
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, winCmd.CommandUUID, cmds[0].CommandUUID)
@@ -160,12 +178,25 @@ func testMDMCommands(t *testing.T, ds *Datastore) {
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
- res, err := tx.ExecContext(ctx, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, windowsEnrollment.ID, "")
+ res, err := tx.ExecContext(
+ ctx,
+ `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`,
+ windowsEnrollment.ID,
+ "",
+ )
if err != nil {
return err
}
resID, _ := res.LastInsertId()
- _, err = tx.ExecContext(ctx, `INSERT INTO windows_mdm_command_results (enrollment_id, command_uuid, raw_result, status_code, response_id) VALUES (?, ?, ?, ?, ?)`, windowsEnrollment.ID, winCmd.CommandUUID, "", "200", resID)
+ _, err = tx.ExecContext(
+ ctx,
+ `INSERT INTO windows_mdm_command_results (enrollment_id, command_uuid, raw_result, status_code, response_id) VALUES (?, ?, ?, ?, ?)`,
+ windowsEnrollment.ID,
+ winCmd.CommandUUID,
+ "",
+ "200",
+ resID,
+ )
return err
})
@@ -228,8 +259,12 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
[]*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")},
[]*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")},
ptr.Uint(1),
- []*fleet.MDMAppleConfigProfile{withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1)},
- []*fleet.MDMWindowsConfigProfile{withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1)},
+ []*fleet.MDMAppleConfigProfile{
+ withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1),
+ },
+ []*fleet.MDMWindowsConfigProfile{
+ withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1),
+ },
[]*fleet.MDMAppleDeclaration{withTeamIDDecl(declForTest("D1", "D1", "foo"), 1)},
)
@@ -378,9 +413,15 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
// add fleet-managed Windows profiles for the team and globally
for name := range mdm_types.FleetReservedProfileNames() {
- _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: &team.ID, SyncML: winProf})
+ _, err = ds.NewMDMWindowsConfigProfile(
+ ctx,
+ fleet.MDMWindowsConfigProfile{Name: name, TeamID: &team.ID, SyncML: winProf},
+ )
require.NoError(t, err)
- _, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: nil, SyncML: winProf})
+ _, err = ds.NewMDMWindowsConfigProfile(
+ ctx,
+ fleet.MDMWindowsConfigProfile{Name: name, TeamID: nil, SyncML: winProf},
+ )
require.NoError(t, err)
}
@@ -398,7 +439,10 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
// create a mac profile for global and a Windows profile for team
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("A", "A", 0))
require.NoError(t, err)
- profB, err := ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: "B", TeamID: &team.ID, SyncML: winProf})
+ profB, err := ds.NewMDMWindowsConfigProfile(
+ ctx,
+ fleet.MDMWindowsConfigProfile{Name: "B", TeamID: &team.ID, SyncML: winProf},
+ )
require.NoError(t, err)
// get global profiles returns the mac one
@@ -449,7 +493,11 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
_, err = ds.NewMDMAppleConfigProfile(ctx, acp)
require.NoError(t, err)
- wcp := fleet.MDMWindowsConfigProfile{Name: string(rune('C' + inc + 2)), TeamID: nil, SyncML: winProf}
+ wcp := fleet.MDMWindowsConfigProfile{
+ Name: string(rune('C' + inc + 2)),
+ TeamID: nil,
+ SyncML: winProf,
+ }
if i == 0 {
wcp.Labels = []fleet.ConfigurationProfileLabel{
{LabelName: labels[4].Name, LabelID: labels[4].ID},
@@ -459,7 +507,11 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
_, err = ds.NewMDMWindowsConfigProfile(ctx, wcp)
require.NoError(t, err)
- wcp = fleet.MDMWindowsConfigProfile{Name: string(rune('C' + inc + 3)), TeamID: &team.ID, SyncML: winProf}
+ wcp = fleet.MDMWindowsConfigProfile{
+ Name: string(rune('C' + inc + 3)),
+ TeamID: &team.ID,
+ SyncML: winProf,
+ }
if i == 0 {
wcp.Labels = []fleet.ConfigurationProfileLabel{
{LabelName: labels[6].Name, LabelID: labels[6].ID},
@@ -499,33 +551,165 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
wantNames []string
wantMeta fleet.PaginationMetadata
}{
- {"all global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true}, []string{"A", "C", "E", "G", "I", "K", "M"}, fleet.PaginationMetadata{}},
- {"all team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true}, []string{"B", "D", "F", "H", "J", "L", "N"}, fleet.PaginationMetadata{}},
+ {
+ "all global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true},
+ []string{"A", "C", "E", "G", "I", "K", "M"},
+ fleet.PaginationMetadata{},
+ },
+ {
+ "all team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true},
+ []string{"B", "D", "F", "H", "J", "L", "N"},
+ fleet.PaginationMetadata{},
+ },
- {"page 0 per page 2, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2}, []string{"A", "C"}, fleet.PaginationMetadata{HasNextResults: true}},
- {"page 1 per page 2, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1}, []string{"E", "G"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 2 per page 2, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2}, []string{"I", "K"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 3 per page 2, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3}, []string{"M"}, fleet.PaginationMetadata{HasPreviousResults: true}},
- {"page 4 per page 2, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4}, []string{}, fleet.PaginationMetadata{HasPreviousResults: true}},
+ {
+ "page 0 per page 2, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2},
+ []string{"A", "C"},
+ fleet.PaginationMetadata{HasNextResults: true},
+ },
+ {
+ "page 1 per page 2, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1},
+ []string{"E", "G"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 2 per page 2, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2},
+ []string{"I", "K"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 3 per page 2, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3},
+ []string{"M"},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
+ {
+ "page 4 per page 2, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4},
+ []string{},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
- {"page 0 per page 2, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2}, []string{"B", "D"}, fleet.PaginationMetadata{HasNextResults: true}},
- {"page 1 per page 2, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1}, []string{"F", "H"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 2 per page 2, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2}, []string{"J", "L"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 3 per page 2, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3}, []string{"N"}, fleet.PaginationMetadata{HasPreviousResults: true}},
- {"page 4 per page 2, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4}, []string{}, fleet.PaginationMetadata{HasPreviousResults: true}},
+ {
+ "page 0 per page 2, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2},
+ []string{"B", "D"},
+ fleet.PaginationMetadata{HasNextResults: true},
+ },
+ {
+ "page 1 per page 2, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1},
+ []string{"F", "H"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 2 per page 2, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2},
+ []string{"J", "L"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 3 per page 2, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3},
+ []string{"N"},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
+ {
+ "page 4 per page 2, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4},
+ []string{},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
- {"page 0 per page 3, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3}, []string{"A", "C", "E"}, fleet.PaginationMetadata{HasNextResults: true}},
- {"page 1 per page 3, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1}, []string{"G", "I", "K"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 2 per page 3, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2}, []string{"M"}, fleet.PaginationMetadata{HasPreviousResults: true}},
- {"page 3 per page 3, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3}, []string{}, fleet.PaginationMetadata{HasPreviousResults: true}},
+ {
+ "page 0 per page 3, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3},
+ []string{"A", "C", "E"},
+ fleet.PaginationMetadata{HasNextResults: true},
+ },
+ {
+ "page 1 per page 3, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1},
+ []string{"G", "I", "K"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 2 per page 3, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2},
+ []string{"M"},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
+ {
+ "page 3 per page 3, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3},
+ []string{},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
- {"page 0 per page 3, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3}, []string{"B", "D", "F"}, fleet.PaginationMetadata{HasNextResults: true}},
- {"page 1 per page 3, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1}, []string{"H", "J", "L"}, fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true}},
- {"page 2 per page 3, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2}, []string{"N"}, fleet.PaginationMetadata{HasPreviousResults: true}},
- {"page 3 per page 3, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3}, []string{}, fleet.PaginationMetadata{HasPreviousResults: true}},
+ {
+ "page 0 per page 3, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3},
+ []string{"B", "D", "F"},
+ fleet.PaginationMetadata{HasNextResults: true},
+ },
+ {
+ "page 1 per page 3, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1},
+ []string{"H", "J", "L"},
+ fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
+ },
+ {
+ "page 2 per page 3, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2},
+ []string{"N"},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
+ {
+ "page 3 per page 3, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3},
+ []string{},
+ fleet.PaginationMetadata{HasPreviousResults: true},
+ },
- {"no metadata, global", nil, fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1}, []string{"E", "G"}, fleet.PaginationMetadata{}},
- {"no metadata, team", &team.ID, fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1}, []string{"F", "H"}, fleet.PaginationMetadata{}},
+ {
+ "no metadata, global",
+ nil,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1},
+ []string{"E", "G"},
+ fleet.PaginationMetadata{},
+ },
+ {
+ "no metadata, team",
+ &team.ID,
+ fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1},
+ []string{"F", "H"},
+ fleet.PaginationMetadata{},
+ },
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
@@ -788,7 +972,13 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
windowsConfigProfileForTest(t, "G2w", "L2"),
windowsConfigProfileForTest(t, "G3w", "L3"),
}
- err = ds.BatchSetMDMProfiles(ctx, nil, macGlobalProfiles, winGlobalProfiles, macGlobalDeclarations)
+ err = ds.BatchSetMDMProfiles(
+ ctx,
+ nil,
+ macGlobalProfiles,
+ winGlobalProfiles,
+ macGlobalDeclarations,
+ )
require.NoError(t, err)
macGlobalProfiles, err = ds.ListMDMAppleConfigProfiles(ctx, nil)
require.NoError(t, err)
@@ -4962,7 +5152,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`, "labeled_prof")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
+ "labeled_prof",
+ )
})
// Update label with host membership
@@ -5035,7 +5231,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`, "labeled_prof_2")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
+ "labeled_prof_2",
+ )
})
// Update label with host membership
@@ -5119,7 +5321,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`, "broken_label_prof")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
+ "broken_label_prof",
+ )
})
// Update label with host membership
@@ -5231,7 +5439,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`, "labeled_prof")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`,
+ "labeled_prof",
+ )
})
// Update label with host membership
@@ -5304,7 +5518,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`, "labeled_prof_2")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`,
+ "labeled_prof_2",
+ )
})
// Update label with host membership
@@ -5388,7 +5608,13 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`, "broken_label_prof")
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &uid,
+ `SELECT profile_uuid FROM mdm_windows_configuration_profiles WHERE name = ?`,
+ "broken_label_prof",
+ )
})
// Update label with host membership
@@ -5593,11 +5819,17 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
wantOtherWin := []fleet.ConfigurationProfileLabel{
{ProfileUUID: otherWinProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID},
}
- require.NoError(t, batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, "windows"))
+ require.NoError(
+ t,
+ batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, "windows"),
+ )
wantOtherMac := []fleet.ConfigurationProfileLabel{
{ProfileUUID: otherMacProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID},
}
- require.NoError(t, batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherMac, "darwin"))
+ require.NoError(
+ t,
+ batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherMac, "darwin"),
+ )
platforms := map[string]string{
"darwin": macOSProfile.ProfileUUID,
@@ -5615,7 +5847,11 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
p = "apple"
}
- query := fmt.Sprintf("SELECT %s_profile_uuid as profile_uuid, label_id, label_name FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ?", p, p)
+ query := fmt.Sprintf(
+ "SELECT %s_profile_uuid as profile_uuid, label_id, label_name FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ?",
+ p,
+ p,
+ )
var got []fleet.ConfigurationProfileLabel
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
@@ -5724,7 +5960,12 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
t.Run("unsupported platform", func(t *testing.T) {
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
- return batchSetProfileLabelAssociationsDB(ctx, tx, []fleet.ConfigurationProfileLabel{{}}, "unsupported")
+ return batchSetProfileLabelAssociationsDB(
+ ctx,
+ tx,
+ []fleet.ConfigurationProfileLabel{{}},
+ "unsupported",
+ )
})
require.Error(t, err)
})
@@ -5745,18 +5986,50 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) {
{"insert:b", "", ": insert:b"},
{"delete:c", "", "batch set windows profiles: delete obsolete profiles: delete:c"},
{"reselect:d", "", "batch set windows profiles: load newly inserted profiles: reselect:d"},
- {"labels:e", "", "batch set windows profiles: inserting windows profile label associations: labels:e"},
- {"inselect:k", "", "batch set windows profiles: build query to load existing profiles: inselect:k"},
- {"indelete:l", "", "batch set windows profiles: build statement to delete obsolete profiles: indelete:l"},
- {"inreselect:m", "", "batch set windows profiles: build query to load newly inserted profiles: inreselect:m"},
+ {
+ "labels:e",
+ "",
+ "batch set windows profiles: inserting windows profile label associations: labels:e",
+ },
+ {
+ "inselect:k",
+ "",
+ "batch set windows profiles: build query to load existing profiles: inselect:k",
+ },
+ {
+ "indelete:l",
+ "",
+ "batch set windows profiles: build statement to delete obsolete profiles: indelete:l",
+ },
+ {
+ "inreselect:m",
+ "",
+ "batch set windows profiles: build query to load newly inserted profiles: inreselect:m",
+ },
{"", "select:f", "batch set apple profiles: load existing profiles: select:f"},
{"", "insert:g", ": insert:g"},
{"", "delete:h", "batch set apple profiles: delete obsolete profiles: delete:h"},
{"", "reselect:i", "batch set apple profiles: load newly inserted profiles: reselect:i"},
- {"", "labels:j", "batch set apple profiles: inserting apple profile label associations: labels:j"},
- {"", "inselect:n", "batch set apple profiles: build query to load existing profiles: inselect:n"},
- {"", "indelete:o", "batch set apple profiles: build statement to delete obsolete profiles: indelete:o"},
- {"", "inreselect:p", "batch set apple profiles: build query to load newly inserted profiles: inreselect:p"},
+ {
+ "",
+ "labels:j",
+ "batch set apple profiles: inserting apple profile label associations: labels:j",
+ },
+ {
+ "",
+ "inselect:n",
+ "batch set apple profiles: build query to load existing profiles: inselect:n",
+ },
+ {
+ "",
+ "indelete:o",
+ "batch set apple profiles: build statement to delete obsolete profiles: indelete:o",
+ },
+ {
+ "",
+ "inreselect:p",
+ "batch set apple profiles: build query to load newly inserted profiles: inreselect:p",
+ },
}
for _, c := range cases {
t.Run(c.windowsErr+" "+c.appleErr, func(t *testing.T) {
@@ -5929,7 +6202,13 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
checkSCEPRenew := func(assoc fleet.SCEPIdentityAssociation, want *string) {
var got *string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- return sqlx.GetContext(ctx, q, &got, `SELECT renew_command_uuid FROM nano_cert_auth_associations WHERE id = ?`, assoc.HostUUID)
+ return sqlx.GetContext(
+ ctx,
+ q,
+ &got,
+ `SELECT renew_command_uuid FROM nano_cert_auth_associations WHERE id = ?`,
+ assoc.HostUUID,
+ )
})
require.EqualValues(t, want, got)
}
@@ -5964,7 +6243,11 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
checkSCEPRenew(assocs[2], ptr.String("bar"))
checkSCEPRenew(assocs[3], ptr.String("bar"))
- err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{{HostUUID: "foo", SHA256: "bar"}}, "bar")
+ err = ds.SetCommandForPendingSCEPRenewal(
+ ctx,
+ []fleet.SCEPIdentityAssociation{{HostUUID: "foo", SHA256: "bar"}},
+ "bar",
+ )
require.ErrorContains(t, err, "this function can only be used to update existing associations")
err = ds.CleanSCEPRenewRefs(ctx, "does-not-exist")
@@ -5998,21 +6281,34 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
}
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
- gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
+ gotHosts, err := ds.ListHosts(
+ ctx,
+ fleet.TeamFilter{User: test.UserAdmin},
+ fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status},
+ )
require.NoError(t, err)
if len(expectedIDs) != len(gotHosts) {
gotIDs := make([]uint, len(gotHosts))
for _, h := range gotHosts {
gotIDs = append(gotIDs, h.ID)
}
- require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs))
+ require.Len(
+ t,
+ gotHosts,
+ len(expectedIDs),
+ fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs),
+ )
}
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
- count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
+ count, err := ds.CountHosts(
+ ctx,
+ fleet.TeamFilter{User: test.UserAdmin},
+ fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status},
+ )
require.NoError(t, err)
require.Equal(t, len(expectedIDs), count, "status: %s", status)
}
@@ -6047,10 +6343,30 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
Verified: expectSummaryWindows[fleet.MDMDeliveryVerified],
})
- checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified])
- checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying])
- checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed])
- checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending])
+ checkListHostsFilterOSSettings(
+ t,
+ teamID,
+ fleet.OSSettingsVerified,
+ ep[fleet.MDMDeliveryVerified],
+ )
+ checkListHostsFilterOSSettings(
+ t,
+ teamID,
+ fleet.OSSettingsVerifying,
+ ep[fleet.MDMDeliveryVerifying],
+ )
+ checkListHostsFilterOSSettings(
+ t,
+ teamID,
+ fleet.OSSettingsFailed,
+ ep[fleet.MDMDeliveryFailed],
+ )
+ checkListHostsFilterOSSettings(
+ t,
+ teamID,
+ fleet.OSSettingsPending,
+ ep[fleet.MDMDeliveryPending],
+ )
}
// checkWinHostProfiles := func(t *testing.T, hostUUID string, statusByProfUUID map[string]string) {
@@ -6093,13 +6409,21 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
default:
require.FailNow(t, "unknown profile type")
}
- stmt := fmt.Sprintf(`INSERT INTO %s (host_uuid, %s_uuid, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`, table, profType)
+ stmt := fmt.Sprintf(
+ `INSERT INTO %s (host_uuid, %s_uuid, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`,
+ table,
+ profType,
+ )
_, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, status)
if err != nil {
require.NoError(t, err)
return err
}
- stmt = fmt.Sprintf(`UPDATE %s SET operation_type = ? WHERE host_uuid = ? AND %s_uuid = ?`, table, profType)
+ stmt = fmt.Sprintf(
+ `UPDATE %s SET operation_type = ? WHERE host_uuid = ? AND %s_uuid = ?`,
+ table,
+ profType,
+ )
_, err = q.ExecContext(ctx, stmt, fleet.MDMOperationTypeInstall, hostUUID, profUUID)
require.NoError(t, err)
return err
@@ -6166,7 +6490,19 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
winHostsByID[h.ID] = h
}
- require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+ require.NoError(
+ t,
+ ds.SetOrUpdateMDMData(
+ ctx,
+ h.ID,
+ false,
+ true,
+ "https://example.com",
+ false,
+ fleet.WellKnownMDMFleet,
+ "",
+ ),
+ )
}
checkExpected(t, nil, nil)
diff --git a/server/datastore/mysql/operating_systems.go b/server/datastore/mysql/operating_systems.go
index 4b9fd0a2d6..b1d8e295d2 100644
--- a/server/datastore/mysql/operating_systems.go
+++ b/server/datastore/mysql/operating_systems.go
@@ -36,6 +36,15 @@ func (ds *Datastore) ListOperatingSystemsForPlatform(ctx context.Context, platfo
}
func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
+ // We optimize for the most common case where the operating system for the host has not changed.
+ // No DB transaction or DB write is needed in this case.
+ updateNeeded, err := isHostOperatingSystemUpdateNeeded(ctx, ds.reader(ctx), hostID, hostOS)
+ if err != nil {
+ return err
+ }
+ if !updateNeeded {
+ return nil
+ }
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
os, err := getOrGenerateOperatingSystemDB(ctx, tx, hostOS)
if err != nil {
@@ -136,26 +145,38 @@ func getOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.
return &os, nil
}
+func isHostOperatingSystemUpdateNeeded(ctx context.Context, qc sqlx.QueryerContext, hostID uint, hostOS fleet.OperatingSystem) (
+ bool, error,
+) {
+ var resultPresent bool
+ err := sqlx.GetContext(
+ ctx, qc, &resultPresent,
+ `SELECT 1 FROM host_operating_system hos
+ INNER JOIN operating_systems os ON hos.os_id = os.id
+ WHERE hos.host_id = ? AND os.name = ? AND os.version = ? AND os.arch = ? AND os.kernel_version = ? AND os.platform = ? AND os.display_version = ?`,
+ hostID, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion, hostOS.Platform, hostOS.DisplayVersion,
+ )
+ switch {
+ case errors.Is(err, sql.ErrNoRows):
+ return true, nil
+ case err != nil:
+ return false, ctxerr.Wrap(ctx, err, "check host operating system")
+ default:
+ return !resultPresent, nil
+ }
+}
+
// upsertHostOperatingSystemDB upserts the host operating system table
// with the operating system id for the given host ID
func upsertHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint, osID uint) error {
- res, err := tx.ExecContext(ctx, "UPDATE host_operating_system SET os_id = ? WHERE host_id = ?", osID, hostID)
- if err != nil {
- return err
- }
-
- if n, _ := res.RowsAffected(); n > 0 {
- // update success
- return nil
- }
-
- // no row to update so insert new row
- _, err = tx.ExecContext(ctx, "INSERT INTO host_operating_system (host_id, os_id) VALUES (?, ?)", hostID, osID)
- if err != nil {
- return err
- }
-
- return nil
+ // We do not use the `UPDATE` then `INSERT` pattern here because it causes a deadlock when multiple hosts are enrolled concurrently.
+ // This method will rarely be called -- only when the host_operating_system needs to be updated.
+ _, err := tx.ExecContext(
+ ctx,
+ `INSERT INTO host_operating_system (host_id, os_id) VALUES (?, ?)
+ ON DUPLICATE KEY UPDATE os_id = VALUES(os_id)`, hostID, osID,
+ )
+ return err
}
// getIDHostOperatingSystemDB queries the `host_operating_system` table and returns the
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index d80dd2d60b..1ad7ef8f47 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
+ "errors"
"fmt"
"sort"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/doug-martin/goqu/v9"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
+ kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/jmoiron/sqlx"
)
@@ -110,8 +112,8 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
// SavePolicy updates some fields of the given policy on the datastore.
//
-// Currently SavePolicy does not allow updating the team of an existing policy.
-func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool) error {
+// Currently, SavePolicy does not allow updating the team of an existing policy.
+func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool, removePolicyStats bool) error {
// We must normalize the name for full Unicode support (Unicode equivalence).
p.Name = norm.NFC.String(p.Name)
sql := `
@@ -133,10 +135,39 @@ func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemo
return ctxerr.Wrap(ctx, notFound("Policy").WithID(p.ID))
}
+ return cleanupPolicy(ctx, ds.writer(ctx), p.ID, p.Platform, shouldRemoveAllPolicyMemberships, removePolicyStats, ds.logger)
+}
+
+func cleanupPolicy(
+ ctx context.Context, extContext sqlx.ExtContext, policyID uint, policyPlatform string, shouldRemoveAllPolicyMemberships bool,
+ removePolicyStats bool, logger kitlog.Logger,
+) error {
+ var err error
if shouldRemoveAllPolicyMemberships {
- return ds.cleanupPolicyMembershipForPolicy(ctx, p.ID)
+ err = cleanupPolicyMembershipForPolicy(ctx, extContext, policyID)
+ } else {
+ err = cleanupPolicyMembershipOnPolicyUpdate(ctx, extContext, policyID, policyPlatform)
}
- return cleanupPolicyMembershipOnPolicyUpdate(ctx, ds.writer(ctx), p.ID, p.Platform)
+ if err != nil {
+ return err
+ }
+ if removePolicyStats {
+ // delete all policy stats for the policy
+ fn := func(tx sqlx.ExtContext) error {
+ _, err := tx.ExecContext(ctx, `DELETE FROM policy_stats WHERE policy_id = ?`, policyID)
+ return err
+ }
+ if _, isDB := extContext.(*sqlx.DB); isDB {
+ // wrapping in a retry to avoid deadlocks with the cleanups_then_aggregation cron job
+ err = withRetryTxx(ctx, extContext.(*sqlx.DB), fn, logger)
+ } else {
+ err = fn(extContext)
+ }
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "cleanup policy stats")
+ }
+ }
+ return nil
}
// FlippingPoliciesForHost fetches previous policy membership results and returns:
@@ -576,8 +607,74 @@ func (ds *Datastore) TeamPolicy(ctx context.Context, teamID uint, policyID uint)
// NOTE: Similar to ApplyQueries, ApplyPolicySpecs will update the author_id of the policies
// that are updated.
//
-// Currently ApplyPolicySpecs does not allow updating the team of an existing policy.
+// Currently, ApplyPolicySpecs does not allow updating the team of an existing policy.
func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
+ // Use the same DB for all operations in this method for performance
+ queryerContext := ds.writer(ctx)
+
+ // Preprocess specs and group them by team
+ teamNameToID := make(map[string]uint, 1)
+ teamIDToPolicies := make(map[uint][]*fleet.PolicySpec, 1)
+
+ // Get the team IDs
+ for _, spec := range specs {
+ // We must normalize the name for full Unicode support (Unicode equivalence).
+ spec.Name = norm.NFC.String(spec.Name)
+ spec.Team = norm.NFC.String(spec.Team)
+ teamID, ok := teamNameToID[spec.Team]
+ if !ok {
+ if spec.Team != "" {
+ // if team name is not empty, it must have a team ID; otherwise teamID defaults to 0 value
+ err := sqlx.GetContext(ctx, queryerContext, &teamID, `SELECT id FROM teams WHERE name = ?`, spec.Team)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return ctxerr.Wrap(ctx, notFound("Team").WithName(spec.Team), "get team id")
+ }
+ return ctxerr.Wrap(ctx, err, "get team id")
+ }
+ }
+ teamNameToID[spec.Team] = teamID
+ }
+ teamIDToPolicies[teamID] = append(teamIDToPolicies[teamID], spec)
+ }
+
+ // Get the query and platforms of the current policies so that we can check if query or platform changed later, if needed
+ type policyLite struct {
+ Name string `db:"name"`
+ Query string `db:"query"`
+ Platforms string `db:"platforms"`
+ }
+ teamIDToPoliciesByName := make(map[uint]map[string]policyLite, len(teamIDToPolicies))
+ for teamID, teamPolicySpecs := range teamIDToPolicies {
+ teamIDToPoliciesByName[teamID] = make(map[string]policyLite, len(teamPolicySpecs))
+ policyNames := make([]string, 0, len(teamPolicySpecs))
+ for _, spec := range teamPolicySpecs {
+ policyNames = append(policyNames, spec.Name)
+ }
+
+ var query string
+ var args []interface{}
+ var err error
+ if teamID == 0 {
+ query, args, err = sqlx.In("SELECT name, query, platforms FROM policies WHERE team_id IS NULL AND name IN (?)", policyNames)
+ } else {
+ query, args, err = sqlx.In(
+ "SELECT name, query, platforms FROM policies WHERE team_id = ? AND name IN (?)", &teamID, policyNames,
+ )
+ }
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "building query to get policies by name")
+ }
+ policies := make([]policyLite, 0, len(teamPolicySpecs))
+ err = sqlx.SelectContext(ctx, queryerContext, &policies, query, args...)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "getting policies by name")
+ }
+ for _, p := range policies {
+ teamIDToPoliciesByName[teamID][p.Name] = p
+ }
+ }
+
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
query := fmt.Sprintf(
`
@@ -592,7 +689,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
critical,
calendar_events_enabled,
checksum
- ) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?), ?, ?, ?, %s)
+ ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)
ON DUPLICATE KEY UPDATE
query = VALUES(query),
description = VALUES(description),
@@ -603,24 +700,45 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
calendar_events_enabled = VALUES(calendar_events_enabled)
`, policiesChecksumComputedColumn(),
)
- for _, spec := range specs {
-
- // We must normalize the name for full Unicode support (Unicode equivalence).
- spec.Name = norm.NFC.String(spec.Name)
- res, err := tx.ExecContext(ctx,
- query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform, spec.Critical,
- spec.CalendarEventsEnabled,
- )
- if err != nil {
- return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
+ for teamID, teamPolicySpecs := range teamIDToPolicies {
+ var teamIDPtr *uint
+ if teamID != 0 {
+ teamIDPtr = &teamID
}
+ for _, spec := range teamPolicySpecs {
- if insertOnDuplicateDidUpdate(res) {
- // when the upsert results in an UPDATE that *did* change some values,
- // it returns the updated ID as last inserted id.
- if lastID, _ := res.LastInsertId(); lastID > 0 {
- if err := cleanupPolicyMembershipOnPolicyUpdate(ctx, tx, uint(lastID), spec.Platform); err != nil {
- return err
+ res, err := tx.ExecContext(
+ ctx,
+ query, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamIDPtr, spec.Platform, spec.Critical,
+ spec.CalendarEventsEnabled,
+ )
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
+ }
+
+ if insertOnDuplicateDidUpdate(res) {
+ // when the upsert results in an UPDATE that *did* change some values,
+ // it returns the updated ID as last inserted id.
+ if lastID, _ := res.LastInsertId(); lastID > 0 {
+ var (
+ shouldRemoveAllPolicyMemberships bool
+ removePolicyStats bool
+ )
+ // Figure out if the query or platform changed
+ if prev, ok := teamIDToPoliciesByName[teamID][spec.Name]; ok {
+ switch {
+ case prev.Query != spec.Query:
+ shouldRemoveAllPolicyMemberships = true
+ removePolicyStats = true
+ case prev.Platforms != spec.Platform:
+ removePolicyStats = true
+ }
+ }
+ if err = cleanupPolicy(
+ ctx, tx, uint(lastID), spec.Platform, shouldRemoveAllPolicyMemberships, removePolicyStats, ds.logger,
+ ); err != nil {
+ return err
+ }
}
}
}
@@ -739,7 +857,7 @@ func cleanupPolicyMembershipOnPolicyUpdate(ctx context.Context, db sqlx.ExecerCo
// cleanupPolicyMembership is similar to cleanupPolicyMembershipOnPolicyUpdate but without the platform constraints.
// Used when we want to remove all policy membership.
-func (ds *Datastore) cleanupPolicyMembershipForPolicy(ctx context.Context, policyID uint) error {
+func cleanupPolicyMembershipForPolicy(ctx context.Context, exec sqlx.ExecerContext, policyID uint) error {
// delete all policy memberships for the policy
delStmt := `
DELETE
@@ -754,21 +872,11 @@ func (ds *Datastore) cleanupPolicyMembershipForPolicy(ctx context.Context, polic
pm.policy_id = ?
`
- _, err := ds.writer(ctx).ExecContext(ctx, delStmt, policyID)
+ _, err := exec.ExecContext(ctx, delStmt, policyID)
if err != nil {
return ctxerr.Wrap(ctx, err, "cleanup policy membership")
}
- // delete all policy stats for the policy
- // wrapping in a retry to avoid deadlocks with the cleanups_then_aggregation cron job
- err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
- _, err := tx.ExecContext(ctx, `DELETE FROM policy_stats WHERE policy_id = ?`, policyID)
- return err
- })
- if err != nil {
- return ctxerr.Wrap(ctx, err, "cleanup policy stats")
- }
-
return nil
}
diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go
index 045d850fa1..90d9015489 100644
--- a/server/datastore/mysql/policies_test.go
+++ b/server/datastore/mysql/policies_test.go
@@ -40,6 +40,7 @@ func TestPolicies(t *testing.T) {
{"PoliciesByID", testPoliciesByID},
{"TeamPolicyTransfer", testTeamPolicyTransfer},
{"ApplyPolicySpec", testApplyPolicySpec},
+ {"ApplyPolicySpecWithQueryPlatformChanges", testApplyPolicySpecWithQueryPlatformChanges},
{"Save", testPoliciesSave},
{"DelUser", testPoliciesDelUser},
{"FlippingPoliciesForHost", testFlippingPoliciesForHost},
@@ -1400,6 +1401,298 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
}))
}
+func testApplyPolicySpecWithQueryPlatformChanges(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ unicode, _ := strconv.Unquote(`"\uAC00"`) // 가
+ unicodeEq, _ := strconv.Unquote(`"\u1100\u1161"`) // ᄀ + ᅡ
+
+ user1 := test.NewUser(t, ds, "User1", "user1@example.com", true)
+ team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1" + unicode})
+ require.NoError(t, err)
+
+ globalNames := []string{"global query1" + unicode, "global query2" + unicode, "global query3" + unicode}
+ teamNames := []string{"team query1", "team query2", "team query3"}
+ require.NoError(
+ t, ds.ApplyPolicySpecs(
+ ctx, user1.ID, []*fleet.PolicySpec{
+ {
+ Name: globalNames[0],
+ Query: "select 1;",
+ Team: "",
+ Platform: "",
+ },
+ {
+ Name: globalNames[1],
+ Query: "select 2;",
+ Team: "",
+ Platform: "darwin",
+ },
+ {
+ Name: globalNames[2],
+ Query: "select 3;",
+ Team: "",
+ Platform: "darwin,linux",
+ },
+ {
+ Name: teamNames[0],
+ Query: "select 1;",
+ Team: "team1" + unicode,
+ Platform: "",
+ },
+ {
+ Name: teamNames[1],
+ Query: "select 2;",
+ Team: "team1" + unicode,
+ Platform: "darwin",
+ },
+ {
+ Name: teamNames[2],
+ Query: "select 3;",
+ Team: "team1" + unicodeEq,
+ Platform: "darwin,linux",
+ },
+ },
+ ),
+ )
+
+ // create hosts with different platforms, for that team
+ const hostWin, hostMac, hostDeb, hostLin = 0, 1, 2, 3
+ platforms := []string{"windows", "darwin", "debian", "linux"}
+ teamHosts := make([]*fleet.Host, len(platforms))
+ for i, pl := range platforms {
+ id := fmt.Sprintf("%s-%d", strings.ReplaceAll(t.Name(), "/", "_"), i)
+ h, err := ds.NewHost(
+ ctx, &fleet.Host{
+ OsqueryHostID: &id,
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: &id,
+ UUID: id,
+ Hostname: id,
+ Platform: pl,
+ TeamID: ptr.Uint(team1.ID),
+ },
+ )
+ require.NoError(t, err)
+ teamHosts[i] = h
+ }
+
+ // create hosts with different platforms, without team
+ globalHosts := make([]*fleet.Host, len(platforms))
+ for i, pl := range platforms {
+ id := fmt.Sprintf("g%s-%d", strings.ReplaceAll(t.Name(), "/", "_"), i)
+ h, err := ds.NewHost(
+ ctx, &fleet.Host{
+ OsqueryHostID: &id,
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: &id,
+ UUID: id,
+ Hostname: id,
+ Platform: pl,
+ },
+ )
+ require.NoError(t, err)
+ globalHosts[i] = h
+ }
+
+ // load the global policies
+ gPolicies, err := ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, gPolicies, 3)
+ // load the team policies
+ tPolicies, _, err := ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, tPolicies, 3)
+
+ // index the policies by name for easier access in the rest of the test
+ polsByName := make(map[string]*fleet.Policy, len(gPolicies)+len(tPolicies))
+ globalPolsByName := make(map[string]*fleet.Policy, len(gPolicies))
+ for _, pol := range tPolicies {
+ polsByName[pol.Name] = pol
+ }
+ for _, pol := range gPolicies {
+ globalPolsByName[pol.Name] = pol
+ polsByName[pol.Name] = pol
+ }
+
+ // record some results for each policy
+ // Note: we are adding results to hosts that shouldn't have results, based on their platform.
+ for _, h := range teamHosts {
+ res := make(map[uint]*bool, len(polsByName))
+ for _, pol := range polsByName {
+ res[pol.ID] = ptr.Bool(false)
+ }
+ err = ds.RecordPolicyQueryExecutions(ctx, h, res, time.Now(), false)
+ require.NoError(t, err)
+ }
+ for _, h := range globalHosts {
+ res := make(map[uint]*bool, len(globalPolsByName))
+ for _, pol := range globalPolsByName {
+ res[pol.ID] = ptr.Bool(false)
+ }
+ err = ds.RecordPolicyQueryExecutions(ctx, h, res, time.Now(), false)
+ require.NoError(t, err)
+ }
+ err = ds.UpdateHostPolicyCounts(ctx)
+ require.NoError(t, err)
+
+ // Update host failure counts and ensure they are correct
+ teamHosts, err = ds.UpdatePolicyFailureCountsForHosts(ctx, teamHosts)
+ require.NoError(t, err)
+ assert.Equal(t, 6, teamHosts[hostWin].FailingPoliciesCount)
+ assert.Equal(t, 6, teamHosts[hostMac].FailingPoliciesCount)
+ assert.Equal(t, 6, teamHosts[hostDeb].FailingPoliciesCount)
+ assert.Equal(t, 6, teamHosts[hostLin].FailingPoliciesCount)
+ globalHosts, err = ds.UpdatePolicyFailureCountsForHosts(ctx, globalHosts)
+ require.NoError(t, err)
+ assert.Equal(t, 3, globalHosts[hostWin].FailingPoliciesCount)
+ assert.Equal(t, 3, globalHosts[hostMac].FailingPoliciesCount)
+ assert.Equal(t, 3, globalHosts[hostDeb].FailingPoliciesCount)
+ assert.Equal(t, 3, globalHosts[hostLin].FailingPoliciesCount)
+
+ // Ensure policy passing and failing counts are correct
+ gPolicies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, gPolicies, 3)
+ tPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, tPolicies, 3)
+
+ for _, pol := range gPolicies {
+ polsByName[pol.Name] = pol
+ }
+ for _, pol := range tPolicies {
+ polsByName[pol.Name] = pol
+ }
+ assert.Equal(t, uint(8), polsByName[globalNames[0]].FailingHostCount)
+ assert.Equal(t, uint(8), polsByName[globalNames[1]].FailingHostCount)
+ assert.Equal(t, uint(8), polsByName[globalNames[2]].FailingHostCount)
+ assert.Equal(t, uint(4), polsByName[teamNames[0]].FailingHostCount)
+ assert.Equal(t, uint(4), polsByName[teamNames[1]].FailingHostCount)
+ assert.Equal(t, uint(4), polsByName[teamNames[2]].FailingHostCount)
+
+ // Update policies
+ require.NoError(
+ t, ds.ApplyPolicySpecs(
+ ctx, user1.ID, []*fleet.PolicySpec{
+ {
+ Name: globalNames[0],
+ Query: "select 1;",
+ Team: "",
+ Platform: "",
+ Description: "updated", // update description
+ },
+ {
+ Name: globalNames[1],
+ Query: "select 2 updated;", // update query
+ Team: "",
+ Platform: "darwin",
+ },
+ {
+ Name: globalNames[2],
+ Query: "select 3;",
+ Team: "",
+ Platform: "darwin", // update platform
+ },
+ {
+ Name: "new global query",
+ Query: "select 4;",
+ Team: "",
+ Platform: "",
+ },
+ {
+ Name: teamNames[0],
+ Query: "select 1;",
+ Team: "team1" + unicode,
+ Platform: "linux", // update platform
+ },
+ {
+ Name: teamNames[1],
+ Query: "select 2;",
+ Team: "team1" + unicode,
+ Platform: "darwin",
+ CalendarEventsEnabled: true, // update calendar events
+ },
+ {
+ Name: teamNames[2],
+ Query: "select 3 updated;", // update query
+ Team: "team1" + unicodeEq,
+ Platform: "darwin,linux",
+ },
+ {
+ Name: "new team query",
+ Query: "select 4;",
+ Team: "team1" + unicode,
+ Platform: "",
+ },
+ },
+ ),
+ )
+
+ // Update host failure counts and ensure they are correct
+ teamHosts, err = ds.UpdatePolicyFailureCountsForHosts(ctx, teamHosts)
+ require.NoError(t, err)
+ assert.Equal(t, 1, teamHosts[hostWin].FailingPoliciesCount) // kept result from globalNames[0]
+ assert.Equal(t, 3, teamHosts[hostMac].FailingPoliciesCount)
+ assert.Equal(t, 2, teamHosts[hostDeb].FailingPoliciesCount)
+ assert.Equal(t, 2, teamHosts[hostLin].FailingPoliciesCount)
+ globalHosts, err = ds.UpdatePolicyFailureCountsForHosts(ctx, globalHosts)
+ require.NoError(t, err)
+ assert.Equal(t, 1, globalHosts[hostWin].FailingPoliciesCount)
+ assert.Equal(t, 2, globalHosts[hostMac].FailingPoliciesCount)
+ assert.Equal(t, 1, globalHosts[hostDeb].FailingPoliciesCount)
+ assert.Equal(t, 1, globalHosts[hostLin].FailingPoliciesCount)
+
+ // Ensure policy passing and failing counts are correct
+ gPolicies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, gPolicies, 4)
+ tPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, tPolicies, 4)
+
+ for _, pol := range gPolicies {
+ polsByName[pol.Name] = pol
+ }
+ for _, pol := range tPolicies {
+ polsByName[pol.Name] = pol
+ }
+ assert.Equal(t, uint(8), polsByName[globalNames[0]].FailingHostCount)
+ assert.Equal(t, uint(0), polsByName[globalNames[1]].FailingHostCount) // updated query
+ assert.Equal(t, uint(0), polsByName[globalNames[2]].FailingHostCount) // updated platform
+ assert.Equal(t, uint(0), polsByName[teamNames[0]].FailingHostCount) // updated platform
+ assert.Equal(t, uint(4), polsByName[teamNames[1]].FailingHostCount)
+ assert.Equal(t, uint(0), polsByName[teamNames[2]].FailingHostCount) // updated query
+
+ err = ds.UpdateHostPolicyCounts(ctx)
+ require.NoError(t, err)
+ gPolicies, err = ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, gPolicies, 4)
+ tPolicies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
+ require.NoError(t, err)
+ require.Len(t, tPolicies, 4)
+
+ for _, pol := range gPolicies {
+ polsByName[pol.Name] = pol
+ }
+ for _, pol := range tPolicies {
+ polsByName[pol.Name] = pol
+ }
+ assert.Equal(t, uint(8), polsByName[globalNames[0]].FailingHostCount) // platform is "" -- no change
+ assert.Equal(t, uint(0), polsByName[globalNames[1]].FailingHostCount) // updated query
+ assert.Equal(t, uint(2), polsByName[globalNames[2]].FailingHostCount) // updated platform
+ assert.Equal(t, uint(2), polsByName[teamNames[0]].FailingHostCount) // updated platform
+ assert.Equal(t, uint(1), polsByName[teamNames[1]].FailingHostCount) // platform is "darwin" -- no change
+ assert.Equal(t, uint(0), polsByName[teamNames[2]].FailingHostCount) // updated query
+
+}
+
func testPoliciesSave(t *testing.T, ds *Datastore) {
user1 := test.NewUser(t, ds, "User1", "user1@example.com", true)
ctx := context.Background()
@@ -1412,7 +1705,8 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
Name: "non-existent query",
Query: "select 1;",
},
- }, false)
+ }, false, false,
+ )
require.Error(t, err)
var nfe *notFoundError
require.True(t, errors.As(err, &nfe))
@@ -1473,7 +1767,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
gp2 := *gp
gp2.Name = "global query updated"
gp2.Critical = true
- err = ds.SavePolicy(ctx, &gp2, false)
+ err = ds.SavePolicy(ctx, &gp2, false, false)
require.NoError(t, err)
gp, err = ds.Policy(ctx, gp.ID)
require.NoError(t, err)
@@ -1493,7 +1787,7 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
tp2.Resolution = ptr.String("team1 query resolution updated")
tp2.Critical = false
tp2.CalendarEventsEnabled = false
- err = ds.SavePolicy(ctx, &tp2, true)
+ err = ds.SavePolicy(ctx, &tp2, true, true)
require.NoError(t, err)
tp1, err = ds.Policy(ctx, tp1.ID)
tp2.UpdateCreateTimestamps = tp1.UpdateCreateTimestamps
@@ -1586,7 +1880,7 @@ func testCachedPolicyCountDeletesOnPolicyChange(t *testing.T, ds *Datastore) {
assert.Equal(t, uint(1), inheritedPolicies[0].PassingHostCount)
// Update the global policy sql to trigger a cache invalidation
- err = ds.SavePolicy(ctx, globalPolicy, true)
+ err = ds.SavePolicy(ctx, globalPolicy, true, true)
require.NoError(t, err)
globalPolicy, err = ds.Policy(ctx, globalPolicy.ID)
@@ -1599,8 +1893,8 @@ func testCachedPolicyCountDeletesOnPolicyChange(t *testing.T, ds *Datastore) {
assert.Equal(t, uint(1), teamPolicies[0].PassingHostCount)
assert.Equal(t, uint(0), inheritedPolicies[0].PassingHostCount)
- // Update the team policy sql to trigger a cache invalidation
- err = ds.SavePolicy(ctx, teamPolicy, true)
+ // Update the team policy platform to trigger a cache invalidation
+ err = ds.SavePolicy(ctx, teamPolicy, false, true)
require.NoError(t, err)
teamPolicies, inheritedPolicies, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{})
@@ -1921,9 +2215,9 @@ func testPolicyPlatformUpdate(t *testing.T, ds *Datastore) {
}
// updating without change works fine
- err = ds.SavePolicy(ctx, polsByName["g1"], false)
+ err = ds.SavePolicy(ctx, polsByName["g1"], false, false)
require.NoError(t, err)
- err = ds.SavePolicy(ctx, polsByName["t2"], false)
+ err = ds.SavePolicy(ctx, polsByName["t2"], false, false)
require.NoError(t, err)
// apply specs that result in an update (without change) works fine
err = ds.ApplyPolicySpecs(ctx, user.ID, []*fleet.PolicySpec{
@@ -1975,7 +2269,7 @@ func testPolicyPlatformUpdate(t *testing.T, ds *Datastore) {
g1 := polsByName["g1"]
g1.Platform = "linux"
polsByName["g1"] = g1
- err = ds.SavePolicy(ctx, g1, false)
+ err = ds.SavePolicy(ctx, g1, false, false)
require.NoError(t, err)
wantHostsByPol["g1"] = []uint{globalHosts[hostDeb].ID, globalHosts[hostLin].ID}
assertPolicyMembership(t, ds, polsByName, wantHostsByPol)
@@ -1984,7 +2278,7 @@ func testPolicyPlatformUpdate(t *testing.T, ds *Datastore) {
t1 := polsByName["t1"]
t1.Platform = "windows,darwin"
polsByName["t1"] = t1
- err = ds.SavePolicy(ctx, t1, false)
+ err = ds.SavePolicy(ctx, t1, false, false)
require.NoError(t, err)
wantHostsByPol["t1"] = []uint{teamHosts[hostWin].ID, teamHosts[hostMac].ID}
assertPolicyMembership(t, ds, polsByName, wantHostsByPol)
@@ -2723,7 +3017,7 @@ func testPoliciesNameUnicode(t *testing.T, ds *Datastore) {
policyEmoji, err := ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{Name: "💻"})
require.NoError(t, err)
err = ds.SavePolicy(
- context.Background(), &fleet.Policy{PolicyData: fleet.PolicyData{ID: policyEmoji.ID, Name: equivalentNames[1]}}, false,
+ context.Background(), &fleet.Policy{PolicyData: fleet.PolicyData{ID: policyEmoji.ID, Name: equivalentNames[1]}}, false, false,
)
assert.True(t, isDuplicate(err), err)
@@ -3270,10 +3564,10 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
//
team2Policy1.Platform = "darwin"
- err = ds.SavePolicy(ctx, team1Policy1, false)
+ err = ds.SavePolicy(ctx, team1Policy1, false, true)
require.NoError(t, err)
team1Policy1.Platform = "darwin"
- err = ds.SavePolicy(ctx, team2Policy1, false)
+ err = ds.SavePolicy(ctx, team2Policy1, false, true)
require.NoError(t, err)
//
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 783c8be08e..861cc47d6e 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
+INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `calendar_events` (
diff --git a/server/fleet/app.go b/server/fleet/app.go
index ac056347f0..8f4525a595 100644
--- a/server/fleet/app.go
+++ b/server/fleet/app.go
@@ -434,8 +434,9 @@ type AppConfig struct {
// SMTPSettings holds the SMTP integration settings.
//
// This field is a pointer to avoid returning this information to non-global-admins.
- SMTPSettings *SMTPSettings `json:"smtp_settings,omitempty"`
- HostExpirySettings HostExpirySettings `json:"host_expiry_settings"`
+ SMTPSettings *SMTPSettings `json:"smtp_settings,omitempty"`
+ HostExpirySettings HostExpirySettings `json:"host_expiry_settings"`
+ ActivityExpirySettings ActivityExpirySettings `json:"activity_expiry_settings"`
// Features allows to globally enable or disable features
Features Features `json:"features"`
DeprecatedHostSettings *Features `json:"host_settings,omitempty"`
@@ -888,6 +889,12 @@ type HostExpirySettings struct {
HostExpiryWindow int `json:"host_expiry_window"`
}
+// ActivityExpirySettings contains settings pertaining to automatic activities cleanup.
+type ActivityExpirySettings struct {
+ ActivityExpiryEnabled bool `json:"activity_expiry_enabled"`
+ ActivityExpiryWindow int `json:"activity_expiry_window"`
+}
+
type Features struct {
EnableHostUsers bool `json:"enable_host_users"`
EnableSoftwareInventory bool `json:"enable_software_inventory"`
diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go
index 71de52073a..04f5742e61 100644
--- a/server/fleet/apple_mdm.go
+++ b/server/fleet/apple_mdm.go
@@ -161,15 +161,6 @@ type EnrolledAPIResult struct {
// EnrolledAPIResults is a map of enrollments to a per-enrollment API result.
type EnrolledAPIResults map[string]*EnrolledAPIResult
-// MDMAppleHostDetails represents the device identifiers used to ingest an MDM device as a Fleet
-// host pending enrollment.
-// See also https://developer.apple.com/documentation/devicemanagement/authenticaterequest.
-type MDMAppleHostDetails struct {
- SerialNumber string
- UDID string
- Model string
-}
-
type MDMAppleCommandTimeoutError struct{}
func (e MDMAppleCommandTimeoutError) Error() string {
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 36c6ff1dd1..d5d1a26d6a 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -570,7 +570,10 @@ type Datastore interface {
// upgraded from a prior version).
CleanupHostOperatingSystems(ctx context.Context) error
- UpdateHostTablesOnMDMUnenroll(ctx context.Context, uuid string) error
+ // MDMTurnOff updates Fleet host information related to MDM when a
+ // host turns off MDM. Anything related to the protocol itself is
+ // managed separately.
+ MDMTurnOff(ctx context.Context, uuid string) error
///////////////////////////////////////////////////////////////////////////////
// ActivitiesStore
@@ -604,7 +607,7 @@ type Datastore interface {
// SavePolicy updates some fields of the given policy on the datastore.
//
// It is also used to update team policies.
- SavePolicy(ctx context.Context, p *Policy, shouldRemoveAllPolicyMemberships bool) error
+ SavePolicy(ctx context.Context, p *Policy, shouldRemoveAllPolicyMemberships bool, removePolicyStats bool) error
ListGlobalPolicies(ctx context.Context, opts ListOptions) ([]*Policy, error)
PoliciesByID(ctx context.Context, ids []uint) (map[uint]*Policy, error)
@@ -1042,16 +1045,16 @@ type Datastore interface {
// joined (nil for no team), and an error.
IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devices []godep.Device) (int64, *uint, error)
- // IngestMDMAppleDeviceFromCheckin creates a new Fleet host record for an MDM-enrolled device that is
- // not already enrolled in Fleet.
- IngestMDMAppleDeviceFromCheckin(ctx context.Context, mdmHost MDMAppleHostDetails) error
+ // MDMAppleUpsertHost creates or matches a Fleet host record for an
+ // MDM-enrolled device.
+ MDMAppleUpsertHost(ctx context.Context, mdmHost *Host) error
// RestoreMDMApplePendingDEPHost restores a host that was previously deleted from Fleet.
RestoreMDMApplePendingDEPHost(ctx context.Context, host *Host) error
- // ResetMDMAppleEnrollment resets all tables with enrollment-related
+ // MDMResetEnrollment resets all tables with enrollment-related
// information if a matching row for the host exists.
- ResetMDMAppleEnrollment(ctx context.Context, hostUUID string) error
+ MDMResetEnrollment(ctx context.Context, hostUUID string) error
// ListMDMAppleDEPSerialsInTeam returns a list of serial numbers of hosts
// that are enrolled or pending enrollment in Fleet's MDM via DEP for the
@@ -1432,6 +1435,11 @@ type Datastore interface {
// CleanupUnusedScriptContents will remove script contents that have no references to them from
// the scripts or host_script_results tables.
CleanupUnusedScriptContents(ctx context.Context) error
+ // CleanupActivitiesAndAssociatedData will cleanup (up to maxCount) activities and their associated data
+ // that are older than the given expiration window.
+ //
+ // The argument maxCount is used to not lock the database for long periods of time.
+ CleanupActivitiesAndAssociatedData(ctx context.Context, maxCount int, expiryWindowDays int) error
// WipeHostViaScript sends a script to wipe a host and updates the
// states in host_mdm_actions.
WipeHostViaScript(ctx context.Context, request *HostScriptRequestPayload, hostFleetPlatform string) error
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index ae0124c0d8..0ed16f60c3 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -361,7 +361,9 @@ type Host struct {
// HostOrbitInfo maps to the host_orbit_info table in the database, which maps to the orbit_info agent table.
type HostOrbitInfo struct {
- ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"`
+ Version string `json:"version" db:"version"`
+ DesktopVersion *string `json:"desktop_version" db:"desktop_version"`
+ ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"`
}
// HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with
diff --git a/server/mdm/apple/mobileconfig/mobileconfig.go b/server/mdm/apple/mobileconfig/mobileconfig.go
index ac7b11f89a..8fecaed417 100644
--- a/server/mdm/apple/mobileconfig/mobileconfig.go
+++ b/server/mdm/apple/mobileconfig/mobileconfig.go
@@ -8,8 +8,11 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/mdm"
+
+ // we are using this package as we were having issues with pasrsing signed apple
+ // mobileconfig profiles with the pcks7 package we were using before.
+ cms "github.com/github/smimesign/ietf-cms"
"github.com/micromdm/micromdm/pkg/crypto/profileutil"
- "go.mozilla.org/pkcs7"
"howett.net/plist"
)
@@ -81,6 +84,24 @@ type Parsed struct {
PayloadType string
}
+func (mc Mobileconfig) isSignedProfile() bool {
+ return !bytes.HasPrefix(bytes.TrimSpace(mc), []byte(" 0 {
- err := svc.checkWriteForHostIDs(ctx, ids)
+ if err := svc.checkWriteForHostIDs(ctx, ids); err != nil {
+ return err
+ }
+
+ hosts, err := svc.ds.ListHostsLiteByIDs(ctx, ids)
if err != nil {
return err
}
- return svc.ds.DeleteHosts(ctx, ids)
+
+ return doDelete(ids, hosts)
}
if opts == nil {
opts = &fleet.HostListOptions{}
}
opts.DisableFailingPolicies = true // don't check policies for hosts that are about to be deleted
- hostIDs, _, err := svc.hostIDsAndNamesFromFilters(ctx, *opts, lid)
+ hostIDs, _, hosts, err := svc.hostIDsAndNamesFromFilters(ctx, *opts, lid)
if err != nil {
return err
}
@@ -309,7 +335,8 @@ func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, filter *map[str
if err != nil {
return err
}
- return svc.ds.DeleteHosts(ctx, hostIDs)
+
+ return doDelete(hostIDs, hosts)
}
/////////////////////////////////////////////////////////////////////////////////
@@ -719,8 +746,15 @@ func (svc *Service) DeleteHost(ctx context.Context, id uint) error {
return ctxerr.Wrap(ctx, err, "delete host")
}
- if host.Platform == "darwin" {
- return svc.maybeRestorePendingDEPHost(ctx, host)
+ if host.Platform == "windows" || host.Platform == "darwin" {
+ mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger)
+ err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
+ Action: mdmlifecycle.HostActionDelete,
+ Platform: host.Platform,
+ UUID: host.UUID,
+ Host: host,
+ })
+ return ctxerr.Wrap(ctx, err, "performing MDM actions after delete")
}
return nil
@@ -888,7 +922,7 @@ func (svc *Service) AddHostsToTeamByFilter(ctx context.Context, teamID *uint, fi
return &fleet.BadRequestError{Message: "filters must be specified"}
}
- hostIDs, hostNames, err := svc.hostIDsAndNamesFromFilters(ctx, *opt, lid)
+ hostIDs, hostNames, _, err := svc.hostIDsAndNamesFromFilters(ctx, *opt, lid)
if err != nil {
return err
}
@@ -1241,10 +1275,10 @@ func (svc *Service) GetHostQueryReportResults(ctx context.Context, hostID uint,
return result, lastFetched, nil
}
-func (svc *Service) hostIDsAndNamesFromFilters(ctx context.Context, opt fleet.HostListOptions, lid *uint) ([]uint, []string, error) {
+func (svc *Service) hostIDsAndNamesFromFilters(ctx context.Context, opt fleet.HostListOptions, lid *uint) ([]uint, []string, []*fleet.Host, error) {
filter, err := processHostFilters(ctx, opt, lid)
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
// Load hosts, either from label if provided or from all hosts.
@@ -1256,11 +1290,11 @@ func (svc *Service) hostIDsAndNamesFromFilters(ctx context.Context, opt fleet.Ho
hosts, err = svc.ds.ListHosts(ctx, filter, opt)
}
if err != nil {
- return nil, nil, err
+ return nil, nil, nil, err
}
if len(hosts) == 0 {
- return nil, nil, nil
+ return nil, nil, nil, nil
}
hostIDs := make([]uint, 0, len(hosts))
@@ -1269,7 +1303,7 @@ func (svc *Service) hostIDsAndNamesFromFilters(ctx context.Context, opt fleet.Ho
hostIDs = append(hostIDs, h.ID)
hostNames = append(hostNames, h.DisplayName())
}
- return hostIDs, hostNames, nil
+ return hostIDs, hostNames, hosts, nil
}
func processHostFilters(ctx context.Context, opt fleet.HostListOptions, lid *uint) (fleet.TeamFilter, error) {
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 0d5f371e44..35576136ed 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -2428,6 +2428,65 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
assert.Equal(t, uint(0), policiesResponse.Policies[0].FailingHostCount)
assert.Equal(t, uint(0), policiesResponse.Policies[0].PassingHostCount)
+ // Record query executions
+ require.NoError(
+ t, s.ds.RecordPolicyQueryExecutions(
+ context.Background(), h1.Host, map[uint]*bool{policiesResponse.Policies[0].ID: ptr.Bool(true)}, time.Now(), false,
+ ),
+ )
+ require.NoError(
+ t, s.ds.RecordPolicyQueryExecutions(
+ context.Background(), h2.Host, map[uint]*bool{policiesResponse.Policies[0].ID: nil}, time.Now(), false,
+ ),
+ )
+ // Update policy stats
+ require.NoError(t, s.ds.UpdateHostPolicyCounts(context.Background()))
+
+ // Fetch policy to make sure stats are updated
+ s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, &policiesResponse)
+ require.Len(t, policiesResponse.Policies, 1)
+ assert.Equal(t, uint(0), policiesResponse.Policies[0].FailingHostCount)
+ assert.Equal(t, uint(1), policiesResponse.Policies[0].PassingHostCount)
+
+ listHostsURL = fmt.Sprintf("/api/latest/fleet/hosts?policy_id=%d&policy_response=passing", policiesResponse.Policies[0].ID)
+ listHostsResp = listHostsResponse{}
+ s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
+ require.Len(t, listHostsResp.Hosts, 1)
+
+ // Modify the platform for the policy, which should clear the policy stats
+ mgpParams = modifyGlobalPolicyRequest{
+ ModifyPolicyPayload: fleet.ModifyPolicyPayload{
+ Platform: ptr.String("linux"),
+ },
+ }
+ mgpResp = modifyGlobalPolicyResponse{}
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", gpResp.Policy.ID), mgpParams, http.StatusOK, &mgpResp)
+ require.NotNil(t, gpResp.Policy)
+ assert.Equal(t, "TestQuery4", mgpResp.Policy.Name)
+ assert.Equal(t, "select * from users;", mgpResp.Policy.Query)
+ assert.Equal(t, "Some description updated", mgpResp.Policy.Description)
+ require.NotNil(t, mgpResp.Policy.Resolution)
+ assert.Equal(t, "some global resolution updated", *mgpResp.Policy.Resolution)
+ assert.Equal(t, "linux", mgpResp.Policy.Platform)
+ assert.Equal(t, uint(0), mgpResp.Policy.FailingHostCount)
+ assert.Equal(t, uint(0), mgpResp.Policy.PassingHostCount)
+
+ // Fetch policy to make sure stats are updated
+ s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, &policiesResponse)
+ require.Len(t, policiesResponse.Policies, 1)
+ assert.Equal(t, uint(0), policiesResponse.Policies[0].FailingHostCount)
+ assert.Equal(t, uint(0), policiesResponse.Policies[0].PassingHostCount)
+
+ listHostsURL = fmt.Sprintf("/api/latest/fleet/hosts?policy_id=%d&policy_response=passing", policiesResponse.Policies[0].ID)
+ listHostsResp = listHostsResponse{}
+ s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
+ require.Len(t, listHostsResp.Hosts, 0)
+
+ listHostsURL = fmt.Sprintf("/api/latest/fleet/hosts?policy_id=%d&policy_response=failing", policiesResponse.Policies[0].ID)
+ listHostsResp = listHostsResponse{}
+ s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
+ require.Len(t, listHostsResp.Hosts, 0)
+
deletePolicyParams := deleteGlobalPoliciesRequest{IDs: []uint{policiesResponse.Policies[0].ID}}
deletePolicyResp := deleteGlobalPoliciesResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies/delete", deletePolicyParams, http.StatusOK, &deletePolicyResp)
@@ -6115,6 +6174,8 @@ func (s *integrationTestSuite) TestAppConfig() {
assert.Equal(t, "free", acResp.License.Tier)
assert.Equal(t, "FleetTest", acResp.OrgInfo.OrgName) // set in SetupSuite
assert.False(t, acResp.MDM.AppleBMTermsExpired)
+ assert.False(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
+ assert.Zero(t, acResp.ActivityExpirySettings.ActivityExpiryWindow)
// set the apple BM terms expired flag, and the enabled and configured flags,
// we'll check again at the end of this test to make sure they weren't
@@ -6157,6 +6218,30 @@ func (s *integrationTestSuite) TestAppConfig() {
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
require.Contains(t, string(*acResp.AgentOptions), `"logger_plugin": "tls"`) // default agent options has this setting
+ // Invalid activity expiry window.
+ acResp = appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "activity_expiry_settings": {
+ "activity_expiry_enabled": true,
+ "activity_expiry_window": -1
+ }
+ }`), http.StatusUnprocessableEntity, &acResp)
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.False(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
+ require.Zero(t, acResp.ActivityExpirySettings.ActivityExpiryWindow)
+
+ // Valid activity expiry window.
+ acResp = appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "activity_expiry_settings": {
+ "activity_expiry_enabled": true,
+ "activity_expiry_window": 42
+ }
+ }`), http.StatusOK, &acResp)
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.True(t, acResp.ActivityExpirySettings.ActivityExpiryEnabled)
+ require.Equal(t, 42, acResp.ActivityExpirySettings.ActivityExpiryWindow)
+
// test a change that does clear the agent options (the field is provided but empty).
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"agent_options": {}
@@ -8863,6 +8948,20 @@ type validationErrResp struct {
} `json:"errors"`
}
+func setOrbitEnrollment(t *testing.T, h *fleet.Host, ds fleet.Datastore) string {
+ orbitKey := uuid.New().String()
+ _, err := ds.EnrollOrbit(context.Background(), false, fleet.OrbitHostInfo{
+ HardwareUUID: *h.OsqueryHostID,
+ HardwareSerial: h.HardwareSerial,
+ }, orbitKey, nil)
+ require.NoError(t, err)
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), h.ID, "1.22.0", sql.NullString{String: "42", Valid: true}, sql.NullBool{Bool: true, Valid: true},
+ )
+ require.NoError(t, err)
+ return orbitKey
+}
+
func createOrbitEnrolledHost(t *testing.T, os, suffix string, ds fleet.Datastore) *fleet.Host {
name := t.Name() + suffix
h, err := ds.NewHost(context.Background(), &fleet.Host{
@@ -8878,16 +8977,8 @@ func createOrbitEnrolledHost(t *testing.T, os, suffix string, ds fleet.Datastore
Platform: os,
})
require.NoError(t, err)
- orbitKey := uuid.New().String()
- _, err = ds.EnrollOrbit(context.Background(), false, fleet.OrbitHostInfo{
- HardwareUUID: *h.OsqueryHostID,
- HardwareSerial: h.HardwareSerial,
- }, orbitKey, nil)
- require.NoError(t, err)
- err = ds.SetOrUpdateHostOrbitInfo(
- context.Background(), h.ID, "1.22.0", sql.NullString{String: "42", Valid: true}, sql.NullBool{Bool: true, Valid: true},
- )
- require.NoError(t, err)
+
+ orbitKey := setOrbitEnrollment(t, h, ds)
h.OrbitNodeKey = &orbitKey
return h
}
diff --git a/server/service/integration_mdm_ddm_test.go b/server/service/integration_mdm_ddm_test.go
index 869277f246..4034e5540a 100644
--- a/server/service/integration_mdm_ddm_test.go
+++ b/server/service/integration_mdm_ddm_test.go
@@ -971,6 +971,9 @@ func (s *integrationMDMTestSuite) TestDDMTransactionRecording() {
// a second device requests tokens
_, mdmDeviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t)
+ err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
+ require.NoError(t, err)
+
_, err = mdmDeviceTwo.DeclarativeManagement("tokens")
require.NoError(t, err)
verifyTransactionRecord(record{
diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go
new file mode 100644
index 0000000000..bf660ec620
--- /dev/null
+++ b/server/service/integration_mdm_lifecycle_test.go
@@ -0,0 +1,762 @@
+package service
+
+import (
+ "context"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
+ "github.com/fleetdm/fleet/v4/server/config"
+ "github.com/fleetdm/fleet/v4/server/datastore/mysql"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ "github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
+ "github.com/fleetdm/fleet/v4/server/ptr"
+ kitlog "github.com/go-kit/log"
+ "github.com/google/uuid"
+ "github.com/groob/plist"
+ "github.com/jmoiron/sqlx"
+ micromdm "github.com/micromdm/micromdm/mdm/mdm"
+ "github.com/stretchr/testify/require"
+ "go.mozilla.org/pkcs7"
+)
+
+// NOTE: the mantra for lifecycle events is:
+// - Noah: When MDM is turned on, install fleetd, bootstrap package (if DEP),
+// and profiles. Don't clear host vitals (everything you see on the Host
+// details page)
+// - Noah: On re-enrollment, don't clear host vitals.
+// - Noah: On lock and wipe, don't clear host vitals.
+// - Noah: On delete, clear host vitals.
+
+// NOTE: ADE lifecycle events are part of the integration_mdm_dep_test.go file
+
+type mdmLifecycleAssertion[T any] func(t *testing.T, host *fleet.Host, device T)
+
+func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() {
+ t := s.T()
+ s.setupLifecycleSettings()
+
+ testCases := []struct {
+ Name string
+ Action mdmLifecycleAssertion[*mdmtest.TestAppleMDMClient]
+ }{
+ {
+ "wiped host turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ s.Do(
+ "POST",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID),
+ nil,
+ http.StatusNoContent,
+ )
+
+ cmd, err := device.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ cmd, err = device.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "locked host turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ s.Do(
+ "POST",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
+ nil,
+ http.StatusNoContent,
+ )
+
+ cmd, err := device.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ cmd, err = device.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "host turns on MDM features out of the blue",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "IT admin turns off MDM for a host via the UI then host turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ originalPushMock := s.pushProvider.PushFunc
+ defer func() { s.pushProvider.PushFunc = originalPushMock }()
+
+ s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
+ res, err := mockSuccessfulPush(pushes)
+ require.NoError(t, err)
+ err = device.Checkout()
+ require.NoError(t, err)
+ return res, err
+ }
+
+ s.Do(
+ "DELETE",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
+ nil,
+ http.StatusOK,
+ )
+
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "host is deleted then turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ var delResp deleteHostResponse
+ s.DoJSON(
+ "DELETE",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
+ nil,
+ http.StatusOK,
+ &delResp,
+ )
+
+ dupeClient := mdmtest.NewTestMDMClientAppleDirect(
+ mdmtest.AppleEnrollInfo{
+
+ SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
+ SCEPURL: s.server.URL + apple_mdm.SCEPPath,
+ MDMURL: s.server.URL + apple_mdm.MDMPath,
+ },
+ )
+ dupeClient.UUID = device.UUID
+ dupeClient.SerialNumber = device.SerialNumber
+ dupeClient.Model = device.Model
+ require.NoError(t, dupeClient.Enroll())
+
+ *device = *dupeClient
+ },
+ },
+ {
+ "host is deleted in bulk then turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ req := deleteHostsRequest{
+ IDs: []uint{host.ID},
+ }
+ resp := deleteHostsResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/hosts/delete", req, http.StatusOK, &resp)
+
+ dupeClient := mdmtest.NewTestMDMClientAppleDirect(
+ mdmtest.AppleEnrollInfo{
+
+ SCEPChallenge: s.fleetCfg.MDM.AppleSCEPChallenge,
+ SCEPURL: s.server.URL + apple_mdm.SCEPPath,
+ MDMURL: s.server.URL + apple_mdm.MDMPath,
+ },
+ )
+ dupeClient.UUID = device.UUID
+ dupeClient.SerialNumber = device.SerialNumber
+ dupeClient.Model = device.Model
+ require.NoError(t, dupeClient.Enroll())
+
+ *device = *dupeClient
+ },
+ },
+ {
+ "host is deleted then osquery enrolls then turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
+ var delResp deleteHostResponse
+ s.DoJSON(
+ "DELETE",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
+ nil,
+ http.StatusOK,
+ &delResp,
+ )
+
+ var err error
+ host.OsqueryHostID = ptr.String(t.Name())
+ host, err = s.ds.NewHost(context.Background(), host)
+ require.NoError(t, err)
+
+ setOrbitEnrollment(t, host, s.ds)
+ deviceToken := uuid.NewString()
+ err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, deviceToken)
+ require.NoError(t, err)
+
+ device.SetDesktopToken(deviceToken)
+ require.NoError(t, device.Enroll())
+ },
+ },
+ }
+
+ assertAction := func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient, action mdmLifecycleAssertion[*mdmtest.TestAppleMDMClient]) {
+ fCmds, fSumm, fHostMDM := s.recordAppleHostStatus(host, device)
+
+ action(t, host, device)
+
+ // reload the host by identifier, tests might
+ // delete hosts and create new records with different IDs
+ var err error
+ host, err = s.ds.HostByIdentifier(context.Background(), host.UUID)
+ require.NoError(t, err)
+
+ sCmds, sSumm, sHostMDM := s.recordAppleHostStatus(host, device)
+
+ // post asssertions
+ require.ElementsMatch(t, fCmds, sCmds)
+ require.Equal(t, fSumm, sSumm)
+ require.Equal(t, fHostMDM, sHostMDM)
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.Name, func(t *testing.T) {
+ t.Run("manual enrollment", func(t *testing.T) {
+ host, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
+ assertAction(t, host, device, tt.Action)
+ })
+
+ t.Run("automatic enrollment", func(t *testing.T) {
+ device := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, "")
+ s.mockDEPResponse(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": "xyz"}`))
+ case "/profile":
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
+ require.NoError(t, err)
+ case "/profile/devices":
+ err := encoder.Encode(godep.ProfileResponse{
+ ProfileUUID: "abc",
+ Devices: map[string]string{},
+ })
+ require.NoError(t, err)
+ case "/server/devices", "/devices/sync":
+ err := encoder.Encode(godep.DeviceResponse{
+ Devices: []godep.Device{
+ {
+ SerialNumber: device.SerialNumber,
+ Model: device.Model,
+ OS: "osx",
+ OpType: "added",
+ },
+ },
+ })
+ require.NoError(t, err)
+ }
+ }))
+
+ s.runDEPSchedule()
+ depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
+ device.SetDEPToken(depURLToken)
+ var err error
+ host, err := s.ds.HostByIdentifier(context.Background(), device.SerialNumber)
+ require.NoError(t, err)
+ require.NoError(t, device.Enroll())
+
+ assertAction(t, host, device, tt.Action)
+ })
+ })
+ }
+}
+
+func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
+ t := s.T()
+ s.setupLifecycleSettings()
+
+ testCases := []struct {
+ Name string
+ Action mdmLifecycleAssertion[*mdmtest.TestWindowsMDMClient]
+ }{
+ {
+ "wiped host turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
+ s.Do(
+ "POST",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID),
+ nil,
+ http.StatusNoContent,
+ )
+
+ status, err := s.ds.GetHostLockWipeStatus(context.Background(), host)
+ require.NoError(t, err)
+
+ cmds, err := device.StartManagementSession()
+ require.NoError(t, err)
+
+ // two status + the wipe command we enqueued
+ require.Len(t, cmds, 3)
+ wipeCmd := cmds[status.WipeMDMCommand.CommandUUID]
+ require.NotNil(t, wipeCmd)
+ require.Equal(t, wipeCmd.Verb, fleet.CmdExec)
+ require.Len(t, wipeCmd.Cmd.Items, 1)
+ require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected", *wipeCmd.Cmd.Items[0].Target)
+
+ msgID, err := device.GetCurrentMsgID()
+ require.NoError(t, err)
+
+ device.AppendResponse(fleet.SyncMLCmd{
+ XMLName: xml.Name{Local: fleet.CmdStatus},
+ MsgRef: &msgID,
+ CmdRef: &status.WipeMDMCommand.CommandUUID,
+ Cmd: ptr.String("Exec"),
+ Data: ptr.String("200"),
+ Items: nil,
+ CmdID: fleet.CmdID{Value: uuid.NewString()},
+ })
+ cmds, err = device.SendResponse()
+ require.NoError(t, err)
+ // the ack of the message should be the only returned command
+ require.Len(t, cmds, 1)
+
+ // re-enroll
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "locked host turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
+ s.Do(
+ "POST",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
+ nil,
+ http.StatusNoContent,
+ )
+
+ status, err := s.ds.GetHostLockWipeStatus(context.Background(), host)
+ require.NoError(t, err)
+
+ var orbitScriptResp orbitPostScriptResultResponse
+ s.DoJSON(
+ "POST",
+ "/api/fleet/orbit/scripts/result",
+ json.RawMessage(
+ fmt.Sprintf(
+ `{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`,
+ *host.OrbitNodeKey,
+ status.LockScript.ExecutionID,
+ ),
+ ),
+ http.StatusOK,
+ &orbitScriptResp,
+ )
+
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "host turns on MDM features out of the blue",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
+ require.NoError(t, device.Enroll())
+ },
+ },
+ {
+ "host is deleted then osquery enrolls then turns on MDM",
+ func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
+ var delResp deleteHostResponse
+ s.DoJSON(
+ "DELETE",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
+ nil,
+ http.StatusOK,
+ &delResp,
+ )
+
+ var err error
+ host.OsqueryHostID = ptr.String(t.Name())
+ host, err = s.ds.NewHost(context.Background(), host)
+ require.NoError(t, err)
+
+ orbitKey := setOrbitEnrollment(t, host, s.ds)
+ host.OrbitNodeKey = &orbitKey
+ if !strings.Contains(device.TokenIdentifier, "@") {
+ device.TokenIdentifier = orbitKey
+ }
+ device.HardwareID = host.UUID
+ device.DeviceID = host.UUID
+
+ require.NoError(t, device.Enroll())
+ },
+ },
+ }
+
+ assertAction := func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient, action mdmLifecycleAssertion[*mdmtest.TestWindowsMDMClient]) {
+ fCmds, fSumm, fHostMDM := s.recordWindowsHostStatus(host, device)
+
+ action(t, host, device)
+
+ // reload the host by identifier, tests might
+ // delete hosts and create new records with different IDs
+ var err error
+ host, err = s.ds.HostByIdentifier(context.Background(), host.UUID)
+ require.NoError(t, err)
+
+ sCmds, sSumm, sHostMDM := s.recordWindowsHostStatus(host, device)
+
+ // post asssertions
+ require.Len(t, sCmds, len(fCmds))
+ require.ElementsMatch(t, fCmds, sCmds)
+ require.Equal(t, fSumm, sSumm)
+ require.Equal(t, fHostMDM, sHostMDM)
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.Name, func(t *testing.T) {
+ t.Run("programmatic enrollment", func(t *testing.T) {
+ host, device := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
+ err := s.ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
+ require.NoError(t, err)
+ assertAction(t, host, device, tt.Action)
+ })
+
+ t.Run("automatic enrollment", func(t *testing.T) {
+ if strings.Contains(tt.Name, "wipe") {
+ t.Skip("wipe tests are not supported for windows automatic enrollment until we fix #TODO")
+ }
+
+ err := s.ds.ApplyEnrollSecrets(context.Background(), nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
+ require.NoError(t, err)
+
+ host := createOrbitEnrolledHost(t, "windows", "windows_automatic", s.ds)
+
+ azureMail := "foo.bar.baz@example.com"
+ device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail)
+ device.HardwareID = host.UUID
+ device.DeviceID = host.UUID
+ require.NoError(t, device.Enroll())
+
+ err = s.ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
+ require.NoError(t, err)
+
+ assertAction(t, host, device, tt.Action)
+ })
+ })
+ }
+}
+
+// Hardcode response type because we are using a custom json marshaling so
+// using getHostMDMResponse fails with "JSON unmarshaling is not supported for HostMDM".
+type jsonMDM struct {
+ EnrollmentStatus string `json:"enrollment_status"`
+ ServerURL string `json:"server_url"`
+ Name string `json:"name,omitempty"`
+ ID *uint `json:"id,omitempty"`
+}
+type getHostMDMResponseTest struct {
+ HostMDM *jsonMDM
+ Err error `json:"error,omitempty"`
+}
+
+func (s *integrationMDMTestSuite) recordWindowsHostStatus(
+ host *fleet.Host,
+ device *mdmtest.TestWindowsMDMClient,
+) ([]fleet.ProtoCmdOperation, getHostMDMSummaryResponse, getHostMDMResponseTest) {
+ t := s.T()
+
+ var recordedCmds []fleet.ProtoCmdOperation
+ cmds, err := device.StartManagementSession()
+ require.NoError(t, err)
+
+ msgID, err := device.GetCurrentMsgID()
+ require.NoError(t, err)
+ for _, c := range cmds {
+ cmdID := c.Cmd.CmdID
+ status := syncml.CmdStatusOK
+ device.AppendResponse(fleet.SyncMLCmd{
+ XMLName: xml.Name{Local: fleet.CmdStatus},
+ MsgRef: &msgID,
+ CmdRef: &cmdID.Value,
+ Cmd: ptr.String(c.Verb),
+ Data: &status,
+ Items: nil,
+ CmdID: fleet.CmdID{Value: uuid.NewString()},
+ })
+ c.Cmd.CmdID.Value = ""
+ c.Cmd.CmdRef = nil
+ recordedCmds = append(recordedCmds, c)
+ }
+
+ _, err = device.SendResponse()
+ require.NoError(t, err)
+
+ mdmAgg := getHostMDMSummaryResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts/summary/mdm", nil, http.StatusOK, &mdmAgg)
+
+ ghr := getHostMDMResponseTest{}
+ s.DoJSON(
+ "GET",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
+ nil,
+ http.StatusOK,
+ &ghr,
+ )
+
+ return recordedCmds, mdmAgg, ghr
+}
+
+func (s *integrationMDMTestSuite) recordAppleHostStatus(
+ host *fleet.Host,
+ device *mdmtest.TestAppleMDMClient,
+) ([]*micromdm.CommandPayload, getHostMDMSummaryResponse, getHostMDMResponseTest) {
+ t := s.T()
+
+ s.runWorker()
+ s.awaitTriggerProfileSchedule(t)
+
+ var cmds []*micromdm.CommandPayload
+
+ cmd, err := device.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ var fullCmd micromdm.CommandPayload
+ require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
+
+ // command uuid is a random value, we only care that's set
+ require.NotEmpty(t, fullCmd.CommandUUID)
+ fullCmd.CommandUUID = ""
+
+ // strip the signature of the profiles so they can be easily compared
+ if fullCmd.Command.RequestType == "InstallProfile" {
+ p7, err := pkcs7.Parse(fullCmd.Command.InstallProfile.Payload)
+ require.NoError(t, err)
+ fullCmd.Command.InstallProfile.Payload = p7.Content
+ }
+ cmds = append(cmds, &fullCmd)
+
+ cmd, err = device.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+
+ mdmAgg := getHostMDMSummaryResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts/summary/mdm", nil, http.StatusOK, &mdmAgg)
+
+ ghr := getHostMDMResponseTest{}
+ s.DoJSON(
+ "GET",
+ fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
+ nil,
+ http.StatusOK,
+ &ghr,
+ )
+
+ return cmds, mdmAgg, ghr
+}
+
+func (s *integrationMDMTestSuite) setupLifecycleSettings() {
+ t := s.T()
+ ctx := context.Background()
+ // add bootstrap package
+ _ = s.ds.DeleteMDMAppleBootstrapPackage(ctx, 0)
+ bp, err := os.ReadFile(filepath.Join("testdata", "bootstrap-packages", "signed.pkg"))
+ require.NoError(t, err)
+ s.uploadBootstrapPackage(
+ &fleet.MDMAppleBootstrapPackage{Bytes: bp, Name: "pkg.pkg", TeamID: 0},
+ http.StatusOK,
+ "",
+ )
+
+ // enable disk encryption
+ acResp := appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": { "macos_settings": {"enable_disk_encryption": true} }
+ }`), http.StatusOK, &acResp)
+ require.True(t, acResp.MDM.EnableDiskEncryption.Value)
+
+ // add profiles (windows, mac)
+ s.Do(
+ "POST",
+ "/api/v1/fleet/mdm/profiles/batch",
+ batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
+ {Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
+ {Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
+ {Name: "N3", Contents: declarationForTest("D1")},
+ }},
+ http.StatusNoContent,
+ )
+}
+
+// Host is renewing SCEP certificates
+func (s *integrationMDMTestSuite) TestLifecycleSCEPCertExpiration() {
+ t := s.T()
+ ctx := context.Background()
+ // ensure there's a token for automatic enrollments
+ s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
+ }))
+ s.runDEPSchedule()
+
+ // add a device that's manually enrolled
+ desktopToken := uuid.New().String()
+ manualHost := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
+ err := s.ds.SetOrUpdateDeviceAuthToken(context.Background(), manualHost.ID, desktopToken)
+ require.NoError(t, err)
+ manualEnrolledDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
+ manualEnrolledDevice.UUID = manualHost.UUID
+ err = manualEnrolledDevice.Enroll()
+ require.NoError(t, err)
+
+ // add a device that's automatically enrolled
+ automaticHost := createOrbitEnrolledHost(t, "darwin", "h2", s.ds)
+ depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
+ automaticEnrolledDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
+ automaticEnrolledDevice.UUID = automaticHost.UUID
+ automaticEnrolledDevice.SerialNumber = automaticHost.HardwareSerial
+ err = automaticEnrolledDevice.Enroll()
+ require.NoError(t, err)
+
+ // add a device that's automatically enrolled with a server ref
+ automaticHostWithRef := createOrbitEnrolledHost(t, "darwin", "h3", s.ds)
+ automaticEnrolledDeviceWithRef := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
+ automaticEnrolledDeviceWithRef.UUID = automaticHostWithRef.UUID
+ automaticEnrolledDeviceWithRef.SerialNumber = automaticHostWithRef.HardwareSerial
+ err = automaticEnrolledDeviceWithRef.Enroll()
+ require.NoError(
+ t,
+ s.ds.SetOrUpdateMDMData(
+ ctx,
+ automaticHostWithRef.ID,
+ false,
+ true,
+ s.server.URL,
+ true,
+ fleet.WellKnownMDMFleet,
+ "foo",
+ ),
+ )
+ require.NoError(t, err)
+
+ // add global profiles
+ globalProfiles := [][]byte{
+ mobileconfigForTest("N1", "I1"),
+ mobileconfigForTest("N2", "I2"),
+ }
+ s.Do(
+ "POST",
+ "/api/v1/fleet/mdm/apple/profiles/batch",
+ batchSetMDMAppleProfilesRequest{Profiles: globalProfiles},
+ http.StatusNoContent,
+ )
+ // ack all commands to install profiles
+ cmd, err := manualEnrolledDevice.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ cmd, err = manualEnrolledDevice.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+ cmd, err = automaticEnrolledDevice.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ cmd, err = automaticEnrolledDevice.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+ cmd, err = automaticEnrolledDeviceWithRef.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ cmd, err = automaticEnrolledDeviceWithRef.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+
+ cert, key, err := generateCertWithAPNsTopic()
+ require.NoError(t, err)
+ fleetCfg := config.TestConfig()
+ config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, testBMToken, "")
+ logger := kitlog.NewJSONLogger(os.Stdout)
+
+ // run without expired certs, no command enqueued
+ err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
+ require.NoError(t, err)
+ cmd, err = manualEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDeviceWithRef.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ // expire all the certs we just created
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, `
+ UPDATE nano_cert_auth_associations
+ SET cert_not_valid_after = DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
+ WHERE id IN (?, ?, ?)
+ `, manualHost.UUID, automaticHost.UUID, automaticHostWithRef.UUID)
+ return err
+ })
+
+ // generate a new config here so we can manipulate the certs.
+ err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
+ require.NoError(t, err)
+
+ checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string) {
+ var renewCmd *mdm.Command
+ cmd, err := device.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ if cmd.Command.RequestType == "InstallProfile" {
+ renewCmd = cmd
+ }
+ cmd, err = device.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+ require.NotNil(t, renewCmd)
+ var fullCmd micromdm.CommandPayload
+ require.NoError(t, plist.Unmarshal(renewCmd.Raw, &fullCmd))
+ s.verifyEnrollmentProfile(fullCmd.Command.InstallProfile.Payload, enrollRef)
+ }
+
+ checkRenewCertCommand(manualEnrolledDevice, "")
+ checkRenewCertCommand(automaticEnrolledDevice, "")
+ checkRenewCertCommand(automaticEnrolledDeviceWithRef, "foo")
+
+ // another cron run shouldn't enqueue more commands
+ err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
+ require.NoError(t, err)
+
+ cmd, err = manualEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDeviceWithRef.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ // devices renew their SCEP cert by re-enrolling.
+ require.NoError(t, manualEnrolledDevice.Enroll())
+ require.NoError(t, automaticEnrolledDevice.Enroll())
+ require.NoError(t, automaticEnrolledDeviceWithRef.Enroll())
+
+ // no new commands are enqueued right after enrollment
+ cmd, err = manualEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDevice.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+
+ cmd, err = automaticEnrolledDeviceWithRef.Idle()
+ require.NoError(t, err)
+ require.Nil(t, cmd)
+}
diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go
index 0b10dbeb4c..303d147abf 100644
--- a/server/service/integration_mdm_profiles_test.go
+++ b/server/service/integration_mdm_profiles_test.go
@@ -1983,14 +1983,20 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
require.Nil(t, h1.TeamID)
h2 := createManualMDMEnrollWithOrbit(globalEnrollSec)
require.Nil(t, h2.TeamID)
+ // run the cron
+ s.awaitTriggerProfileSchedule(t)
s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
h1: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
h2: {
{Identifier: "G1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "G2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
})
@@ -2001,14 +2007,20 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h4 := createManualMDMEnrollWithOrbit(tm1EnrollSec)
require.NotNil(t, h4.TeamID)
require.Equal(t, tm1.ID, *h4.TeamID)
+ // run the cron
+ s.awaitTriggerProfileSchedule(t)
s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
h3: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
h4: {
{Identifier: "T1.1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: "T1.2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
},
})
@@ -4001,9 +4013,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() {
if len(secrets) == 0 {
require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}}))
}
- signingCert, _, _, err := s.fleetCfg.MDM.AppleSCEP()
- require.NoError(t, err)
- require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger, signingCert))
+ require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger, s.fleetCfg.MDM))
// turn on disk encryption and os updates
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
@@ -4081,7 +4091,7 @@ func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() {
require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)
- require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger, signingCert))
+ require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger, s.fleetCfg.MDM))
checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...)
checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...)
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 90cb5fad04..7dc1ee4fce 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -212,9 +212,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
if s.onProfileJobDone != nil {
s.onProfileJobDone()
}
- signingCert, _, _, err := fleetCfg.MDM.AppleSCEP()
- require.NoError(s.T(), err)
- err = ReconcileAppleProfiles(ctx, ds, mdmCommander, logger, signingCert)
+ err = ReconcileAppleProfiles(ctx, ds, mdmCommander, logger, fleetCfg.MDM)
require.NoError(s.T(), err)
return err
}),
@@ -315,6 +313,8 @@ func (s *integrationMDMTestSuite) TearDownTest() {
appCfg.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
// ensure global Windows OS updates are always disabled for the next test
appCfg.MDM.WindowsUpdates = fleet.WindowsUpdates{}
+ // ensure the server URL is constant
+ appCfg.ServerSettings.ServerURL = s.server.URL
err := s.ds.SaveAppConfig(ctx, &appCfg.AppConfig)
require.NoError(t, err)
@@ -6071,22 +6071,79 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail)
require.NoError(t, d.Enroll())
- cmds, err := d.StartManagementSession()
+ checkinAndAck := func(expectFleetdCmds bool) {
+ cmds, err := d.StartManagementSession()
+ require.NoError(t, err)
+
+ if !expectFleetdCmds {
+ // receives only the 2 status commands
+ require.Len(t, cmds, 2)
+ for _, c := range cmds {
+ require.Equal(t, "Status", c.Verb, c)
+ }
+ return
+ }
+
+ // 2 status + 2 commands to install fleetd
+ require.Len(t, cmds, 4)
+ var fleetdAddCmd, fleetdExecCmd fleet.ProtoCmdOperation
+ for _, c := range cmds {
+ switch c.Verb {
+ case "Add":
+ fleetdAddCmd = c
+ case "Exec":
+ fleetdExecCmd = c
+ }
+ }
+ require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
+ require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
+
+ // reply with success for both commands
+ msgID, err := d.GetCurrentMsgID()
+ require.NoError(t, err)
+
+ d.AppendResponse(fleet.SyncMLCmd{
+ XMLName: xml.Name{Local: fleet.CmdStatus},
+ MsgRef: &msgID,
+ CmdRef: &fleetdAddCmd.Cmd.CmdID.Value,
+ Cmd: &fleetdAddCmd.Verb,
+ Data: ptr.String("200"),
+ Items: nil,
+ CmdID: fleet.CmdID{Value: uuid.NewString()},
+ })
+ d.AppendResponse(fleet.SyncMLCmd{
+ XMLName: xml.Name{Local: fleet.CmdStatus},
+ MsgRef: &msgID,
+ CmdRef: &fleetdExecCmd.Cmd.CmdID.Value,
+ Cmd: &fleetdExecCmd.Verb,
+ Data: ptr.String("200"),
+ Items: nil,
+ CmdID: fleet.CmdID{Value: uuid.NewString()},
+ })
+ cmds, err = d.SendResponse()
+ require.NoError(t, err)
+
+ // the ack of the message should be the only returned command
+ require.Len(t, cmds, 1)
+ }
+
+ // start a management session, will receive the install fleetd commands
+ checkinAndAck(true)
+
+ // start a new management session again, Fleetd is not reported as installed
+ // so it receives the commands again
+ checkinAndAck(true)
+
+ // simulate fleetd installed and enrolled
+ host := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
+ err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, d.DeviceID)
+ require.NoError(t, err)
+ err = s.ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, "1.23", sql.NullString{}, sql.NullBool{})
require.NoError(t, err)
- // 2 status + 2 commands to install fleetd
- require.Len(t, cmds, 4)
- var fleetdAddCmd, fleetdExecCmd fleet.ProtoCmdOperation
- for _, c := range cmds {
- switch c.Verb {
- case "Add":
- fleetdAddCmd = c
- case "Exec":
- fleetdExecCmd = c
- }
- }
- require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
- require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
+ // start a new management session again, Fleetd is reported as installed so
+ // it does not receive the commands
+ checkinAndAck(false)
}
func (s *integrationMDMTestSuite) TestValidManagementUnenrollRequest() {
@@ -7525,7 +7582,6 @@ func (s *integrationMDMTestSuite) checkMDMProfilesSummaries(t *testing.T, teamID
if expectedAppleSummary != nil {
var apple getMDMAppleProfilesSummaryResponse
s.DoJSON("GET", "/api/v1/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryRequest{}, http.StatusOK, &apple, queryParams...)
- fmt.Println(expectedSummary, apple)
require.Equal(t, expectedSummary.Failed, apple.Failed, "failed summary count doesn't match")
require.Equal(t, expectedSummary.Pending, apple.Pending, "pending summary count doesn't match")
require.Equal(t, expectedSummary.Verifying, apple.Verifying, "verifying summary count doesn't match")
@@ -7637,7 +7693,7 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() {
checkInstallFleetdCommandSent(mdmDevice, true)
// create a device that's enrolled into Fleet before turning on MDM features,
- // it shouldn't get the command to install fleetd
+ // it should still get the command to install fleetd if turns on MDM.
desktopToken := uuid.New().String()
host := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, desktopToken)
@@ -7647,7 +7703,7 @@ func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() {
err = mdmDevice.Enroll()
require.NoError(t, err)
s.runWorker()
- checkInstallFleetdCommandSent(mdmDevice, false)
+ checkInstallFleetdCommandSent(mdmDevice, true)
}
func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() {
@@ -8320,161 +8376,6 @@ func (s *integrationMDMTestSuite) TestDontIgnoreAnyProfileErrors() {
}
}
-func (s *integrationMDMTestSuite) TestSCEPCertExpiration() {
- t := s.T()
- ctx := context.Background()
- // ensure there's a token for automatic enrollments
- s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
- }))
- s.runDEPSchedule()
-
- // add a device that's manually enrolled
- desktopToken := uuid.New().String()
- manualHost := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
- err := s.ds.SetOrUpdateDeviceAuthToken(context.Background(), manualHost.ID, desktopToken)
- require.NoError(t, err)
- manualEnrolledDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
- manualEnrolledDevice.UUID = manualHost.UUID
- err = manualEnrolledDevice.Enroll()
- require.NoError(t, err)
-
- // add a device that's automatically enrolled
- automaticHost := createOrbitEnrolledHost(t, "darwin", "h2", s.ds)
- depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
- automaticEnrolledDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
- automaticEnrolledDevice.UUID = automaticHost.UUID
- automaticEnrolledDevice.SerialNumber = automaticHost.HardwareSerial
- err = automaticEnrolledDevice.Enroll()
- require.NoError(t, err)
-
- // add a device that's automatically enrolled with a server ref
- automaticHostWithRef := createOrbitEnrolledHost(t, "darwin", "h3", s.ds)
- automaticEnrolledDeviceWithRef := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
- automaticEnrolledDeviceWithRef.UUID = automaticHostWithRef.UUID
- automaticEnrolledDeviceWithRef.SerialNumber = automaticHostWithRef.HardwareSerial
- err = automaticEnrolledDeviceWithRef.Enroll()
- require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, automaticHostWithRef.ID, false, true, s.server.URL, true, fleet.WellKnownMDMFleet, "foo"))
- require.NoError(t, err)
-
- // add global profiles
- globalProfiles := [][]byte{
- mobileconfigForTest("N1", "I1"),
- mobileconfigForTest("N2", "I2"),
- }
- s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
- // ack all commands to install profiles
- cmd, err := manualEnrolledDevice.Idle()
- require.NoError(t, err)
- for cmd != nil {
- cmd, err = manualEnrolledDevice.Acknowledge(cmd.CommandUUID)
- require.NoError(t, err)
- }
- cmd, err = automaticEnrolledDevice.Idle()
- require.NoError(t, err)
- for cmd != nil {
- cmd, err = automaticEnrolledDevice.Acknowledge(cmd.CommandUUID)
- require.NoError(t, err)
- }
- cmd, err = automaticEnrolledDeviceWithRef.Idle()
- require.NoError(t, err)
- for cmd != nil {
- cmd, err = automaticEnrolledDeviceWithRef.Acknowledge(cmd.CommandUUID)
- require.NoError(t, err)
- }
-
- cert, key, err := generateCertWithAPNsTopic()
- require.NoError(t, err)
- fleetCfg := config.TestConfig()
- config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, testBMToken, "")
- logger := kitlog.NewJSONLogger(os.Stdout)
-
- // run without expired certs, no command enqueued
- err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
- require.NoError(t, err)
- cmd, err = manualEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDeviceWithRef.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- // expire all the certs we just created
- mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
- _, err := q.ExecContext(ctx, `
- UPDATE nano_cert_auth_associations
- SET cert_not_valid_after = DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
- WHERE id IN (?, ?, ?)
- `, manualHost.UUID, automaticHost.UUID, automaticHostWithRef.UUID)
- return err
- })
-
- // generate a new config here so we can manipulate the certs.
- err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
- require.NoError(t, err)
-
- checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string) {
- var renewCmd *mdm.Command
- cmd, err := device.Idle()
- require.NoError(t, err)
- for cmd != nil {
- if cmd.Command.RequestType == "InstallProfile" {
- renewCmd = cmd
- }
- cmd, err = device.Acknowledge(cmd.CommandUUID)
- require.NoError(t, err)
- }
- require.NotNil(t, renewCmd)
- var fullCmd micromdm.CommandPayload
- require.NoError(t, plist.Unmarshal(renewCmd.Raw, &fullCmd))
- s.verifyEnrollmentProfile(fullCmd.Command.InstallProfile.Payload, enrollRef)
- }
-
- checkRenewCertCommand(manualEnrolledDevice, "")
- checkRenewCertCommand(automaticEnrolledDevice, "")
- checkRenewCertCommand(automaticEnrolledDeviceWithRef, "foo")
-
- // another cron run shouldn't enqueue more commands
- err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
- require.NoError(t, err)
-
- cmd, err = manualEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDeviceWithRef.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- // devices renew their SCEP cert by re-enrolling.
- require.NoError(t, manualEnrolledDevice.Enroll())
- require.NoError(t, automaticEnrolledDevice.Enroll())
- require.NoError(t, automaticEnrolledDeviceWithRef.Enroll())
-
- // no new commands are enqueued right after enrollment
- cmd, err = manualEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDevice.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-
- cmd, err = automaticEnrolledDeviceWithRef.Idle()
- require.NoError(t, err)
- require.Nil(t, cmd)
-}
-
func (s *integrationMDMTestSuite) TestMDMDiskEncryptionIssue16636() {
// see https://github.com/fleetdm/fleet/issues/16636
diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go
index 1a6e113f50..e8a44ae0ae 100644
--- a/server/service/microsoft_mdm.go
+++ b/server/service/microsoft_mdm.go
@@ -23,6 +23,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
+ mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
kitlog "github.com/go-kit/kit/log"
@@ -1274,7 +1275,23 @@ func (svc *Service) isFleetdPresentOnDevice(ctx context.Context, deviceID string
// If user identity is a MS-MDM UPN it means that the device was enrolled through user-driven flow
// This means that fleetd might not be installed
if isValidUPN(enrolledDevice.MDMEnrollUserID) {
- return false, nil
+ var isPresent bool
+ if enrolledDevice.HostUUID != "" {
+ host, err := svc.ds.HostLiteByIdentifier(ctx, enrolledDevice.HostUUID)
+ if err != nil && !fleet.IsNotFound(err) {
+ return false, ctxerr.Wrap(ctx, err, "get host lite by identifier")
+ }
+ if host != nil {
+ orbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
+ if err != nil && !fleet.IsNotFound(err) {
+ return false, ctxerr.Wrap(ctx, err, "get host orbit info")
+ }
+ if orbitInfo != nil {
+ isPresent = orbitInfo.Version != ""
+ }
+ }
+ }
+ return isPresent, nil
}
// TODO: Add check here to determine if MDM DeviceID is connected with Smbios UUID present on
@@ -1746,6 +1763,20 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st
return err
}
+ // TODO: azure enrollments come with an empty uuid, I haven't figured
+ // out a good way to identify the device.
+ if hostUUID != "" {
+ mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger)
+ err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
+ Action: mdmlifecycle.HostActionTurnOn,
+ Platform: "windows",
+ UUID: hostUUID,
+ })
+ if err != nil {
+ return err
+ }
+ }
+
err = svc.ds.NewActivity(ctx, nil, &fleet.ActivityTypeMDMEnrolled{
HostDisplayName: reqDeviceName,
MDMPlatform: fleet.MDMPlatformMicrosoft,
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index d6e34624f6..10387de164 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -259,7 +259,10 @@ var allDetailQueries = osquery_utils.GetDetailQueries(
context.Background(),
config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}},
nil,
- &fleet.Features{EnableHostUsers: true},
+ &fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ },
)
func expectedDetailQueriesForPlatform(platform string) map[string]osquery_utils.DetailQuery {
@@ -1027,7 +1030,11 @@ func TestHostDetailQueries(t *testing.T) {
ds := new(mock.Store)
additional := json.RawMessage(`{"foobar": "select foo", "bim": "bam"}`)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{AdditionalQueries: &additional, EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ AdditionalQueries: &additional,
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
mockClock := clock.NewMockClock()
@@ -1311,7 +1318,10 @@ func TestLabelQueries(t *testing.T) {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
@@ -1468,7 +1478,10 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
ctx = hostctx.NewContext(ctx, host)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
ds.LabelQueriesForHostFunc = func(context.Context, *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
@@ -1658,7 +1671,10 @@ func TestDetailQueries(t *testing.T) {
lq.On("QueriesForHost", host.ID).Return(map[string]string{}, nil)
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
ds.LabelQueriesForHostFunc = func(context.Context, *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
@@ -1707,9 +1723,8 @@ func TestDetailQueries(t *testing.T) {
// queries)
queries, discovery, acc, err := svc.GetDistributedQueries(ctx)
require.NoError(t, err)
- // +2 for software inventory (+1 for the main software query +1 software_vscode_extensions)
// +1 for fleet_no_policies_wildcard
- if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+2+1, len(queries)) {
+ if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+1, len(queries)) {
// this is just to print the diff between the expected and actual query
// keys when the count assertion fails, to help debugging - they are not
// expected to match.
@@ -1975,9 +1990,8 @@ func TestDetailQueries(t *testing.T) {
queries, discovery, acc, err = svc.GetDistributedQueries(ctx)
require.NoError(t, err)
- // +2 software inventory (+1 main software query and +1 software extra query )
// +1 fleet_no_policies_wildcard query
- require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2+1, len(queries), distQueriesMapKeys(queries))
+ require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+1, len(queries), distQueriesMapKeys(queries))
verifyDiscovery(t, queries, discovery)
assert.Zero(t, acc)
}
@@ -2156,7 +2170,10 @@ func TestDistributedQueryResults(t *testing.T) {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
hostCtx := hostctx.NewContext(ctx, host)
@@ -3012,7 +3029,10 @@ func TestPolicyQueries(t *testing.T) {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
lq.On("QueriesForHost", uint(0)).Return(map[string]string{}, nil)
@@ -3209,7 +3229,8 @@ func TestPolicyWebhooks(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
Features: fleet.Features{
- EnableHostUsers: true,
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
},
WebhookSettings: fleet.WebhookSettings{
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
@@ -3477,7 +3498,10 @@ func TestLiveQueriesFailing(t *testing.T) {
return host, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, nil
+ return &fleet.AppConfig{Features: fleet.Features{
+ EnableHostUsers: true,
+ EnableSoftwareInventory: true,
+ }}, nil
}
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
@@ -3765,3 +3789,17 @@ func TestPreProcessSoftwareResults(t *testing.T) {
})
}
}
+
+func TestDetailQueriesLinuxDistros(t *testing.T) {
+ for _, linuxPlatform := range fleet.HostLinuxOSs {
+ m := expectedDetailQueriesForPlatform(linuxPlatform)
+ require.Contains(t, m, "users")
+ require.Contains(t, m, "network_interface_unix")
+ require.Contains(t, m, "disk_space_unix")
+ require.Contains(t, m, "os_unix_like")
+ require.Contains(t, m, "orbit_info")
+ require.Contains(t, m, "disk_encryption_linux")
+ require.Contains(t, m, "software_vscode_extensions")
+ require.Contains(t, m, "software_linux")
+ }
+}
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index f9a6af2bcd..ecbbf264ca 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -1055,7 +1055,7 @@ var usersQuery = DetailQuery{
// with many user accounts and groups, this query could be very expensive as the `groups` table
// was generated once for each user.
Query: usersQueryStr,
- Platforms: []string{"linux", "darwin", "windows"},
+ Platforms: append(fleet.HostLinuxOSs, "darwin", "windows"),
DirectIngestFunc: directIngestUsers,
}
diff --git a/server/service/team_policies.go b/server/service/team_policies.go
index 7786c7fe68..75cbe3ae96 100644
--- a/server/service/team_policies.go
+++ b/server/service/team_policies.go
@@ -368,7 +368,8 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
})
}
- var shouldRemoveAll bool
+ var removeAllMemberships bool
+ var removeStats bool
if p.Name != nil {
policy.Name = *p.Name
}
@@ -377,9 +378,8 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
}
if p.Query != nil {
if policy.Query != *p.Query {
- shouldRemoveAll = true
- policy.FailingHostCount = 0
- policy.PassingHostCount = 0
+ removeAllMemberships = true
+ removeStats = true
}
policy.Query = *p.Query
}
@@ -387,6 +387,9 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
policy.Resolution = p.Resolution
}
if p.Platform != nil {
+ if policy.Platform != *p.Platform {
+ removeStats = true
+ }
policy.Platform = *p.Platform
}
if p.Critical != nil {
@@ -395,9 +398,13 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
if p.CalendarEventsEnabled != nil {
policy.CalendarEventsEnabled = *p.CalendarEventsEnabled
}
+ if removeStats {
+ policy.FailingHostCount = 0
+ policy.PassingHostCount = 0
+ }
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
- err = svc.ds.SavePolicy(ctx, policy, shouldRemoveAll)
+ err = svc.ds.SavePolicy(ctx, policy, removeAllMemberships, removeStats)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "saving policy")
}
diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go
index e6079f1101..9e1a502f67 100644
--- a/server/service/team_policies_test.go
+++ b/server/service/team_policies_test.go
@@ -44,7 +44,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
}
return nil, nil
}
- ds.SavePolicyFunc = func(ctx context.Context, p *fleet.Policy, shouldDeleteAll bool) error {
+ ds.SavePolicyFunc = func(ctx context.Context, p *fleet.Policy, shouldDeleteAll bool, removePolicyStats bool) error {
return nil
}
ds.DeleteTeamPoliciesFunc = func(ctx context.Context, teamID uint, ids []uint) ([]uint, error) {
diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go
index 5b7af86800..82a675ec30 100644
--- a/server/service/testing_utils.go
+++ b/server/service/testing_utils.go
@@ -351,6 +351,7 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ
if len(opts) > 0 {
mdmStorage := opts[0].MDMStorage
scepStorage := opts[0].SCEPStorage
+ commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher, cfg.MDM)
if mdmStorage != nil && scepStorage != nil {
err := RegisterAppleMDMProtocolServices(
rootMux,
@@ -358,11 +359,7 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ
mdmStorage,
scepStorage,
logger,
- &MDMAppleCheckinAndCommandService{
- ds: ds,
- commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher, cfg.MDM),
- logger: kitlog.NewNopLogger(),
- },
+ NewMDMAppleCheckinAndCommandService(ds, commander, logger),
&MDMAppleDDMService{
ds: ds,
logger: logger,
diff --git a/server/vulnerabilities/nvd/cpe_matching_rule.go b/server/vulnerabilities/nvd/cpe_matching_rule.go
index 222af5e125..32b331d4f3 100644
--- a/server/vulnerabilities/nvd/cpe_matching_rule.go
+++ b/server/vulnerabilities/nvd/cpe_matching_rule.go
@@ -6,7 +6,7 @@ import (
"strings"
"github.com/Masterminds/semver"
- "github.com/facebookincubator/nvdtools/wfn"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
)
// CPEMatchingRuleSpec allows you to match against a CPE. Version ranges are supported via SemVer constraints.
diff --git a/server/vulnerabilities/nvd/cpe_matching_rule_test.go b/server/vulnerabilities/nvd/cpe_matching_rule_test.go
index c5f3abc1ca..46f4899ce1 100644
--- a/server/vulnerabilities/nvd/cpe_matching_rule_test.go
+++ b/server/vulnerabilities/nvd/cpe_matching_rule_test.go
@@ -4,7 +4,7 @@ import (
"errors"
"testing"
- "github.com/facebookincubator/nvdtools/wfn"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
"github.com/stretchr/testify/require"
)
diff --git a/server/vulnerabilities/nvd/cpe_matching_rules.go b/server/vulnerabilities/nvd/cpe_matching_rules.go
index 76ce97b442..f745f97ec0 100644
--- a/server/vulnerabilities/nvd/cpe_matching_rules.go
+++ b/server/vulnerabilities/nvd/cpe_matching_rules.go
@@ -3,7 +3,7 @@ package nvd
import (
"fmt"
- "github.com/facebookincubator/nvdtools/wfn"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
)
type CPEMatchingRules []CPEMatchingRule
diff --git a/server/vulnerabilities/nvd/cpe_test.go b/server/vulnerabilities/nvd/cpe_test.go
index 6f8a5f4c94..bb0b318b18 100644
--- a/server/vulnerabilities/nvd/cpe_test.go
+++ b/server/vulnerabilities/nvd/cpe_test.go
@@ -11,10 +11,10 @@ import (
"testing"
"time"
- "github.com/facebookincubator/nvdtools/cpedict"
"github.com/fleetdm/fleet/v4/pkg/nettest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cpedict"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/log"
"github.com/stretchr/testify/assert"
diff --git a/server/vulnerabilities/nvd/cve.go b/server/vulnerabilities/nvd/cve.go
index ff5661cc14..906c3b7485 100644
--- a/server/vulnerabilities/nvd/cve.go
+++ b/server/vulnerabilities/nvd/cve.go
@@ -15,16 +15,16 @@ import (
"time"
"github.com/Masterminds/semver"
- "github.com/facebookincubator/nvdtools/cvefeed"
- feednvd "github.com/facebookincubator/nvdtools/cvefeed/nvd"
- "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema"
- "github.com/facebookincubator/nvdtools/providers/nvd"
- "github.com/facebookincubator/nvdtools/wfn"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
nvdsync "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/sync"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed"
+ feednvd "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd/schema"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/providers/nvd"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
"github.com/go-kit/log"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
diff --git a/server/vulnerabilities/nvd/cve_test.go b/server/vulnerabilities/nvd/cve_test.go
index 8f29cc3e8b..0123d1835d 100644
--- a/server/vulnerabilities/nvd/cve_test.go
+++ b/server/vulnerabilities/nvd/cve_test.go
@@ -11,11 +11,11 @@ import (
"testing"
"time"
- "github.com/facebookincubator/nvdtools/cvefeed"
- "github.com/facebookincubator/nvdtools/wfn"
"github.com/fleetdm/fleet/v4/pkg/nettest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
"github.com/go-kit/log"
kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
diff --git a/server/vulnerabilities/nvd/db.go b/server/vulnerabilities/nvd/db.go
index 690132171f..b0e0c33b9e 100644
--- a/server/vulnerabilities/nvd/db.go
+++ b/server/vulnerabilities/nvd/db.go
@@ -6,8 +6,8 @@ import (
"os"
"strings"
- "github.com/facebookincubator/nvdtools/cpedict"
- "github.com/facebookincubator/nvdtools/wfn"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cpedict"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
diff --git a/server/vulnerabilities/nvd/indexed_cpe_item.go b/server/vulnerabilities/nvd/indexed_cpe_item.go
index 3bc3415027..f2e38086d2 100644
--- a/server/vulnerabilities/nvd/indexed_cpe_item.go
+++ b/server/vulnerabilities/nvd/indexed_cpe_item.go
@@ -1,8 +1,8 @@
package nvd
import (
- "github.com/facebookincubator/nvdtools/wfn"
"github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn"
)
type IndexedCPEItem struct {
diff --git a/server/vulnerabilities/nvd/sync.go b/server/vulnerabilities/nvd/sync.go
index 6426303897..a4405b7c9d 100644
--- a/server/vulnerabilities/nvd/sync.go
+++ b/server/vulnerabilities/nvd/sync.go
@@ -15,12 +15,12 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
- "github.com/facebookincubator/nvdtools/cvefeed"
- feednvd "github.com/facebookincubator/nvdtools/cvefeed/nvd"
"github.com/fleetdm/fleet/v4/pkg/download"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed"
+ feednvd "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)
diff --git a/server/vulnerabilities/nvd/sync/cve_syncer.go b/server/vulnerabilities/nvd/sync/cve_syncer.go
index 065d8faec8..178429cf65 100644
--- a/server/vulnerabilities/nvd/sync/cve_syncer.go
+++ b/server/vulnerabilities/nvd/sync/cve_syncer.go
@@ -21,11 +21,11 @@ import (
"strings"
"time"
- "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/ptr"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd/schema"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/pandatix/nvdapi/common"
@@ -36,7 +36,7 @@ import (
// to the directory specified in the dbDir field in the form of JSON files.
// It stores the CVE information using the legacy feed format.
// The reason we decided to store in the legacy format is because
-// the github.com/facebookincubator/nvdtools doesn't yet support parsing
+// the github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools doesn't yet support parsing
// the new API 2.0 JSON format.
type CVE struct {
client *http.Client
@@ -183,7 +183,7 @@ func (s *CVE) update(ctx context.Context) error {
func (s *CVE) updateYearFile(year int, cves []nvdapi.CVEItem) error {
// The NVD legacy feed files start at year 2002.
- // This is assumed by the facebookincubator/nvdtools package.
+ // This is assumed by the github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools package.
if year < 2002 {
year = 2002
}
diff --git a/server/vulnerabilities/nvd/sync/cve_syncer_test.go b/server/vulnerabilities/nvd/sync/cve_syncer_test.go
index d58e7449c4..b25e0fbefc 100644
--- a/server/vulnerabilities/nvd/sync/cve_syncer_test.go
+++ b/server/vulnerabilities/nvd/sync/cve_syncer_test.go
@@ -18,7 +18,7 @@ import (
"testing"
"time"
- "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema"
+ "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd/schema"
"github.com/go-kit/log"
"github.com/google/go-cmp/cmp"
"github.com/pandatix/nvdapi/v2"
diff --git a/server/vulnerabilities/nvd/tools/HOWTO.md b/server/vulnerabilities/nvd/tools/HOWTO.md
new file mode 100644
index 0000000000..937f12d565
--- /dev/null
+++ b/server/vulnerabilities/nvd/tools/HOWTO.md
@@ -0,0 +1,335 @@
+# How to use nvdtools
+
+The command line tools provided by nvdtools were designed for processing inventory data in pipelines.
+
+To start, you will need a vulnerability database. In this toolkit you'll find the [nvdsync](https://github.com/facebookincubator/nvdtools/tree/master/cmd/nvdsync) command, which can download the public NVD database to local disk:
+
+```bash
+nvdsync -v=1 -cve_feed=cve-1.0.json.gz /tmp/nvd
+```
+
+Next up, you need a data collector to create a CPE inventory. Collectors are domain-specific programs capable of acquiring asset information (e.g. a list of hardware, or packages in a repo or system) and printing this information to standard output.
+
+Think of the simplest data collector as an execution of rpm (or repoquery):
+
+```bash
+rpm -qa | rpm2cpe -rpm=1 -cpe=2
+```
+
+This collector [rpm2cpe](https://github.com/facebookincubator/nvdtools/tree/master/cmd/rpm2cpe) will use the name of the rpm files in column 1 of the input, produce a CPE in column 2, and print both to standard output.
+
+Finally, use the [cpe2cve](https://github.com/facebookincubator/nvdtools/tree/master/cmd/cpe2cve) processor to consume the CPE inventory from standard input and print CVEs affecting which CPEs to standard output:
+
+```bash
+rpm -qa | \
+rpm2cpe -rpm=1 -cpe=2 | \
+cpe2cve -cpe=2 -cve=3 -cwe=4 /tmp/nvd/*.json.gz
+```
+
+The command above process each CPE individually and prints their respective CVEs. However, it's not uncommon in the NVD database to have more elaborate CVEs which affect a combination of CPEs, e.g. if A and B and not C. For this case, you could group your CPEs per host, for example, and process them in a single batch:
+
+```bash
+set -o pipefail
+(hostname
+rpm -qa | rpm2cpe -rpm=1 -cpe=2 -e=1 | sort -u | paste -s -d, | \
+cpe2cve -cpe=1 -cve=2 -e=1 /tmp/nvd/*.json.gz | paste -s -d,) | paste -s -d'\t'
+```
+
+The command above prints a single line containing `
[Isabell Reedy](https://www.linkedin.com/in/isabell-reedy-202aa3123/) _([@ireedy](https://github.com/ireedy))_
## Contact us
- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-business-operations&projects=&template=custom-request.md&title=Request%3A+_______________________) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in [#g-business-operations](https://fleetdm.slack.com/archives/C047N5L6EGH).
diff --git a/handbook/business-operations/security-audits.md b/handbook/business-operations/security-audits.md
index b9628c4b2c..3be3f10609 100644
--- a/handbook/business-operations/security-audits.md
+++ b/handbook/business-operations/security-audits.md
@@ -2,7 +2,30 @@
This page contains explanations of the latest external security audits performed on Fleet software.
## June 2023 penetration testing of Fleet 4.33
-Available on request.
+In June 2023, [Latacora](https://www.latacora.com/) performed an application penetration assessment of the application from Fleet.
+
+An application penetration test captures a point-in-time assessment of vulnerabilities, misconfigurations, and gaps in applications that could allow an attacker to compromise the security, availability, processing integrity, confidentiality, and privacy (SAPCP) of sensitive data and application resources. An application penetration test simulates the capabilities of a real adversary, but accelerates testing by using information provided by the target company.
+
+You can find the full report here: [2023-06-09-fleet-penetration-test.pdf](https://github.com/fleetdm/fleet/raw/main/docs/files/2023-06-09-fleet-penetration-test.pdf).
+
+### Findings
+#### 1 - Stored cross-site scripting (XSS) in tooltip
+| Type | Latacora Severity |
+| ------------------- | -------------- |
+| Cross-site scripting| High risk |
+
+All tooltips using the "tipContent" tag are set using "dangerouslySetInnerHTML". This allows manipulation of the DOM without sanitization. If a user can control the content sent to this function, it can lead to a cross-site scripting vulnerability.
+
+- Resolved. Resolution information TBA
+
+#### 2 - Broken authorization leads to observers able to add hosts
+| Type | Latacora Severity |
+| ------------------- | -------------- |
+| Authorization issue | High risk |
+
+Observers are not supposed to be able to add hosts to Fleet. Via specific endpoints, it becomes possible to retrieve the certificate chains and the secrets for all teams, and these are the information required to add a host.
+
+- Resolved. Resolution information TBA
## April 2022 penetration testing of Fleet 4.12
In April 2022, we worked with [Lares](https://www.lares.com/) to perform penetration testing on our Fleet instance, which was running 4.12 at the time.
diff --git a/handbook/company/README.md b/handbook/company/README.md
index 05bbe2f238..ee0874d669 100644
--- a/handbook/company/README.md
+++ b/handbook/company/README.md
@@ -135,7 +135,7 @@ Fleet added support for [scripting and management capabilities](https://fleetdm.
> Still curious? Check out this [visualization of the Fleet repo over the years](https://www.linkedin.com/feed/update/urn:li:activity:7045068060168220672/) or listen to this [conversation between Zach and Mike Arpaia about the origin story of osquery](https://fleetdm.com/podcasts/the-future-of-device-management-ep1).
## Org chart
-To provide clarity about decision-making, [responsibility](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility), and resources, everyone at Fleet has a manager, and [every manager](https://fleetdm.com/handbook/company#management) has direct reports. Fleet's organizational chart is accessible company-wide as a sub-tab in ["🧑🚀 Fleeties" (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0). On the other sub-tabs, you can also check out a world map of where everyone is located, hiring stats, and fun facts about each team member.
+To provide clarity about decision-making, [responsibility](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility), and resources, everyone at Fleet has a manager, and [every manager](https://fleetdm.com/handbook/company/leadership) has direct reports. Fleet's organizational chart is accessible company-wide as a sub-tab in ["🧑🚀 Fleeties" (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0). On the other sub-tabs, you can also check out a world map of where everyone is located, hiring stats, and fun facts about each team member.
- 🔦 [Business Operations](https://fleetdm.com/handbook/business-operations): The Business Operations department is directly responsible for these traditional functions: People, Finance, tax, compliance, Legal, and IT.
- 🌦️ [Customer Success](https://fleetdm.com/handbook/customer-success): The customer success department is directly responsible for ensuring that customers and community members of Fleet achieve their desired outcomes with Fleet products and services.
diff --git a/handbook/company/communications.md b/handbook/company/communications.md
index 5df2104ffa..bb46f45a2a 100644
--- a/handbook/company/communications.md
+++ b/handbook/company/communications.md
@@ -13,19 +13,41 @@ You can read about the company's positioning and product strategy in ["🎐 Why
We track competitors' capabilities and adjacent (or commonly integrated) products in Google doc [Competition](https://docs.google.com/document/d/1Bqdui6oQthdv5XtD5l7EZVB-duNRcqVRg7NVA4lCXeI/edit) (private Google doc).
## Directly responsible individuals (DRIs)
-| Responsibility | DRI |
-| -------------- | --- |
-| Intentionality of Fleet's interfaces | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/noahtalerman))_ |
-| Best practices for using Fleet | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/noahtalerman))_ |
-| What goes in a release | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_ |
-| Engineering output and architecture | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_ |
-| Structure and intentionality of the [Docs](https://fleetdm.com/docs/get-started/why-fleet)| [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_ |
-| Design and content of the [Docs](https://fleetdm.com/docs/get-started/why-fleet) | [Rachael Shaw](https://www.linkedin.com/in/rachaelcshaw/) _([@rachaelshaw](https://github.com/rachaelshaw))_ |
-| API design | [Rachael Shaw](https://www.linkedin.com/in/rachaelcshaw/) _([@rachaelshaw](https://github.com/rachaelshaw))_ |
+| Area of responsibility | [DRI](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) |
+| -------------- | --- |
+| Revenue | _See [🐋 Chief Revenue Officer](https://fleetdm.com/handbook/sales#team)_
+| Pricing | _See [🛠️ CEO responsibilities](https://fleetdm.com/handbook/company/leadership#ceo-responsibilities)_
+| Illustrations | _See [🌐 Head of Design](https://fleetdm.com/handbook/digital-experience#team)_
+| Website | _See [🌐 Digital Experience team](https://fleetdm.com/handbook/digital-experience#team)_
+| Product marketing (PMM) | _See [🛠️ CEO responsibilities](https://fleetdm.com/handbook/company/leadership#ceo-responsibilities)_
+| Brand marketing | _See [🛠️ CEO responsibilities](https://fleetdm.com/handbook/company/leadership#ceo-responsibilities)_
+| Public relations | _See [🛠️ CEO responsibilities](https://fleetdm.com/handbook/company/leadership#ceo-responsibilities)_
+| Revenue pipeline | _See [🫧 Head of Demand Generation](https://fleetdm.com/handbook/demand#team)_
+| Ads | _See [🫧 Demand team](https://fleetdm.com/handbook/demand#team)_
+| Video | _See [🫧 Digital Marketing Manager](https://fleetdm.com/handbook/demand#team)_
+| Social media | _See [🫧 Digital Marketing Manager](https://fleetdm.com/handbook/demand#team)_
+| Blog | _See [🚀 Client Platform Engineer & Community Advocate](https://fleetdm.com/handbook/engineering#team)_
+| Information technology (IT) | _See [🚀 Client Platform Engineer & Community Advocate](https://fleetdm.com/handbook/engineering#team)_
+| Payroll, bookkeeping, AR/AP | _See [🔦 Head of Business Operations](https://fleetdm.com/handbook/customer-success#team)_
+| Legal contracts | _See [🔦 Business Operations team](https://fleetdm.com/handbook/customer-success#team)_
+| Customer renewals | _See [🌦️ VP of Customer Success](https://fleetdm.com/handbook/customer-success#team)_
+| Customer deployments | _See [🌦️ Infrastructure Engineer](https://fleetdm.com/handbook/customer-success#team)_
+| Customer support | _See [🌦️ Customer Success team](https://fleetdm.com/handbook/customer-success#team)_
+| Quality assurance (QA) | _See [🚀 Engineering team](https://fleetdm.com/handbook/engineering#team)_
+| Features & product adoption | _See [🦢 Head of Product Design](https://fleetdm.com/handbook/product-design#team)_
+| Feature prioritization | _See [🦢 Head of Product Design](https://fleetdm.com/handbook/product-design#team)_
+| Intentionality of Fleet's interfaces | _See [🦢 Head of Product Design](https://fleetdm.com/handbook/product-design#team)_
+| Best practices for using Fleet | _See [🦢 Product Design team](https://fleetdm.com/handbook/product-design#team)_
+| [API design](https://fleetdm.com/docs/rest-api/rest-api) | _See [🦢 Rachael Shaw](https://fleetdm.com/handbook/product-design#team)_
+| Structure of the [docs](https://fleetdm.com/docs/get-started/why-fleet) | _See [🌐 Head of Design](https://fleetdm.com/handbook/digital-experience#team)_
+| Product reference documentation | _See [🦢 Rachael Shaw](https://fleetdm.com/handbook/product-design#team)_
+| What goes in a release | _See [🚀 Chief Technology Officer](https://fleetdm.com/handbook/engineering#team)_
+| Engineering output and architecture | _See [🚀 Chief Technology Officer](https://fleetdm.com/handbook/engineering#team)_
+| Product development | _See [🛩️ Product groups](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
### Docs
-This page details processes related to maintaining and updating the ([Fleet docs](https://fleetdm.com/docs)).
+This page details processes related to maintaining and updating the [Fleet documentation](https://fleetdm.com/docs).
When someone asks a question in a public channel, it's safe to assume they aren't the only person looking for an answer.
@@ -33,6 +55,7 @@ To make our docs as helpful as possible, the Community team gathers these questi
Fleet's goal is to answer every question with a link to the docs and/or result in a documentation update.
+> Fleet's philosophy on how to write useful documentation is public and open-source: ["Why read documentation?"](https://fleetdm.com/handbook/company/why-this-way#why-read-documentation)
## Fleetdm.com
Any change to fleetdm.com follows the same process as [making changes](https://fleetdm.com/handbook/company/product-groups#making-changes) to the core product. To propose a change to Fleet's website [create a website request](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=website-request.md&title=Request%3A+__________________________) on the #g-digital-experience kanban board.
diff --git a/handbook/company/open-positions.yml b/handbook/company/open-positions.yml
index cd91548db0..7d313763f6 100644
--- a/handbook/company/open-positions.yml
+++ b/handbook/company/open-positions.yml
@@ -61,32 +61,5 @@
- 🛠️ Technical: You understand the software development processes.
- 🟣 Openness: You are flexible and open to new ideas and ways of working
- ➕ Bonus: Cybersecurity or IT background
-- jobTitle: 🐋 Customer Success Engineer
- department: Customers
- hiringManagerName: Jason Lewis
- hiringManagerGithubUsername: Patagonia121
- hiringManagerLinkedInUrl: https://www.linkedin.com/in/jlewis0451/
- responsibilities: |
- - 🎯 Strong attention to detail and can act as an encyclopedia of knowledge about how Fleet works - our customers represent a wide range of needs across many different use cases. Be adaptable to learning new things quickly and then share this knowledge with others.
- - 📣 Manage multiple customer deployments and escalations simultaneously with the ability to stay organized.
- - 🚀 Deploy Fleet on your own to have a better understanding of the customer experience and how the product works.
- - 🪴 Promote product adoption, referencability, and customer advocacy with key customer stakeholders.
- - 🥇 Be the first line of defense in customer Slack channels for any reported problems, how-to questions, feature request intake, and bug report filling.
- - 🚀 Work collaboratively with product and engineering teams to facilitate bug resolution and feature development based on customer asks.
- - ⏫ Work hand-in-hand with the customer success team by participating in ad-hoc calls with customers to discuss any support issues they may have.
- - 💡 Excellent communication and collaboration skills, with the ability to work cross-functionally with CS, engineering, and product teams.
- experience: |
- - 💭 Cybersecurity or IT background, experience with cloud environments like AWS and Azure or device management solutions like Fleet, Intune, Jamf Pro, Workspace One, etc.
- - 💖 You know how to manage your time and priorities between customer support engagements, customer escalations, and other day-to-day responsibilities.
- - 🧬 An excellent understanding of macOS, Windows, Linux and core services like Autopilot, ABM/ASM, MDM, ADE, APNs, syslog, etc.
- - 🤝 You work best in a team-based environment. You are decisive with the ability to shift gears between thinking and doing.
- - 👥 A customer-centric mindset, focusing on delivering value and a positive user experience.
- - 🦉 2-3 years of work experience providing technical support to enterprise customers in the cybersecurity or device management space. Experience with executing and tracking results tied to customer escalations.
- - 🛠️ You are personable, enjoy being customer facing, and have a passion for problem solving while assisting external and internal stakeholders.
- - 🧪 Extensive experience with Slack, Google Suite, and GitHub.
- - ✍️ Familiarity with shell scripting, Python, Powershell, and using Terminal to execute commands or run scripts, and other line of business applications.
- - 🟣 Openness: Speak freely. Interrupt and be interrupted. Give pointed and respectful feedback, even when you disagree.
- - 🔴 Empathy: You should demonstrate empathy by keenly understanding and addressing customer concerns with genuine compassion.
- - ➕ Bonus: Familiarity with osquery, MySQL, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams.
diff --git a/handbook/company/pricing-features-table.yml b/handbook/company/pricing-features-table.yml
index d22f054d7a..1621f3a65b 100644
--- a/handbook/company/pricing-features-table.yml
+++ b/handbook/company/pricing-features-table.yml
@@ -632,7 +632,7 @@
description: Easily configure and install SentinelOne, Crowdstrike, and other security tools.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14921
tier: Premium
- comingSoonOn: 2024-04-22 #customer-reedtimmer,customer-flacourtia
+ comingSoonOn: 2024-05-13 #customer-flacourtia
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md
index d85d80eeb7..fc66b775b3 100644
--- a/handbook/company/product-groups.md
+++ b/handbook/company/product-groups.md
@@ -52,7 +52,7 @@ The goal of the MDM group is to increase and exceed [Fleet's product maturity go
| Product Designer | [Marko Lisica](https://www.linkedin.com/in/markolisica/) _([@marko-lisica](https://github.com/marko-lisica))_
| Engineering Manager | [George Karr](https://www.linkedin.com/in/george-karr-4977b441/) _([@georgekarrv](https://github.com/georgekarrv))_
| Product Manager | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/@noahtalerman))_
-| Quality Assurance | [Position open](https://www.fleetdm.com/jobs/)
+| Quality Assurance | [Gabe Lopez](https://www.linkedin.com/in/gabelopez/) _([@PezHub](https://github.com/PezHub))_
| Developer | [Gabe Hernandez](https://www.linkedin.com/in/gabriel-hernandez-gh) _([@ghernandez345](https://github.com/ghernandez345))_, [Roberto Dip](https://www.linkedin.com/in/roperzh) _([@roperzh](https://github.com/roperzh))_, Sarah Gillespie _([@gillespi314](https://github.com/gillespi314))_, [Martin Angers](https://www.linkedin.com/in/martin-angers-3210305/) _([@mna](https://github.com/mna))_, [Jahziel Villasana-Espinoza](https://www.linkedin.com/in/jahziel-v/) _([@jahzielv](https://github.com/jahzielv))_, [Dante Catalfamo](https://www.linkedin.com/in/dante-catalfamo-a6330412b/) _([@dantecatalfamo](https://github.com/dantecatalfamo))_
> The [Slack channel](https://fleetdm.slack.com/archives/C03C41L5YEL), [kanban release board](https://app.zenhub.com/workspaces/-g-mdm-current-sprint-63bc507f6558550011840298/board), and [GitHub label](https://github.com/fleetdm/fleet/issues?q=is%3Aopen+is%3Aissue+label%3A%23g-mdm) for this product group is `#g-mdm`.
@@ -431,20 +431,20 @@ Bugs will be verified as fixed by QA when they are placed in the "Awaiting QA" c
## High priority user stories and bugs
All issues are treated as standard priority by default. Some issues are assigned a priority label to indicate urgency for the business.
-1. Emergency: `P0`
-- Examples: Customer outage, confirmed security vulnerability ([critical bug](https://fleetdm.com/handbook/company/product-groups#release-testing)), a new feature is needed to address an immediate business emergency.
-- Response: Immediately stop other work to swarm the issue. Work 24/7 in shifts until resolved.
-- Impact: Significant impact. May void current sprint.
+- Emergency: `P0`
+ - Examples: Customer outage, confirmed security vulnerability ([critical bug](https://fleetdm.com/handbook/company/product-groups#release-testing)), a new feature is needed to address an immediate business emergency.
+ - Response: Immediately stop other work to swarm the issue. Work 24/7 in shifts until resolved.
+ - Impact: Significant impact. May void current sprint.
-2. Critical: `P1`
-- Examples: A supported workflow is broken ([critical bug](https://fleetdm.com/handbook/company/product-groups#release-testing)), a potential security vulnerability, a new feature is required to address an immediate critical business need.
-- Response: Issue brought to next standup for estimation and immediately brought into the sprint. Necessary team members are assigned as their top priority.
-- Impact: High impact. Does not void sprint, but reduces overall velocity and requires deprioritizing other work.
+- Critical: `P1`
+ - Examples: A supported workflow is broken ([critical bug](https://fleetdm.com/handbook/company/product-groups#release-testing)), a potential security vulnerability, a new feature is required to address an immediate critical business need.
+ - Response: Issue brought to next standup for estimation and immediately brought into the sprint. Necessary team members are assigned as their top priority.
+ - Impact: High impact. Does not void sprint, but reduces overall velocity and requires deprioritizing other work.
-3. Urgent: `P2`
-- Examples: A supported workflow is not functioning as intended, a newly drafted feature has an associated urgent business need.
-- Response: Issue is prioritized at the top of the next sprint. If opporunity cost of waiting for the next sprint is too high, it may be considered for current sprint.
-- Impact: Low to medium impact. If prioritized into current sprint, may reduce overall velocity and require deprioritizing other work.
+- Urgent: `P2`
+ - Examples: A supported workflow is not functioning as intended, a newly drafted feature has an associated urgent business need.
+ - Response: Issue is prioritized at the top of the next sprint. If opporunity cost of waiting for the next sprint is too high, it may be considered for current sprint.
+ - Impact: Low to medium impact. If prioritized into current sprint, may reduce overall velocity and require deprioritizing other work.
Add as much context as possible to the issue description and assign labels to help the team understand the problem and what is driving the urgency. All issues with a `P0`, `P1`, or `P2` label should be assigned to the [DRI for what goes in a release](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris). For immediate action, follow up on Slack or by phone.
diff --git a/handbook/company/testimonials.yml b/handbook/company/testimonials.yml
index 227a42f16d..ed03143f9f 100644
--- a/handbook/company/testimonials.yml
+++ b/handbook/company/testimonials.yml
@@ -71,7 +71,7 @@
quoteAuthorJobTitle: Staff CPE at Stripe
productCategories: [Endpoint operations, Device management]
-
- quote: Fleet’s come a long way - to now being the top open-source osquery manager. Just in the past 6 months.
+ quote: Fleet’s come a long way - to now being the top open-source osquery manager.
quoteImageFilename: social-proof-logo-atlassian-192x32@2x.png
quoteLinkUrl: https://www.linkedin.com/in/bshak/
quoteAuthorName: Brendan Shaklovitz
diff --git a/handbook/customer-success/README.md b/handbook/customer-success/README.md
index 2f96814de7..9ffcf30914 100644
--- a/handbook/customer-success/README.md
+++ b/handbook/customer-success/README.md
@@ -5,10 +5,9 @@ This handbook page details processes specific to working [with](#contact-us) and
| Role | Contributor(s) |
|:--------------------------------------|:------------------------------------------------------------------------------------------------------------------------|
| VP of Customer Success | [Zay Hanlon](https://www.linkedin.com/in/zayhanlon/) _([@zayhanlon](https://github.com/zayhanlon))_
-| Customer Success Managers (CSM) | [Jason Lewis](https://www.linkedin.com/in/jlewis0451/) _([@patagonia121](https://github.com/patagonia121))_, [Michael Pinto](https://www.linkedin.com/in/michael-pinto-a06b4515a/) _([@pintomi1989](https://github.com/pintomi1989))_
-| Customer Solutions Architect (CSA) | [Brock Walters](https://www.linkedin.com/in/brock-walters-247a2990/) _([@nonpunctual](https://github.com/nonpunctual))_
-| Customer Support Engineer (CSE) | [Kathy Satterlee](https://www.linkedin.com/in/ksatter/) _([@ksatter](https://github.com/ksatter))_, [Grant Bilstad](https://www.linkedin.com/in/grantbilstad/) _([@Pacamaster](https://github.com/Pacamaster))_, Ben Edwards _([@edwardsb](https://github.com/edwardsb))_
| Infrastructure Engineer | [Robert Fairburn](https://www.linkedin.com/in/robert-fairburn/) _([@rfairburn](https://github.com/rfairburn))_
+| Customer Support (CSE/CSA) | [Kathy Satterlee](https://www.linkedin.com/in/ksatter/) _([@ksatter](https://github.com/ksatter))_
[Grant Bilstad](https://www.linkedin.com/in/grantbilstad/) _([@Pacamaster](https://github.com/Pacamaster))_
Ben Edwards _([@edwardsb](https://github.com/edwardsb))_
[Brock Walters](https://www.linkedin.com/in/brock-walters-247a2990/) _([@nonpunctual](https://github.com/nonpunctual))_
+| Customer Success Manager (CSM) | [Jason Lewis](https://www.linkedin.com/in/jlewis0451/) _([@patagonia121](https://github.com/patagonia121))_
[Michael Pinto](https://www.linkedin.com/in/michael-pinto-a06b4515a/) _([@pintomi1989](https://github.com/pintomi1989))_
## Contact us
- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-customer-success&projects=&template=custom-request.md&title=Request%3A+_______________________) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in the [#g-customer-success](https://fleetdm.slack.com/archives/C062D0THVV1)).
diff --git a/handbook/demand/README.md b/handbook/demand/README.md
index 2360016984..85717e41fe 100644
--- a/handbook/demand/README.md
+++ b/handbook/demand/README.md
@@ -2,11 +2,10 @@
This handbook page details processes specific to working [with](#contact-us) and [within](#responsibilities) this department.
## Team
-| Role | Contributor(s)
-|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------|
-| 🫧 Head of Demand Generation | [Dustin Gerdes](https://www.linkedin.com/in/dustingerdes/) _([@3kindsoffish](https://github.com/3kindsoffish))_
-| 🫧 Field Marketer | [Drew Baker](https://www.linkedin.com/in/andrew-baker-51547179/) _([@drewbakerfdm](https://github.com/drewbakerfdm))_
-| _🎐 Head of Brand & Product Marketing (CEO)_ | [Mike McNeil](https://www.linkedin.com/in/mikermcneil) _([@mikermcneil](https://github.com/mikermcneil))_
+| Role | Contributor(s)
+|:-----------------------------------|:------------------------------------------------------------------------------------------------------------------------|
+| Head of Demand Generation | [Dustin Gerdes](https://www.linkedin.com/in/dustingerdes/) _([@3kindsoffish](https://github.com/3kindsoffish))_
+| Digital Marketing Manager | [Drew Baker](https://www.linkedin.com/in/andrew-baker-51547179/) _([@drewbakerfdm](https://github.com/drewbakerfdm))_
## Contact us
@@ -14,10 +13,10 @@ This handbook page details processes specific to working [with](#contact-us) and
- Please use **issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/g-demand-64e6c8e2d35c7f001a457b7f/board?sprints=none) for this department, including pending tasks and the status of new requests.
-> To **make a request** related to **product marketing**, **press**, **brandfronts**, **pitchfronts**, **featurefronts**, **ideal customer profiles (ICPs)**, **personas**, or **targeting** [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=custom-request.md&title=Product%20marketing%20request%3A+_______________________) (If urgent, at-mention the [Head of Product Marketing](#team) in the [help-pmm-2023](https://fleetdm.slack.com/archives/C0600L1TTPY) Slack channel).
+> To **make a request** related to **product marketing**, **press**, **brandfronts**, **pitchfronts**, **featurefronts**, **ideal customer profiles (ICPs)**, **personas**, or **targeting** [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=custom-request.md&title=Product%20marketing%20request%3A+_______________________) (If urgent, at-mention the [Head of Product Marketing](#team) in the [#help-leadership](https://fleetdm.slack.com/archives/C0600L1TTPY) Slack channel).
## Responsibilities
-The Demand department is directly responsible for growing awareness of Fleet and nurturing the community through participation in events, conversations, and other [programs](https://fleetdm.com/handbook/company/communications#programs).
+The Demand department is directly responsible for achieving revenue pipeline targets, increasing awareness and interest in the open-source project, and nurturing the Fleet community through participation in video, sponsored events, and other [programs](https://fleetdm.com/handbook/company/communications#programs).
### Respond to a "Contact us" submission
1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions.
diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md
index 5f973ae726..f76e62ad2c 100644
--- a/handbook/digital-experience/README.md
+++ b/handbook/digital-experience/README.md
@@ -4,9 +4,10 @@ This page details processes specific to working [with](#contact-us) and [within]
## Team
| Role | Contributor(s)
|:--------------------------------|:----------------------------------------------------------------------|
-| Head of Digital Experience | [Sam Pfluger](https://www.linkedin.com/in/sampfluger88/) _([@sampfluger88](https://github.com/sampfluger88))_
-| Head of Design | [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_
-| Software Engineer | [Eric Shaw](https://www.linkedin.com/in/eric-shaw-1423831a9/) _([@eashaw](https://github.com/eashaw))_
+| Head of Digital Experience | See [🌐 Apprentice to the CEO](https://fleetdm.com/handbook/digital-experience#team)
+| Head of Design | [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_
+| Software Engineer | [Eric Shaw](https://www.linkedin.com/in/eric-shaw-1423831a9/) _([@eashaw](https://github.com/eashaw))_
+| Apprentice to the CEO | [Sam Pfluger](https://www.linkedin.com/in/sampfluger88/) _([@sampfluger88](https://github.com/sampfluger88))_
[Savannah Friend](https://www.linkedin.com/in/savannah-friend-2b1a53148/) _(@todo)_
## Contact us
@@ -15,11 +16,11 @@ This page details processes specific to working [with](#contact-us) and [within]
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/g-sales-64fbb46c65f9ff003a1530a8/board?sprints=none) for this department, including pending tasks and the status of new requests.
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
-> _**Note:** If a user story involves only changes to fleetdm.com, without changing the core product, then that user story is prioritized, drafted, implemented, and shipped by the [Digital Experience](https://fleetdm.com/handbook/digital-experience) department. Otherwise, if the story **also** involves changes to the core product **as well as** fleetdm.com, then that user story is prioritized, drafted, implemented, and shipped by [the other relevant product group](https://fleetdm.com/handbook/company/product-groups#current-product-groups), and not by `#g-digital-experience`._
## Responsibilities
The Digital Experience department is directly responsible for the framework, content design, and technology behind Fleet's remote work culture, including fleetdm.com, the handbook, issue templates, UI style guides, internal tooling, Zapier flows, Docusign templates, key spreadsheets, and project management processes.
+> _**Note:** If a user story involves only changes to fleetdm.com, without changing the core product, then that user story is prioritized, drafted, implemented, and shipped by the [Digital Experience](https://fleetdm.com/handbook/digital-experience) department. Otherwise, if the story **also** involves changes to the core product **as well as** fleetdm.com, then that user story is prioritized, drafted, implemented, and shipped by [the other relevant product group](https://fleetdm.com/handbook/company/product-groups#current-product-groups), and not by `#g-digital-experience`._
### QA a change to fleetdm.com
Each PR to the website is manually checked for quality and tested before before going live on fleetdm.com. To test any change to fleetdm.com
diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md
index 2b72932d2b..f8262fae36 100644
--- a/handbook/engineering/README.md
+++ b/handbook/engineering/README.md
@@ -1,13 +1,15 @@
# Engineering
-This handbook page details processes specific to working [with](#team) and [within](#responsibilities) this department
+This handbook page details processes specific to working [with](#contact-us) and [within](#responsibilities) this department.
## Team
| Role | Contributor(s) |
|:--------------------------------|:-----------------------------------------------------------------------------------------------------------|
-| CTO | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_
-| Engineering Manager | _See ["Current product groups"](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
-| Quality Assurance | [Reed Haynes](https://www.linkedin.com/in/reed-haynes-633a69a3/) _([@xpkoala](https://github.com/xpkoala))_
-| Developer | _See ["Current product groups"](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
+| Chief Technology Officer (CTO) | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_
+| Client Platform Engineer & Community Advocate | [JD Strong](https://www.linkedin.com/in/jackdaniyelstrong/) _([@spokanemac](https://github.com/spokanemac/spokanemac))_
+| Engineering Manager (EM) | _See [🛩️ Product groups](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
+| Quality Assurance Engineer (QA) | _See [🛩️ Product groups](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
+| Software Engineer | _See [🛩️ Product groups](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
+
## Contact us
- To **make a request** of this department, [create an issue](https://fleetdm.com/handbook/company/product-groups#current-product-groups) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in the [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) Slack channel.
@@ -48,7 +50,7 @@ If the bug is labeled `~unreleased bug`, branch off and put your PR into `main`.
If the bug is labeled `~released bug`, branch off the tag for the latest release of Fleet and put your PR into `main`. For example, `git checkout fleet-v4.48.2`, then `git checkout -b my-bug-fix-branch`. These issues are not closed until the next release of Fleet. This approach makes sure the bug fix is not built on top of unreleased feature code, which can cause merge conflicts during patch releases.
### Begin a merge freeze
-To ensure release quality, Fleet has a freeze period for testing beginning the Tuesday before the release at 9:00 AM Pacific. Effective at the start of the freeze period, new feature work will not be merged into `main`.
+To ensure release quality, Fleet has a freeze period for testing beginning the Tuesday before the release at 11:00 AM Pacific. Effective at the start of the freeze period, new feature work will not be merged into `main`.
Bugs are exempt from the release freeze period.
diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md
index a560a0764e..a224150441 100644
--- a/handbook/product-design/README.md
+++ b/handbook/product-design/README.md
@@ -1,14 +1,11 @@
-# Product design
+# Product Design
This handbook page details processes specific to working [with](#contact-us) and [within](#responsibilities) this department.
## Team
-| Role | Contributor(s) |
-|:--------------------------------|:-----------------------------------------------------------------------------------------------------------|
+| Role | Contributor(s) |
+|:---------------------------------|:-----------------------------------------------------------------------------------------------------------|
| Head of Product Design | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/noahtalerman))_
-| Head of Design | [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_
-| Product Designer | [Rachael Shaw](https://www.linkedin.com/in/rachaelcshaw/) _([@rachaelshaw](https://github.com/rachaelshaw))_, [Marko Lisica](https://www.linkedin.com/in/markolisica/) _([@marko-lisica](https://github.com/marko-lisica))_
-| Developer | [Eric Shaw](https://www.linkedin.com/in/eric-shaw-1423831a9/) _([@eashaw](https://github.com/eashaw))_
-
+| Product Designer | _See [🛩️ Product groups](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
## Contact us
- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in `#help-design`.
diff --git a/handbook/sales/README.md b/handbook/sales/README.md
index b1fe017ad4..6201676cc5 100644
--- a/handbook/sales/README.md
+++ b/handbook/sales/README.md
@@ -4,12 +4,11 @@ This handbook page details processes specific to working [with](#contact-us) and
## Team
| Role | Contributor(s) |
|:--------------------------------------|:------------------------------------------------------------------------------------------------------------------------|
-| Chief Revenue Officer (CRO) | [Alex Mitchell](https://www.linkedin.com/in/alexandercmitchell/) _([@alexmitchelliii](https://github.com/alexmitchelliii))_
-| 🏹 [Customer Success](https://www.fleetdm.com/handbook/customer-success#responsibilities) | [Customer Success team members](https://www.fleetdm.com/handbook/customer-success#team)
-| Director of Solutions Consulting | [Dave Herder](https://www.linkedin.com/in/daveherder/) _([@dherder](https://github.com/dherder))_
-| Solutions Consultant (SC) | [Will Mayhone](https://www.linkedin.com/in/william-mayhone-671977b6/) _([@willmayhone88](https://github.com/willmayhone88))_
-| Head of Public Sector | [Keith Barnes](https://www.linkedin.com/in/keith-barnes-8b666/) _([@KAB703](https://github.com/KAB703))_
-| Account Executive (AE) | [Tom Ostertag](https://www.linkedin.com/in/tom-ostertag-77212791/) _([@TomOstertag](https://github.com/TomOstertag))_, [Patricia Ambrus](https://www.linkedin.com/in/pambrus/) _([@ambrusps](https://github.com/ambrusps))_, [Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@AnthonySnyder8](https://github.com/AnthonySnyder8))_, [Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_
+| Chief Revenue Officer (CRO) | [Alex Mitchell](https://www.linkedin.com/in/alexandercmitchell/) _([@alexmitchelliii](https://github.com/alexmitchelliii))_
+| Solutions Consulting (SC) | [Dave Herder](https://www.linkedin.com/in/daveherder/) _([@dherder](https://github.com/dherder))_
[Zach Wasserman](https://www.linkedin.com/in/zacharywasserman/) _([@zwass](https://github.com/zwass))_
[Will Mayhone](https://www.linkedin.com/in/william-mayhone-671977b6/) _([@willmayhone88](https://github.com/willmayhone88))_
+| Public Sector | [Keith Barnes](https://www.linkedin.com/in/keith-barnes-8b666/) _([@KAB703](https://github.com/KAB703))_
+| Channel Sales | [Tom Ostertag](https://www.linkedin.com/in/tom-ostertag-77212791/) _([@tomostertag](https://github.com/TomOstertag))_
+| Account Executive (AE) | [Patricia Ambrus](https://www.linkedin.com/in/pambrus/) _([@ambrusps](https://github.com/ambrusps))_
[Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_
[Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_
## Contact us
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/free.tf b/infrastructure/dogfood/terraform/aws-tf-module/free.tf
index 4efc3999b9..ec6aeaa454 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/free.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/free.tf
@@ -25,6 +25,7 @@ module "free" {
}
rds_config = {
name = local.customer_free
+ engine_version = "8.0.mysql_aurora.3.05.2"
snapshot_identifier = "arn:aws:rds:us-east-2:611884880216:cluster-snapshot:a2023-03-06-pre-migration"
db_parameters = {
# 8mb up from 262144 (256k) default
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/main.tf b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
index 2b0056fc79..a3c3614abb 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/main.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
@@ -70,6 +70,7 @@ module "main" {
}
rds_config = {
name = local.customer
+ engine_version = "8.0.mysql_aurora.3.05.2"
snapshot_identifier = "arn:aws:rds:us-east-2:611884880216:cluster-snapshot:a2023-03-06-pre-migration"
db_parameters = {
# 8mb up from 262144 (256k) default
diff --git a/infrastructure/dogfood/terraform/aws/rds.tf b/infrastructure/dogfood/terraform/aws/rds.tf
index 10e0fb512a..bd37ea9cf9 100644
--- a/infrastructure/dogfood/terraform/aws/rds.tf
+++ b/infrastructure/dogfood/terraform/aws/rds.tf
@@ -67,7 +67,7 @@ module "aurora_mysql" {
name = "${local.name}-mysql-iam"
engine = "aurora-mysql"
- engine_version = "8.0.mysql_aurora.3.02.0"
+ engine_version = "8.0.mysql_aurora.3.05.2"
instance_type = var.db_instance_type_writer
instance_type_replica = var.db_instance_type_reader
diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf
index 592383ad2d..83f3879dac 100644
--- a/infrastructure/dogfood/terraform/aws/variables.tf
+++ b/infrastructure/dogfood/terraform/aws/variables.tf
@@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
- default = "fleetdm/fleet:v4.48.3"
+ default = "fleetdm/fleet:v4.49.1"
}
variable "software_inventory" {
diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf
index a1b08bab80..7de4a7f02f 100644
--- a/infrastructure/dogfood/terraform/gcp/variables.tf
+++ b/infrastructure/dogfood/terraform/gcp/variables.tf
@@ -68,5 +68,5 @@ variable "redis_mem" {
}
variable "image" {
- default = "fleet:v4.48.3"
+ default = "fleet:v4.49.1"
}
diff --git a/it-and-security/lib/collect-windows-defender.queries.yml b/it-and-security/lib/collect-windows-defender.queries.yml
new file mode 100644
index 0000000000..739dfcd999
--- /dev/null
+++ b/it-and-security/lib/collect-windows-defender.queries.yml
@@ -0,0 +1,10 @@
+- name: Collect Windows Defender
+ automations_enabled: false
+ description: Collects the pid, process name, user, path and command line for Windows Defender installed on hosts.
+ discard_data: false
+ interval: 3600
+ logging: snapshot
+ min_osquery_version: ""
+ observer_can_run: true
+ platform: "windows"
+ query: SELECT processes.pid, processes.name, users.username, processes.path, processes.cmdline FROM processes LEFT JOIN users ON processes.uid = users.uid WHERE processes.path != '' AND name LIKE 'MpCmdRun.exe';
\ No newline at end of file
diff --git a/it-and-security/lib/macos-device-health.policies.yml b/it-and-security/lib/macos-device-health.policies.yml
index 427ac7a149..f06480bd50 100644
--- a/it-and-security/lib/macos-device-health.policies.yml
+++ b/it-and-security/lib/macos-device-health.policies.yml
@@ -64,12 +64,3 @@
description: Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders.
resolution: Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location.
platform: darwin
-- name: macOS - Check if latest version
- query: |
- SELECT 1 FROM os_version
- WHERE (major > 14 OR (major = 14 AND minor > 4) OR (major = 14 AND minor = 4 AND patch >= 1)) --Sonoma
- critical: false
- description: This policy check if macOS version is most recent version available.
- resolution: From the Apple menu, select System Settings. Navigate to General > Software Update.
- platform: darwin
- calendar_events_enabled: true
diff --git a/it-and-security/lib/windows-device-health.policies.yml b/it-and-security/lib/windows-device-health.policies.yml
index 5a15b90cc5..40e276caae 100644
--- a/it-and-security/lib/windows-device-health.policies.yml
+++ b/it-and-security/lib/windows-device-health.policies.yml
@@ -22,4 +22,10 @@
description: This policy checks if the end user is required to enter a password, with at least 10 characters, to unlock the host.
resolution: "As an IT admin, deploy a Windows profile with the DevicePasswordEnabled and MinDevicePasswordLength option documented here: https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-devicelock"
platform: windows
+- name: Windows - Antivirus healthy
+ query: SELECT 1 from windows_security_center wsc CROSS JOIN windows_security_products wsp WHERE antivirus = 'Good' AND type = 'Antivirus' AND signatures_up_to_date=1;
+ critical: false
+ description: Checks the status of antivirus and signature updates from the Windows Security Center.
+ resolution: "Ensure Windows Defender or your third-party antivirus is running, up to date, and visible in the Windows Security Center."
+ platform: windows
diff --git a/it-and-security/mdm-commands/apple/send-fleetd.xml b/it-and-security/mdm-commands/apple/send-fleetd.xml
new file mode 100644
index 0000000000..0a10ff6d99
--- /dev/null
+++ b/it-and-security/mdm-commands/apple/send-fleetd.xml
@@ -0,0 +1,15 @@
+
+object({ | `{}` | no |
| [certificate\_arn](#input\_certificate\_arn) | n/a | `string` | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
name = optional(string, "fleet")
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})object({ |
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
}){ | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}object({ |
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.3")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}){ | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}object({ |
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}){ | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. |
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}object({ |
mem = number
cpu = number
}){ | no |
| [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module |
"cpu": 1024,
"mem": 2048
}object({ |
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.04.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
}){ | no |
| [redis\_config](#input\_redis\_config) | n/a |
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.04.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"snapshot_identifier": null,
"subnets": []
}object({ |
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string)
allowed_security_group_ids = optional(list(string), [])
subnets = optional(list(string))
availability_zones = optional(list(string))
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
}){ | no |
diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md
index 651d206147..89aa013d1f 100644
--- a/terraform/byo-vpc/README.md
+++ b/terraform/byo-vpc/README.md
@@ -34,7 +34,7 @@ No requirements.
| [alb\_config](#input\_alb\_config) | n/a |
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": null,
"cluster_size": 3,
"elasticache_subnet_group_name": null,
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
}object({ | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module |
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
})object({ |
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
}){ | no |
<<<<<<< HEAD
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
}object({ |
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.3")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}){ | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}object({ |
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}){ | no |
=======
| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. |
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}object({ |
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
repository_credentials = optional(string, "")
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}){ | no |
>>>>>>> 025004bcf (support private registry in the ecs task definition)
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 907f153cb2..3e9bd4507a 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -13,7 +13,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.3")
+ image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index ca169eebfa..194a11aef7 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -74,7 +74,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.3")
+ image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index 897ec9ef89..aae37cc107 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.48.3"
+ fleet_image = "fleetdm/fleet:v4.49.1"
domain_name = "example.com"
}
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index 66ed4ef168..e9463dda11 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -167,7 +167,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.3")
+ image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 9b1b314ff5..4a245a7969 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -59,8 +59,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.48.3"
- image = "fleetdm/fleet:v4.48.3" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.49.1"
+ image = "fleetdm/fleet:v4.49.1" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
diff --git a/terraform/variables.tf b/terraform/variables.tf
index d72f733ea1..d12ca9e1c7 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -215,7 +215,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.3")
+ image = optional(string, "fleetdm/fleet:v4.49.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/tools/calendar/delete-events/delete-events.go b/tools/calendar/delete-events/delete-events.go
index 23933e11bf..dea8197dfa 100644
--- a/tools/calendar/delete-events/delete-events.go
+++ b/tools/calendar/delete-events/delete-events.go
@@ -27,7 +27,7 @@ var (
)
const (
- eventTitle = "💻🚫Downtime"
+ eventTitle = "💻🚫 Scheduled maintenance"
)
func main() {
diff --git a/tools/calendar/move-events/move-events.go b/tools/calendar/move-events/move-events.go
index d4906eaef1..7e413abaf7 100644
--- a/tools/calendar/move-events/move-events.go
+++ b/tools/calendar/move-events/move-events.go
@@ -28,7 +28,7 @@ var (
)
const (
- eventTitle = "💻🚫Downtime"
+ eventTitle = "💻🚫 Scheduled maintenance"
)
func main() {
diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt
index 7fc10249d5..f3b6c8df0f 100644
--- a/tools/cloner-check/generated_files/appconfig.txt
+++ b/tools/cloner-check/generated_files/appconfig.txt
@@ -28,6 +28,9 @@ github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPEnableStartTLS bool
github.com/fleetdm/fleet/v4/server/fleet/AppConfig HostExpirySettings fleet.HostExpirySettings
github.com/fleetdm/fleet/v4/server/fleet/HostExpirySettings HostExpiryEnabled bool
github.com/fleetdm/fleet/v4/server/fleet/HostExpirySettings HostExpiryWindow int
+github.com/fleetdm/fleet/v4/server/fleet/AppConfig ActivityExpirySettings fleet.ActivityExpirySettings
+github.com/fleetdm/fleet/v4/server/fleet/ActivityExpirySettings ActivityExpiryEnabled bool
+github.com/fleetdm/fleet/v4/server/fleet/ActivityExpirySettings ActivityExpiryWindow int
github.com/fleetdm/fleet/v4/server/fleet/AppConfig Features fleet.Features
github.com/fleetdm/fleet/v4/server/fleet/Features EnableHostUsers bool
github.com/fleetdm/fleet/v4/server/fleet/Features EnableSoftwareInventory bool
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index cc84162fe0..e14b40f2c6 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.48.3",
+ "version": "v4.49.1",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/mdm/apple/macos-vm-auto-enroll/README.md b/tools/mdm/apple/macos-vm-auto-enroll/README.md
index 7f50931750..3045544f71 100644
--- a/tools/mdm/apple/macos-vm-auto-enroll/README.md
+++ b/tools/mdm/apple/macos-vm-auto-enroll/README.md
@@ -9,6 +9,7 @@ The script takes no arguments, but can be configured through three environment v
- `FLEET_ENROLL_SECRET` (required) The fleet enrollment secret
- `FLEET_URL` (required) The fleet base url
- `MACOS_ENROLLMENT_VM_NAME` (optional) The name of the VM. If nothing is specified, the default name is `enrollment-test`.
+- `MACOS_ENROLLMENT_VM_IMAGE` (optional) The image to use for the VM. If nothing is specified, the default image is `ghcr.io/cirruslabs/macos-sonoma-base:latest`
The entire process from the generation of the `pkg` file to the installation is automated. The only part that requires user intervention is installing the MDM profile.
diff --git a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
index 32b469dd04..2d15275e75 100755
--- a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
+++ b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
@@ -6,9 +6,13 @@ set -m
# Fleet enroll secret placed in $FLEET_ENROLL_SECRET
# Fleet URL placed in $FLEET_URL
# Optional VM name in $MACOS_ENROLLMENT_VM_NAME
+# Optional VM image in $MACOS_ENROLLMENT_VM_IMAGE
+# For others see https://tart.run/quick-start/
+# - ghcr.io/cirruslabs/macos-ventura-base:latest
+# - ghcr.io/cirruslabs/macos-monterey-base:latest
vm_name="${MACOS_ENROLLMENT_VM_NAME:-enrollment-test}"
-image_name="ghcr.io/cirruslabs/macos-sonoma-base:latest"
+image_name="${MACOS_ENROLLMENT_VM_IMAGE:-ghcr.io/cirruslabs/macos-sonoma-base:latest}"
alias ssh_cmd="sshpass -p admin ssh -o \"StrictHostKeyChecking no\" admin@\$(tart ip $vm_name)"
alias ssh_interactive_cmd="sshpass -p admin ssh -o \"StrictHostKeyChecking no\" -t admin@\$(tart ip $vm_name)"
diff --git a/tools/release/README.md b/tools/release/README.md
index 08aed8a25d..0fe38b990a 100644
--- a/tools/release/README.md
+++ b/tools/release/README.md
@@ -39,7 +39,7 @@ example
# Tag main
./tools/release/publish_release.sh -ag
# Publish main
-./tools/release/publish_release.sh -au
+./tools/release/publish_release.sh -auq
# Go update osquery-slack version
```
diff --git a/tools/release/publish_release.sh b/tools/release/publish_release.sh
index 059b1595d0..1ea01e37ac 100755
--- a/tools/release/publish_release.sh
+++ b/tools/release/publish_release.sh
@@ -76,7 +76,8 @@ usage() {
echo " -f, --force Skip all confirmations"
echo " -h, --help Display this help message and exit"
echo " -g, --tag Run the tag step"
- echo " -m, --minor Increment to a minor version instead of patch (Required if including non-bugs"
+ echo " -m, --minor Increment to a minor version instead of patch (Required if including non-bugs)"
+ echo " -n, --announce_only Announce the release only, do not publish the release."
echo " -o, --open_api_key Set the Open API key for calling out to ChatGPT"
echo " -p, --print If the release is already drafted then print out the helpful info"
echo " -q, --quiet This will skip notifying in slack"
@@ -196,7 +197,7 @@ build_changelog() {
prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.'
if [[ "$main_release" == "true" ]]; then
# Place to make a main targeted prompt
- prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.'
+ prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules: Organize updates into three categories: Endpoint Operations, Device Management (MDM), and Vulnerability Management, with all bug fixes and misc. improvements listed under "Bug fixes and improvements". Start each entry with a past tense verb, using hyphens for bullet points. Include specific details for new features, bug fixes, API changes, and any necessary user actions. Note changes in user interfaces, system feedback, and significant architectural updates. Highlight mandatory actions and major impacts, especially for system administrators. Order seemingly important features at the top of their respective lists.'
fi
content=$(cat new_changelog | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g')
@@ -325,6 +326,44 @@ print_announce_info() {
fi
}
+general_announce_info() {
+ if [[ "$main_release" == "true" ]]; then
+ article_url="https://fleetdm.com/releases/fleet-$target_milestone"
+ article_published=`curl -is "$article_url" | head -n 1 | awk '{print $2}'`
+ if [[ "$article_published" != "200" ]]; then
+ echo "Could't find article at '$article_url'"
+ exit 1
+ fi
+
+ # TODO Publish Linkedin post about release article here and save url
+ linkedin_post_url=""
+ fi
+ echo "========================================================================="
+ echo "Update osquery Slack Fleet channel topic to say the correct version $next_ver"
+ echo "========================================================================="
+ # Slack
+ slack_hook_url=https://hooks.slack.com/services
+ app_id=T019PP37ALW
+ announce_text=":cloud: :rocket: The latest version of Fleet is $target_milestone.\nMore info: https://github.com/fleetdm/fleet/releases/tag/$next_tag"
+ if [[ "$main_release" == "true" ]]; then
+ announce_text=":cloud: :rocket: The latest version of Fleet is $target_milestone.\nMore info: https://github.com/fleetdm/fleet/releases/tag/$next_tag\nRelease article: $article_url\nLinkedIn post: $linkedin_post_url"
+ fi
+
+ echo -e $announce_text
+
+ if [ "$quiet" = "false" ]; then
+ if [ "$dry_run" = "false" ]; then
+ curl -X POST -H 'Content-type: application/json' \
+ --data "{\"text\":\"$announce_text\"}" \
+ $slack_hook_url/$app_id/$SLACK_GENERAL_TOKEN
+
+ curl -X POST -H 'Content -type: application/json' \
+ --data "{\"text\":\"$announce_text\nDogfood Deployed $dogfood_deploy\"}" \
+ $slack_hook_url/$app_id/$SLACK_HELP_INFRA_TOKEN
+ fi
+ fi
+}
+
update_release_notes() {
if [ "$dry_run" = "false" ]; then
if [ ! -f temp_changelog ]; then
@@ -412,62 +451,34 @@ tag() {
publish() {
if [ "$dry_run" = "false" ]; then
- if [[ "$main_release" == "true" ]]; then
- article_url="https://fleetdm.com/releases/fleet-$target_milestone"
- article_published=`curl -is "$article_url" | head -n 1 | awk '{print $2}'`
- if [[ "$article_published" != "200" ]]; then
- echo "Coulndn't find article at '$article_url'"
- exit 1
- fi
+ if [ "$announce_only" = "false" ]; then
+ # TODO more checks to validate we are ready to publish
+ gh release edit --draft=false --latest $next_tag
+ gh workflow run dogfood-deploy.yml -f DOCKER_IMAGE=fleetdm/fleet:$next_ver
+ show_spinner 200
+ dogfood_deploy=`gh run list --workflow=dogfood-deploy.yml --status in_progress -L 1 --json url | jq -r '.[] | .url'`
+ cd tools/fleetctl-npm && npm publish
- # TODO Publish Linkedin post about release article here and save url
- linkedin_post_url=""
- fi
- # TODO more checks to validate we are ready to publish
- gh release edit --draft=false --latest $next_tag
- gh workflow run dogfood-deploy.yml -f DOCKER_IMAGE=fleetdm/fleet:$next_ver
- show_spinner 200
- echo "========================================================================="
- echo "Update osquery Slack Fleet channel topic to say the correct version $next_ver"
- echo "========================================================================="
- dogfood_deploy=`gh run list --workflow=dogfood-deploy.yml --status in_progress -L 1 --json url | jq -r '.[] | .url'`
- cd tools/fleetctl-npm && npm publish
+ issues=`gh issue list -m $target_milestone --json number | jq -r '.[] | .number'`
+ for iss in $issues; do
+ is_story=`gh issue view $iss --json labels | jq -r '.labels | .[] | .name' | grep story`
+ # close all non-stories
+ if [[ "$is_story" == "" ]]; then
+ echo "Closing #$iss"
+ gh issue close $iss
+ fi
+ done
- issues=`gh issue list -m $target_milestone --json number | jq -r '.[] | .number'`
- for iss in $issues; do
- is_story=`gh issue view $iss --json labels | jq -r '.labels | .[] | .name' | grep story`
- # close all non-stories
- if [[ "$is_story" == "" ]]; then
- echo "Closing #$iss"
- gh issue close $iss
- fi
- done
-
- echo "Closing milestone"
- gh api repos/fleetdm/fleet/milestones/$target_milestone_number -f state=closed
-
- # Slack
- slack_hook_url=https://hooks.slack.com/services
- app_id=T019PP37ALW
- announce_text=":cloud: :rocket: The latest version of Fleet is $target_milestone.\nMore info: https://github.com/fleetdm/fleet/releases/tag/$next_tag\nUpgrade now: https://fleetdm.com/docs/deploying/upgrading-fleet"
- if [[ "$main_release" == "true" ]]; then
- announce_text=":cloud: :rocket: The latest version of Fleet is $target_milestone.\nMore info: https://github.com/fleetdm/fleet/releases/tag/$next_tag\nUpgrade now: https://fleetdm.com/docs/deploying/upgrading-fleet\nRelease Article: $article_url\nLinkedIn Post: $linkedin_post_url"
- fi
-
- echo $announce_text
-
- if [ "$quiet" = "false" ]; then
- curl -X POST -H 'Content-type: application/json' \
- --data "{\"text\":\"$announce_text\"}" \
- $slack_hook_url/$app_id/$SLACK_GENERAL_TOKEN
-
- curl -X POST -H 'Content-type: application/json' \
- --data "{\"text\":\"$announce_text\nDogfood Deployed $dogfood_deploy\"}" \
- $slack_hook_url/$app_id/$SLACK_HELP_INFRA_TOKEN
+ echo "Closing milestone"
+ gh api repos/fleetdm/fleet/milestones/$target_milestone_number -f state=closed
fi
else
echo "DRYRUN: Would have published $next_tag / deployed to dogfood / closed non-stories / closed milestone / announced in slack"
fi
+
+ echo "Send general announce"
+ # Send general announcement in #general
+ general_announce_info
}
# Validate we have all commands required to perform this script
@@ -478,6 +489,7 @@ cherry_pick_resolved=false
dry_run=false
force=false
minor=false
+announce_only=false
open_api_key=""
start_version=""
target_date=""
@@ -499,6 +511,7 @@ for arg in "$@"; do
"--force") set -- "$@" "-f" ;;
"--help") set -- "$@" "-h" ;;
"--minor") set -- "$@" "-m" ;;
+ "--announce_only") set -- "$@" "-n" ;;
"--open_api_key") set -- "$@" "-o" ;;
"--print") set -- "$@" "-p" ;;
"--quiet") set -- "$@" "-q" ;;
@@ -513,7 +526,7 @@ for arg in "$@"; do
done
# Extract options and their arguments using getopts
-while getopts "acdfhgmo:pqrs:t:uv:" opt; do
+while getopts "acdfhgmno:pqrs:t:uv:" opt; do
case "$opt" in
a) main_release=true ;;
c) cherry_pick_resolved=true ;;
@@ -522,6 +535,7 @@ while getopts "acdfhgmo:pqrs:t:uv:" opt; do
h) usage; exit 0 ;;
g) do_tag=true ;;
m) minor=true ;;
+ n) announce_only=true ;;
o) open_api_key=$OPTARG ;;
p) print_info=true ;;
q) quiet=true ;;
@@ -664,7 +678,6 @@ if [[ "$target_milestone_number" == "" ]]; then
fi
echo "Found milestone $target_milestone with number $target_milestone_number"
-
if [ "$print_info" = "true" ]; then
print_announce_info
exit 0
@@ -842,6 +855,13 @@ if [[ "$failed" == "false" ]]; then
echo "DRYRUN: Would have switched back to branch $target_patch_branch"
fi
+ if [[ "$main_release" == "false" ]]; then
+ # Cherry-pick from update-changelog-branch
+ ch_commit=`git log -n 1 --pretty=format:"%H" $update_changelog_branch`
+ git cherry-pick $ch_commit
+ git push origin $target_patch_branch -f
+ fi
+
# Check for QA issue
create_qa_issue
diff --git a/website/api/controllers/deliver-contact-form-message.js b/website/api/controllers/deliver-contact-form-message.js
index a746c93627..532308baf2 100644
--- a/website/api/controllers/deliver-contact-form-message.js
+++ b/website/api/controllers/deliver-contact-form-message.js
@@ -71,6 +71,12 @@ module.exports = {
`Name: ${firstName + ' ' + lastName}, Email: ${emailAddress}, Message: ${message ? message : 'No message.'}`
});
+ await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
+ emailAddress: emailAddress,
+ firstName: firstName,
+ lastName: lastName,
+ });
+
// Send a POST request to Zapier
await sails.helpers.http.post(
diff --git a/website/api/controllers/deliver-mdm-beta-signup.js b/website/api/controllers/deliver-mdm-beta-signup.js
index 5965858d9d..78eb54780b 100644
--- a/website/api/controllers/deliver-mdm-beta-signup.js
+++ b/website/api/controllers/deliver-mdm-beta-signup.js
@@ -1,6 +1,6 @@
module.exports = {
-
+ // TODO: This isn't a thing anymore, we can delete it.
friendlyName: 'Deliver MDM beta signup',
diff --git a/website/api/controllers/deliver-talk-to-us-form-submission.js b/website/api/controllers/deliver-talk-to-us-form-submission.js
index 42abe97909..d07f2474b4 100644
--- a/website/api/controllers/deliver-talk-to-us-form-submission.js
+++ b/website/api/controllers/deliver-talk-to-us-form-submission.js
@@ -73,6 +73,15 @@ module.exports = {
if(_.includes(sails.config.custom.bannedEmailDomainsForWebsiteSubmissions, emailDomain.toLowerCase())){
throw 'invalidEmailDomain';
}
+
+ await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
+ emailAddress,
+ firstName,
+ lastName,
+ organization: organization,
+ primaryBuyingSituation: primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : 'Vulnerability management',
+ });
+
await sails.helpers.http.post.with({
url: 'https://hooks.zapier.com/hooks/catch/3627242/3cxwxdo/',
data: {
diff --git a/website/api/controllers/entrance/signup.js b/website/api/controllers/entrance/signup.js
index c5f36303f5..e20c3252e1 100644
--- a/website/api/controllers/entrance/signup.js
+++ b/website/api/controllers/entrance/signup.js
@@ -138,6 +138,14 @@ the account verification message.)`,
.intercept({name: 'UsageError'}, 'invalid')
.fetch();
+
+ await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
+ emailAddress: newEmailAddress,
+ firstName: firstName,
+ lastName: lastName,
+ organization: organization,
+ });
+
// Send a POST request to Zapier
await sails.helpers.http.post.with({
url: 'https://hooks.zapier.com/hooks/catch/3627242/30bq2ib/',
diff --git a/website/api/controllers/get-human-interpretation-from-osquery-sql.js b/website/api/controllers/get-human-interpretation-from-osquery-sql.js
new file mode 100644
index 0000000000..a7d2e18b4a
--- /dev/null
+++ b/website/api/controllers/get-human-interpretation-from-osquery-sql.js
@@ -0,0 +1,106 @@
+module.exports = {
+
+
+ friendlyName: 'Get human interpretation from osquery sql',
+
+
+ description: 'Infer policy information from osquery SQL.',
+
+
+ inputs: {
+
+ sql: {
+ type: 'string',
+ required: true
+ },
+
+ },
+
+
+ exits: {
+
+ success: {
+ outputFriendlyName: 'Humanesque interpretation',
+ outputDescription: 'If the call to the LLM fails, then a success response is sent with an explanation about the failure (e.g. "under heavy load", etc)',
+ outputExample: {
+ risks: 'Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability.',
+ whatWillProbablyHappenDuringMaintenance: 'We will update your macOS to version 14.4.1 to enhance security and stability.'
+ }
+ },
+
+ },
+
+
+ fn: async function ({sql}) {
+
+ if (!sails.config.custom.openAiSecret) {
+ throw new Error('sails.config.custom.openAiSecret not set.');
+ }//•
+
+ // Build our prompt
+ let prompt = `Given this osquery policy: aka a query which either passes (≥1 row) or fails (0 rows) for a given laptop, what risks might we anticipate from that laptop having failed the policy?
+
+Here is the query:
+\`\`\`
+${sql}
+\`\`\`
+
+Remember to minimize the number of words used!
+
+Please give me all of the above in JSON, with this data shape:
+
+{
+ risks: 'TODO',
+ whatWillProbablyHappenDuringMaintenance: 'TODO'
+}
+
+Please do not add any text outside of the JSON report or wrap it in a code fence.`;
+ // Fallback message in case LLM API request fails.
+ let failureMessage = 'Failed to generate human interpretation using generative AI.';
+
+ let BASE_MODEL = 'gpt-4';// The base model to use. https://platform.openai.com/docs/models/gpt-4
+ // (Max tokens for gpt-3.5 ≈≈ 4000) (Max tokens for gpt-4 ≈≈ 8000)
+ // [?] API: https://platform.openai.com/docs/api-reference/chat/create
+ let openAiResponse = await sails.helpers.http.post('https://api.openai.com/v1/chat/completions', {
+ model: BASE_MODEL,
+ messages: [// https://platform.openai.com/docs/guides/chat/introduction
+ {
+ role: 'user',
+ content: prompt
+ }
+ ],
+ temperature: 0.7,
+ max_tokens: 256//eslint-disable-line camelcase
+ }, {
+ Authorization: `Bearer ${sails.config.custom.openAiSecret}`
+ })
+ .tolerate((err)=>{
+ sails.log.warn(failureMessage+' Error details from LLM: '+err.stack);
+ return {
+ choices: [
+ {
+ message: {
+ content: `{ "risks": "${failureMessage}", "whatWillProbablyHappenDuringMaintenance": "${failureMessage}" }`
+ }
+ }
+ ]
+ };
+ });
+
+ let report;
+ try {
+ report = JSON.parse(openAiResponse.choices[0].message.content);
+ } catch (err) {
+ sails.log.warn('When trying to parse a JSON report returned from the Open AI API, an error occurred. Error details from JSON.parse: '+err.stack+'\n Report returned from Open AI:'+openAiResponse.choices[0].message.content);
+ report = {
+ risks: failureMessage,
+ whatWillProbablyHappenDuringMaintenance: failureMessage
+ };
+ }
+
+ return report;
+
+ }
+
+
+};
diff --git a/website/api/controllers/save-questionnaire-progress.js b/website/api/controllers/save-questionnaire-progress.js
index f89b1b116e..36794f96f6 100644
--- a/website/api/controllers/save-questionnaire-progress.js
+++ b/website/api/controllers/save-questionnaire-progress.js
@@ -63,22 +63,6 @@ module.exports = {
.set({
primaryBuyingSituation: primaryBuyingSituation
});
- // Send a POST request to Zapier
- await sails.helpers.http.post.with({
- url: 'https://hooks.zapier.com/hooks/catch/3627242/3pl7yt1/',
- data: {
- primaryBuyingSituation,
- emailAddress: this.req.me.emailAddress,
- webhookSecret: sails.config.custom.zapierSandboxWebhookSecret,
- }
- })
- .timeout(5000)
- .tolerate(['non200Response', 'requestFailed'], (err)=>{
- // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
- sails.log.warn(`When a user completed the 'What are you using Fleet for' questionnaire step, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`);
- return;
- });
-
// Set the primary buying situation in the user's session.
this.req.session.primaryBuyingSituation = primaryBuyingSituation;
}//fi
@@ -96,30 +80,32 @@ module.exports = {
// - yes-deployed-local: » Stage 3 (Tried Fleet but might not have a use case)
// - yes-deployed-long-time: Stage 2 (Tried Fleet long ago but might not fully grasp)
// - no: Stage 2 (Never tried Fleet and might not fully grasp)
- // 'how-many-hosts': No change // TODO (see above -- instead of no change there should be a change)
- // 'will-you-be-self-hosting': No change // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // 'how-many-hosts': Stage 6
+ // 'will-you-be-self-hosting': Stage 6
// 'what-are-you-working-on-eo-security'
- // - no-use-case-yet: » No change // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // - no-use-case-yet: » Stage 2/3 (depends on answer from 'have-you-ever-used-fleet' step)
// - All other options » Stage 4
// 'what-does-your-team-manage-eo-it'
- // - no-use-case-yet: » No change // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // - no-use-case-yet: » Stage 2/3 (depends on answer from 'have-you-ever-used-fleet' step)
// - All other options » Stage 4
// 'what-does-your-team-manage-vm'
- // - no-use-case-yet: » No change // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // - no-use-case-yet: » Stage 2/3 (depends on answer from 'have-you-ever-used-fleet' step)
// - All other options » Stage 4
// 'what-do-you-manage-mdm'
- // - no-use-case-yet: » No change // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // - no-use-case-yet: » Stage 2/3 (depends on answer from 'have-you-ever-used-fleet' step)
// - All other options » Stage 4
- // 'is-it-any-good': // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // 'is-it-any-good': Stage 2/3/4 (depends on answer from 'have-you-ever-used-fleet' & the buying situation specific step)
// 'what-did-you-think'
// - deploy-fleet-in-environment » Stage 5
- // - let-me-think-about-it » // TODO (see above -- instead of no change there should maybe be a change, sometimes)
+ // - let-me-think-about-it » Stage 2
// - host-fleet-for-me » N/A (currently not selectable, but should set the user's psychologicalStage to stage 5)
let psychologicalStage = userRecord.psychologicalStage;
// Get the value of the submitted formData, we do this so we only need to check one variable, instead of (formData.attribute === 'foo');
let valueFromFormData = _.values(formData)[0];
- if(currentStep === 'what-are-you-using-fleet-for') {
+ if(currentStep === 'start') {
+ // There is change when the user completes the start step.
+ } else if(currentStep === 'what-are-you-using-fleet-for') {
psychologicalStage = '2 - Aware';
} else if(currentStep === 'have-you-ever-used-fleet') {
if(['yes-deployed', 'yes-recently-deployed'].includes(valueFromFormData)) {
@@ -132,41 +118,94 @@ module.exports = {
// Otherwise, we'll just assume liu're only aware. Maybe liu don't fully grasp what Fleet can do.
psychologicalStage = '2 - Aware';
}
- } else if(['what-are-you-working-on-eo-security','what-does-your-team-manage-eo-it','what-does-your-team-manage-vm','what-do-you-manage-mdm'].includes(currentStep)){
- if(valueFromFormData === 'no-use-case-yet') {
- // If this user doe not have a use case for Fleet yet, set their psyStage to 3
- psychologicalStage = '3 - Intrigued';
- } else {// Otherwise, they have a use case and will be set to stage 4.
- psychologicalStage = '4 - Has use case';
- }
- } else if(currentStep === 'what-did-you-think') {
- // If the user is ready to deploy Fleet in their work environemnt, then they're ready to get buy-in from their team, so set their psyStage to 5.
- if(valueFromFormData === 'deploy-fleet-in-environment') {
- psychologicalStage = '5 - Personally confident';
- }
- // If the user selects let me think about it, their stage will not change.
+ } else {
+ // If the user submitted any other step, we'll set variables using the answers to the previous questions.
+ // Get the user's selected primaryBuyingSiutation.
+ let currentSelectedBuyingSituation = questionnaireProgress['what-are-you-using-fleet-for'].primaryBuyingSituation;
+ // Get the user's answer to the "Have you ever used Fleet?" question.
+ let hasUsedFleetAnswer = questionnaireProgress['have-you-ever-used-fleet'].fleetUseStatus;
+ if(['what-are-you-working-on-eo-security','what-does-your-team-manage-eo-it','what-does-your-team-manage-vm','what-do-you-manage-mdm'].includes(currentStep)){
+ if(valueFromFormData === 'no-use-case-yet') {
+ // Check the user's answer to the previous question
+ if(hasUsedFleetAnswer === 'yes-deployed-local'){
+ // If they've tried Fleet locally, set their stage to 3.
+ psychologicalStage = '3 - Intrigued';
+ } else {
+ psychologicalStage = '2 - Aware';
+ }
+ } else {// Otherwise, they have a use case and will be set to stage 4.
+ psychologicalStage = '4 - Has use case';
+ }
+ } else if(currentStep === 'is-it-any-good') {
+ if(currentSelectedBuyingSituation === 'mdm') {
+ // Since the mdm use case question is the only buying situation-sepcific question where a use case can't
+ // be selected, we'll check the user's previous answers befroe changing their psyStage
+ if(questionnaireProgress['what-do-you-manage-mdm'].mdmUseCase === 'no-use-case-yet'){
+ // Check the user's answer to the have-you-ever-used-fleet question.
+ if(hasUsedFleetAnswer === 'yes-deployed-local') {
+ // If they've tried Fleet locally, set their stage to 3.
+ psychologicalStage = '3 - Intrigued';
+ } else {
+ psychologicalStage = '2 - Aware';
+ }
+ } else {
+ psychologicalStage = '4 - Has use case';
+ }
+ } else {// For any other selected primary buying situation, since a use case will have been selected, set their psyStage to 4
+ psychologicalStage = '4 - Has use case';
+ // FUTURE: check previous answers for other selected buying situations.
+ }
+ } else if(currentStep === 'what-did-you-think') {
+ // If the user is ready to deploy Fleet in their work environemnt, then they're ready to get buy-in from their team, so set their psyStage to 5.
+ if(valueFromFormData === 'deploy-fleet-in-environment') {
+ psychologicalStage = '5 - Personally confident';
+ } else if(valueFromFormData === 'let-me-think-about-it') {
+ // If the user selects "Let me think about it", their stage change to 2
+ psychologicalStage = '2 - Aware';
+ }
+ // If the user selects "I’d like you to host Fleet for me", the form is not submitted, and they are taken to the /contact page instead. FUTURE: set stage to stage 5.
+ } else if(currentStep === 'how-many-hosts') {
+ // If they have Fleet deployed, they have team buy-in
+ psychologicalStage = '6 - Has team buy-in';
+ } else if(currentStep === 'will-you-be-self-hosting') {
+ // If they have Fleet deployed, they have team buy-in
+ psychologicalStage = '6 - Has team buy-in';
+ }//fi
+
}//fi
- // Send a POST request to Zapier
- await sails.helpers.http.post.with({
- url: 'https://hooks.zapier.com/hooks/catch/3627242/3nltwbg/',
- data: {
+ // Only update CRM records if the user's psychological stage changes.
+ if(currentStep !== userRecord.currentStep){
+ await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
emailAddress: this.req.me.emailAddress,
firstName: this.req.me.firstName,
lastName: this.req.me.lastName,
- primaryBuyingSituation: primaryBuyingSituation,
+ primaryBuyingSituation: primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : primaryBuyingSituation === 'vm' ? 'Vulnerability management' : undefined,
organization: this.req.me.organization,
psychologicalStage,
- currentStep,
- webhookSecret: sails.config.custom.zapierSandboxWebhookSecret,
- }
- })
- .timeout(5000)
- .tolerate(['non200Response', 'requestFailed'], (err)=>{
- // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
- sails.log.warn(`When a user completed a questionnaire step, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`);
- return;
- });
+ });
+ }
+ // TODO: send all other answers to Salesforce (when there are fields for them)
+
+ // await sails.helpers.http.post.with({
+ // url: 'https://hooks.zapier.com/hooks/catch/3627242/3nltwbg/',
+ // data: {
+ // emailAddress: this.req.me.emailAddress,
+ // firstName: this.req.me.firstName,
+ // lastName: this.req.me.lastName,
+ // primaryBuyingSituation: primaryBuyingSituation,
+ // organization: this.req.me.organization,
+ // psychologicalStage,
+ // currentStep,
+ // webhookSecret: sails.config.custom.zapierSandboxWebhookSecret,
+ // }
+ // })
+ // .timeout(5000)
+ // .tolerate(['non200Response', 'requestFailed'], (err)=>{
+ // // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
+ // sails.log.warn(`When a user completed a questionnaire step, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`);
+ // return;
+ // });
// Set the user's answer to the current step.
questionnaireProgress[currentStep] = formData;
// Clone the questionnaireProgress to prevent any mutations from sending it through the updateOne Waterline method.
diff --git a/website/api/controllers/view-endpoint-ops.js b/website/api/controllers/view-endpoint-ops.js
index e67c72cdce..ec913c6972 100644
--- a/website/api/controllers/view-endpoint-ops.js
+++ b/website/api/controllers/view-endpoint-ops.js
@@ -23,13 +23,19 @@ module.exports = {
// Get testimonials for the
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"repository_credentials": "",
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
}
© <%= (new Date()).getFullYear() %> Fleet Device Management Inc.
All trademarks, service marks, and company names are the property of their respective owners.
© <%= (new Date()).getFullYear() %> Fleet Inc.
All trademarks, service marks, and company names are the property of their respective owners.