diff --git a/.github/workflows/deploy-fleet-website.yml b/.github/workflows/deploy-fleet-website.yml index 0b5e657a0f..e88d8ebd3f 100644 --- a/.github/workflows/deploy-fleet-website.yml +++ b/.github/workflows/deploy-fleet-website.yml @@ -31,7 +31,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2 @@ -47,7 +47,7 @@ jobs: # Set the Node.js version - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -58,6 +58,9 @@ jobs: with: go-version: 1.19 + # Download top-level dependencies and build Storybook in the website's assets/ folder + - run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose + # Now start building! # > …but first, get a little crazy for a sec and delete the top-level package.json file # > i.e. the one used by the Fleet server. This is because require() in node will go diff --git a/.github/workflows/test-website.yml b/.github/workflows/test-website.yml index cf2470efdb..89e36b621a 100644 --- a/.github/workflows/test-website.yml +++ b/.github/workflows/test-website.yml @@ -8,6 +8,7 @@ on: - 'handbook/**' - 'schema/**' - 'articles/**' + - '.github/workflows/test-website.yml' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: @@ -28,17 +29,21 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v2 # Set the Node.js version - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + + # Download top-level dependencies and build Storybook in the website's assets/ folder. + - run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose + # Now start building! # > …but first, get a little crazy for a sec and delete the top-level package.json file # > i.e. the one used by the Fleet server. This is because require() in node will go diff --git a/.goreleaser.yml b/.goreleaser.yml index d44c472f20..e7cc4a6e47 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -59,9 +59,7 @@ archives: - id: fleet builds: - fleet - name_template: fleet_v{{.Version}}_{{.Os}} - replacements: - darwin: macos + name_template: fleet_v{{.Version}}_{{- if eq .Os "darwin" }}macos{{- else }}{{ .Os }}{{ end }} format_overrides: - goos: windows format: zip @@ -70,18 +68,14 @@ archives: - id: fleetctl builds: - fleetctl - name_template: fleetctl_v{{.Version}}_{{.Os}} - replacements: - darwin: macos + name_template: fleetctl_v{{.Version}}_{{- if eq .Os "darwin" }}macos{{- else }}{{ .Os }}{{ end }} wrap_in_directory: true - id: fleetctl-zip builds: - fleetctl - name_template: fleetctl_v{{.Version}}_{{.Os}} + name_template: fleetctl_v{{.Version}}_{{- if eq .Os "darwin" }}macos{{- else }}{{ .Os }}{{ end }} format: zip - replacements: - darwin: macos wrap_in_directory: true dockers: diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d06b070c..a4834bfa4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,97 @@ +## Fleet 4.34.0 (Jul 11, 2023) + +* Added execution of programmatic Windows MDM enrollment on eligible devices when Windows MDM is enabled. + +* Microsoft MDM Enrollment Protocol: Added support for the RequestSecurityToken messages. + +* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages. + +* Microsoft MDM Enrollment Protocol: Added support for the GetPolicies messages. + +* Added `enabled_windows_mdm` and `disabled_windows_mdm` activities when a user turns on/off Windows MDM. + +* Added support to enable and configure Windows MDM and to notify devices that are able to programmatically enroll. + +* Added ability to turn Windows MDM on and off from the Fleet UI. + +* Added enable and disable Windows MDM activity UI. + +* Updated MDM detail query ingestion to switch MDM profiles from "verifying" or "verified" status to "failed" status when osquery reports that this profile is not installed on the host. + +* Added notification and execution of programmatic Windows MDM unenrollment on eligible devices when Windows MDM is disabled. + +* Added the `FLEET_DEV_MDM_ENABLED` environment variable to enable the Windows MDM feature during its development and beta period. + +* Added the `mdm_enabled` feature flag information to the response payload of the `PATCH /config` endpoint. + +* When creating a PolicySpec, return the proper HTTP status code if the team is not found. + +* Added CPEMatchingRule type, used for correcting false positives caused by incorrect entries in the NVD dataset. + +* Optimized macOS CIS query "Ensure Appropriate Permissions Are Enabled for System Wide Applications" (5.1.5). + +* Updated macOS CIS policies 5.1.6 and 5.1.7 to use a new fleetd table `find_cmd` instead of relying on the osquery `file` table to improve performance. + +* Implemented the privacy_preferences table for the Fleetd Chrome extension. + +* Warnings in fleetctl now go to stderr instead of stdout. + +* Updated UI for transferred hosts activity items. + +* Added Organization support URL input on the setting page organization info form. + +* Added improved ABM 400 error message to the UI. + +* Hide any osquery tables or columns from Fleet UI that has hidden set to true to match Fleet website. + +* Ignore casing in SAML response for display name. For example the display name attribute can be provided now as `displayname` or `displayName`. + +* Provide feedback to users when `fleetctl login` is using EMAIL and PASSWORD environment variables. + +* Added a new activity `transferred_hosts` created when hosts are transferred to a new team (or no team). + +* Added milliseconds to the timestamp of auto-generated team name when creating a new team in `GET /mdm/apple/profiles/match`. + +* Improved dashboard loading states. + +* Improved UI for selecting targets. + +* Made sure that all configuration profiles and commands are sent to devices if MDM is turned on, even if the device never turned off MDM. + +* Fixed bug when reading filevault key in osquery and created new Fleet osquery extension table to read the file directly rather than via filelines table. + +* Fixed UI bug on host details and device user pages that caused the software search to not work properly when searching by CVE. + +* Fixed not validating the schema used in the Metadata URL. + +* Fixed improper HTTP status code if SMTP is invalid. + +* Fixed false positives for iCloud on macOS. + +* Fixed styling of copy message when copying fields. + +* Fixed a bug where an empty file uploaded to `POST /api/latest/fleet/mdm/apple/setup/eula` resulted in a 500; now returns a 400 Bad Request. + +* Fixed vulnerability dropdown that was hiding if no vulnerabilities. + +* Fixed scroll behavior with disk encryption status. + +* Fixed empty software image in sandbox mode. + +* Fixed improper HTTP status code when `fleet/forgot_password` endpoint is rate limited. + +* Fixed MaxBurst limit parameter for `fleet/forgot_password` endpoint. + +* Fixed a bug where reading from the replica would not read recent writes when matching a set of MDM profiles to a team (the `GET /mdm/apple/profiles/match` endpoint). + +* Fixed an issue that displayed Nudge to macOS hosts if MDM was configured but MDM features weren't turned on for the host. + +* Fixed tooltip word wrapping on the error cell in the macOS settings table. + +* Fixed extraneous loading spinner rendering on the software page. + +* Fixed styling bug on setup caused by new font being much wider. + ## Fleet 4.33.1 (Jun 20, 2023) * Fixed ChromeOS add host instructions to use variable Fleet URL. diff --git a/CODEOWNERS b/CODEOWNERS index 5e00194c41..ff8a4141ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,95 +1,70 @@ -# Go engineers are automatically added as reviewers when changes are made to go -# files or related backend files. +############################################################################################## +# ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███╗ ██╗███████╗██████╗ ███████╗ +# ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔═══██╗██║ ██║████╗ ██║██╔════╝██╔══██╗██╔════╝ +# ██║ ██║ ██║██║ ██║█████╗ ██║ ██║██║ █╗ ██║██╔██╗ ██║█████╗ ██████╔╝███████╗ +# ██║ ██║ ██║██║ ██║██╔══╝ ██║ ██║██║███╗██║██║╚██╗██║██╔══╝ ██╔══██╗╚════██║ +# ╚██████╗╚██████╔╝██████╔╝███████╗╚██████╔╝╚███╔███╔╝██║ ╚████║███████╗██║ ██║███████║ +# ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚══════╝ +############################################################################################## +# ⛔ This file indicates REQUIRED reviewers for changes to certain file paths in this repo. +# +# > How? This "requiredness" is provided natively by GitHub. If a team is specified, then +# > the logic behaves slightly differently. See GitHub's latest documentation on CODEOWNERS +# > for more information. +# +# ⚠️ For file paths not listed, the DRI is indicated elsewhere (website/config/custom.js). +# (In either case, the DRI is automatically requested for review when changes are proposed.) +# +# ✅ Some paths also have multiple individuals who are allowed to make changes without review, +# even though they are not the DRI. These are called "maintainers". +# +# For more information on how this works, see: +# - What is a DRI and how is this configured? https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility +# - Historical context: https://github.com/fleetdm/fleet/pull/12786 +############################################################################################## + + +############################################################################################## +# Golang files and other files related to the core product backend. +# (1 or more Golang-literate engineers is required to review changes.) +# FUTURE: Look for a way to not have this notify every single person in this "github team". +############################################################################################## *.go @fleetdm/go go.sum @fleetdm/go go.mod @fleetdm/go /server/ @fleetdm/go /cmd/ @fleetdm/go -# Compliance -/ee/cis/ @sharon-fdm @lucasmrod @marcosd4h @rachelElysia - -# MDM -/ee/tools/puppet @roperzh @gillespi314 @mna @georgekarrv - -# React engineers are automatically added as reviewers when changes are made to react files +############################################################################################## +# React files and other files related to the core product frontend. +# (1 or more React-literate engineers is required to review changes.) +# FUTURE: Look for a way to not have this notify every single person in this "github team". +############################################################################################## /frontend/ @fleetdm/frontend -# Infra/terraform -*.tf @edwardsb @zwinnerman-fleetdm @rfairburn -/infrastructure/ @zwinnerman-fleetdm @edwardsb @rfairburn -/charts/ @zwinnerman-fleetdm @edwardsb @rfairburn -/terraform @zwinnerman-fleetdm @edwardsb @rfairburn +############################################################################################## +# Config as code for infrastructure, internal security and IT use cases, and more. +# (1 or more infra-literate engineers is required to review changes.) +# FUTURE: Look for a way to not have this notify every single person in this "github team". +############################################################################################## +/infrastructure/ @fleetdm/infra +/charts/ @fleetdm/infra +/terraform/ @fleetdm/infra -# GitHub issue templates -/.github/ISSUE_TEMPLATE @mikermcneil - -# Codeowners file -/CODEOWNERS @mikermcneil - -# Changelog -/CHANGELOG.md @spokanemac - -# Fleet documentation (who is auto-requested as reviewer for changes to docs?) +############################################################################################## +# Key handbook pages w/ required reviewers +# +# (Especially useful for paths that tend to end up in PRs with lots of other reviewers) +############################################################################################## +/handbook/company/development-groups.md @mikermcneil +/handbook/company/why-this-way.md @mikermcneil +/handbook/company/README.md @mikermcneil +/handbook/business-operations/README.md @mikermcneil /docs/ @rachaelshaw +/schema/ @rachaelshaw #« Data tables (osquery/fleetd schema) documentation +CHANGELOG.md @lukeheath -# REST API reference documentation -/docs/Using-Fleet/REST-API.md @rachaelshaw -/docs/Contributing/API-for-contributors.md @rachaelshaw -# Standard query library YAML -/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml @zwass -# Expanded table documentation -/schema @eashaw - -# Articles -/articles @jarodreyes - -# Website -/website/ @eashaw -/website/views/ @eashaw -/website/assets/ @eashaw - -# Features table -# - CEO is DRI for pricing -# - Mo is DRI for features table -# - Eric is DRI for website frontend code -/website/views/pages/pricing.ejs @mikermcneil -/handbook/product/pricing-features-table.yml @mikermcneil - -# Website redirects and URLs -/website/config/routes.js @mikermcneil @eashaw - -# Website backend, scripts, deps -/website/api/ @mikermcneil @eashaw -/website/config/ @mikermcneil @eashaw -/website/scripts/ @mikermcneil @eashaw -/website/package.json @mikermcneil @eashaw - -# GitHub brandfront -/README.md @mikermcneil - -# NPM brandfront (npmjs.com/package/fleetctl) -/tools/fleetctl-npm/README.md @mikermcneil - -# Handbook -/handbook/company @mikermcneil -/handbook/company/* @mikermcneil -/handbook/business-operations @mikermcneil -/handbook/business-operations/* @mikermcneil -/handbook/engineering @lukeheath -/handbook/engineering/* @lukeheath -/handbook/product @zhumo -/handbook/product/* @zhumo -/handbook/customers @alexmitchelliii -/handbook/customers/* @alexmitchelliii -/handbook/marketing @jarodreyes -/handbook/marketing/* @jarodreyes -/handbook/README.md @mikermcneil # « This is the "Table of contents" - -# -# For configuration that determines auto-approval + auto-unfreezing, so that contributors -# can merge their own PRs without additional approval, please see the latest version of: -# https://github.com/fleetdm/fleet/blob/74f65447b718663bd04df31ea1da28915d98792c/website/config/custom.js#L88-L128 -# +# ℹ️ But wait, there's more! +# See the comments up top to learn where else DRIs and maintainers are configured. diff --git a/README.md b/README.md index 2f39ac6026..e6332f1ddd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Open-source platform for IT and security teams with thousands of computers. Designed for APIs, GitOps, webhooks, YAML, and humans. -Wallpaper featuring a futuristic cloud city with the Fleet logo +https://github.com/fleetdm/fleet/assets/618009/f705c7ee-6efe-448e-b5ee-f5535d7cd101 ## What's it for? @@ -56,7 +56,7 @@ In keeping with Fleet's value of openness, [Fleet Device Management's company ha -## Is it any good? +## Is it any good?? Fleet is used in production by IT and security teams with thousands of laptops and servers. Many deployments support tens of thousands of hosts, and a few large organizations manage deployments as large as 400,000+ hosts. diff --git a/articles/deploying-fleet-on-render.md b/articles/deploying-fleet-on-render.md index 337ac5f2cf..ccbe413709 100644 --- a/articles/deploying-fleet-on-render.md +++ b/articles/deploying-fleet-on-render.md @@ -19,9 +19,13 @@ First let’s get these dependencies up and running on Render. Fleet uses MySQL as the datastore to organize host enrollment and other metadata around serving Fleet. Start by forking [https://github.com/edwardsb/render-mysql](https://github.com/edwardsb/render-mysql), then create a new private service within Render. When prompted for the repository — enter your fork’s URL here. -![Private Service component in Render](../website/assets/images/articles/deploying-fleet-on-render-2-216x163@2x.png) +![Private Service component in Render](../website/assets/images/articles/deploying-fleet-on-render-1-216x165@2x.png) *Private Service component in Render* +Your private service should look like this: +![Private Service settings in Render](../website/assets/images/articles/deploying-fleet-on-render-5-450x286@2x.png) +*Private Service settings in Render* + This private service will run MySQL, our database, so let’s give it a fitting name, something like “fleet-mysql”. We’re also going to need to set up some environment variables and a disk to mount. Expand “Advanced” and enter the following: @@ -39,6 +43,10 @@ We’re also going to need to set up some environment variables and a disk to mo - Mount Path: `/var/lib/mysql` - Size: `50GB` +Once you've setup your mysql service on Render we will need to copy the address. You can find that here: +![mysql address on render](../website/assets/images/articles/deploying-fleet-on-render-6-666x416@2x.png) +*mysql address on Render* + --- ## Redis @@ -73,7 +81,7 @@ Give it the following environment variables: Additionally we’ll configure the following so Render knows how to build our app and make sure its healthy: -![Additional component details](../website/assets/images/articles/deploying-fleet-on-render-3-512x213@2x.png) +![Additional component details](../website/assets/images/articles/deploying-fleet-on-render-7-627x416@2x.png) - Health Check Path: `/healthz` - Docker Build Context Directory: `.` @@ -97,7 +105,9 @@ Fleet is up and running, head to your public URL. You should be prompted with a setup page, where you can enter your name, email, and password. Run through those steps and you should have an empty hosts page waiting for you. -You’ll find the enroll-secret after clicking “Add New Hosts”. This is a special secret the host will need to register to your Fleet instance. Once you have the enroll-secret you can use `fleetctl` to create Orbit installers, which makes installing and updating osquery super simple. [Download fleetctl](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.3.0) and try the following command (Docker require) on your terminal: +You’ll find the enroll-secret after clicking “Add hosts”. This is a special secret the host will need to register to your Fleet instance. Once you have the enroll-secret you can use `fleetctl` to generate installers, which makes installing and updating osquery super simple. + +To install `fleetctl`, which is the command line interface (CLI) used to communicate between your computer and Fleet, you either run `npm install -g fleetctl` or [download fleetctl](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.3.0) from Github. Once it's installed try the following command (Docker require) on your terminal: ``` fleetctl package --type=msi --enroll-secret --fleet-url https://.onrender.com diff --git a/articles/embracing-the-future-declarative-device-management.md b/articles/embracing-the-future-declarative-device-management.md new file mode 100644 index 0000000000..134266ef1a --- /dev/null +++ b/articles/embracing-the-future-declarative-device-management.md @@ -0,0 +1,65 @@ +# Embracing the future: Declarative Device Management + +![Embracing the future: Declarative Device Management](../website/assets/images/articles/embracing-the-future-declarative-device-management@2x.png) + +As a Mac administrator, managing a fleet of Apple devices across your organization requires consistency and airtight security. With a variety of system services and background tasks to oversee, the challenge is not only to maintain uniform configurations but also to keep the organization's data secure. Recognizing these challenges, Apple has advanced a powerful new approach - Declarative Device Management (DDM). + +DDM is a paradigm shift in device management, enabling a more efficient and secure administration of macOS devices. It allows for tamper-resistant configurations and ensures simplified monitoring of system services and background tasks. + +In this blog post, we dive into Apple's forthcoming DDM in macOS Sonoma. Specifically, we'll explore how it will alter the way you manage system services, certificates and identities, and how it transitions you from traditional Mobile Device Management (MDM) systems. Whether you're an experienced Mac admin or just getting started, hopefully, this guide will provide some insights into DDM for you and your organization. Let's dive in! + + +## Declarative device management for system services + +DDM paves the way for a secure and reliable mechanism to manage system services. Using tamper-resistant system configuration files for different system services ensures uniform and secure configurations across all devices. Declarative Device Management provides an added layer of protection against accidental changes by users. + +For instance, system services like sshd, sudo, PAM, CUPS, Apache httpd, bash and Z-shells will be able to adopt managed service configuration files to ensure consistency and compliance. The configuration files reference a data asset that provides a ZIP archive of SSH keys that is downloaded and expanded into a tamper-resistant, service-specific location when required conditions are met—for example, FileVault is enabled—and are always prioritized over any default or overridden system configuration. + + +## Monitoring and compliance rules for background tasks + +DDM provides an excellent way of keeping track of background tasks. A new status item in this coming release reports the list of installed background tasks, making it easier to verify that required tasks are running and unwanted tasks aren't. + +In addition, the FileVault enabled state of the macOS boot volume is reported, allowing you to install sensitive configurations only when it is safe to proceed. With these features, you can ensure compliance and consistency across all macOS devices in your organization. + + +## Secure access with certificates and identities + +Certificates and identities play a crucial role in ensuring secure access to organizational resources. In this context, DDM provides a more efficient mechanism for managing certificates and identities using its declaration data model. + +Certificates and identities are defined as asset declarations, which various configurations can reference. This eliminates the need for duplicating certificates and identities across multiple profiles, thereby reducing management overhead. + + +## A new paradigm: software updates + +Apple's DDM introduces a redefined software update process, which marks another significant step forward in device management. + +Traditionally, administrators have faced considerable challenges in managing software updates. However, with DDM, this process has been dramatically simplified. The Declarative model handles scheduling and applying updates, allowing administrators to specify the desired state – for instance, maintaining the latest software version – and leave the rest to DDM. + +To improve upon this functionality, Fleet, with its osquery integration, allows admins to monitor the status of these updates in real time. It provides critical insights about the update process, such as software versions, pending updates, and the update history. These features make the software update process significantly more manageable and transparent. + +DDM represents an important advancement in how we manage and understand software updates. It not only will streamline administrative tasks but also elevates the overall security, performance, and integrity of the devices Mac admins manage. + + +## Seamless transition from MDM to DDM + +Transitioning from traditional MDM to DDM will be a challenge. However, DDM provides a smooth transition without causing disruption or leaving a management gap. This is achieved by allowing DDM to take over the management of already installed MDM profiles without the need to remove them. + + +## Fleet + osquery + DDM = 💗 + +The innovations introduced with DDM, including the new software update process, represent a paradigm shift in device management. Fleet's MDM solution, powered by osquery, complements these changes and offers a GitOps-driven management platform for Mac admins. + +As we continue to navigate this evolving landscape, we have tools that equip us better than ever to handle the challenges and complexities of modern device management. This new era presents opportunities for enhanced security, control, and efficiency in managing our devices. + +Fleet is transforming how we manage and secure devices. Offering an open-core, cross-platform solution, Fleet is committed to empowering Mac admins with the tools they need to meet the challenges of today's and tomorrow's device management. Through its powerful and versatile platform, Fleet is illuminating the path forward in device management. + + + + + + + + + + diff --git a/articles/fleet-4.34.0.md b/articles/fleet-4.34.0.md new file mode 100644 index 0000000000..fdfd08f380 --- /dev/null +++ b/articles/fleet-4.34.0.md @@ -0,0 +1,134 @@ +# Fleet 4.34.0 | ChromeOS tables, CIS Benchmark load testing. + +![Fleet 4.34.0](../website/assets/images/articles/fleet-4.34.0-1600x900@2x.png) + +Fleet 4.34.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.33.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 + +* Fleet adds support for ChromeOS +* Boosted compliance with 'verified' status + + +### Additional tables for ChromeOS + +In line with Fleet's value of 🟢 Results, we work relentlessly to enhance your experience. Our aim is to deliver results, focusing on pragmatic and meaningful improvements. With this in mind, we are delighted to introduce new ChromeOS-specific tables: screenlock, system_state, privacy_preferences, and disk_info. These additions not only represent our commitment to iterative progress but also our dedication to enhancing Fleet's utility for managing and understanding your ChromeOS devices better. + + +### Load testing CIS Benchmarks for macOS + +Embodying Fleet's values of 🟠 Ownership and 🟢 Results, our team is always ready to tackle challenges head-on for the sake of delivering a reliable and high-performing product. Recently, we pondered the performance impact of running the comprehensive set of 100 CIS Benchmarks for macOS, known colloquially as "eating our own dogfood." + +Upon digging deeper, our engineers identified CIS queries 5.1.5, 5.1.6, and 5.1.7 as the three primary outliers in terms of CPU usage and memory footprint. These queries were found to be causing process terminations due to high resource usage. + +The queries, which are designed to verify appropriate permissions for system-wide applications (5.1.5) and ensure no world-writable files exist in the System Folder (5.1.6) or Library Folder (5.1.7), had to be refined for efficiency. + +With a clear focus on achieving results and owning the challenges we face, this rigorous load testing has led not only to the improvement of the 5.1.5, 5.1.6, and 5.1.7 queries but also to the development of additional tooling for future load testing. This is another stride in our continued effort to enhance Fleet and osquery's performance, reliability, and user experience. + + +## More new features, improvements, and bug fixes + +* Added execution of programmatic Windows MDM enrollment on eligible devices when Windows MDM is enabled. + +* Microsoft MDM Enrollment Protocol: Added support for the RequestSecurityToken messages. + +* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages. + +* Microsoft MDM Enrollment Protocol: Added support for the GetPolicies messages. + +* Added `enabled_windows_mdm` and `disabled_windows_mdm` activities when a user turns on/off Windows MDM. + +* Added support to enable and configure Windows MDM and to notify devices that are able to programmatically enroll. + +* Added ability to turn Windows MDM on and off from the Fleet UI. + +* Added enable and disable Windows MDM activity UI. + +* Updated MDM detail query ingestion to switch MDM profiles from "verifying" or "verified" status to "failed" status when osquery reports that this profile is not installed on the host. + +* Added notification and execution of programmatic Windows MDM unenrollment on eligible devices when Windows MDM is disabled. + +* Added the `FLEET_DEV_MDM_ENABLED` environment variable to enable the Windows MDM feature during its development and beta period. + +* Added the `mdm_enabled` feature flag information to the response payload of the `PATCH /config` endpoint. + +* When creating a PolicySpec, return the proper HTTP status code if the team is not found. + +* Added CPEMatchingRule type, used for correcting false positives caused by incorrect entries in the NVD dataset. + +* Optimized macOS CIS query "Ensure Appropriate Permissions Are Enabled for System Wide Applications" (5.1.5). + +* Updated macOS CIS policies 5.1.6 and 5.1.7 to use a new fleetd table `find_cmd` instead of relying on the osquery `file` table to improve performance. + +* Implemented the privacy_preferences table for the Fleetd Chrome extension. + +* Warnings in fleetctl now go to stderr instead of stdout. + +* Updated UI for transferred hosts activity items. + +* Added Organization support URL input on the setting page organization info form. + +* Added improved ABM 400 error message to the UI. + +* Hide any osquery tables or columns from Fleet UI that has hidden set to true to match Fleet website. + +* Ignore casing in SAML response for display name. For example, the display name attribute can be provided now as `displayname` or `displayName`. + +* Provide feedback to users when `fleetctl login` is using EMAIL and PASSWORD environment variables. + +* Added a new activity `transferred_hosts` created when hosts are transferred to a new team (or no team). + +* Added milliseconds to the timestamp of the auto-generated team name when creating a new team in `GET /mdm/apple/profiles/match`. + +* Improved dashboard loading states. + +* Improved UI for selecting targets. + +* Made sure that all configuration profiles and commands are sent to devices if MDM is turned on, even if the device never turned off MDM. + +* Fixed bug when reading FileVault key in osquery and created new Fleet osquery extension table to read the file directly rather than via filelines table. + +* Fixed UI bug on host details and device user pages that caused the software search to not work properly when searching by CVE. + +* Fixed not validating the schema used in the Metadata URL. + +* Fixed improper HTTP status code if SMTP is invalid. + +* Fixed false positives for iCloud on macOS. + +* Fixed styling of copy message when copying fields. + +* Fixed a bug where an empty file uploaded to `POST /api/latest/fleet/mdm/apple/setup/eula` resulted in a 500; now returns a 400 Bad Request. + +* Fixed vulnerability dropdown that was hiding if no vulnerabilities. + +* Fixed scroll behavior with disk encryption status. + +* Fixed empty software image in sandbox mode. + +* Fixed improper HTTP status code when `fleet/forgot_password` endpoint is rate limited. + +* Fixed MaxBurst limit parameter for `fleet/forgot_password` endpoint. + +* Fixed a bug where reading from the replica would not read recent writes when matching a set of MDM profiles to a team (the `GET /mdm/apple/profiles/match` endpoint). + +* Fixed an issue that displayed Nudge to macOS hosts if MDM was configured but MDM features weren't turned on for the host. + +* Fixed tooltip word wrapping on the error cell in the macOS settings table. + +* Fixed extraneous loading spinner rendering on the software page. + +* Fixed styling bug on setup caused by new font being much wider. + + +## 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.34.0. + + + + + + + diff --git a/articles/psu-macadmins-conference-2023.md b/articles/psu-macadmins-conference-2023.md new file mode 100644 index 0000000000..b8fddd0687 --- /dev/null +++ b/articles/psu-macadmins-conference-2023.md @@ -0,0 +1,49 @@ +# Mac admins summer camp ⛺ at PSU MacAdmins Conference 2023 + +[![PSU MacAdmins Conference July 18-21](../website/assets/images/articles/psu-macadmins-conference-2023@2x.png)](https://mdoyvr.com/) + +Hello there, macOS admins! Let's talk about the upcoming PSU MacAdmins Conference 2023 (aka Summer Camp for Mac Admins). PSUMAC is July 18-21 at Penn State University. Hope to see you there. + + +## What's on the agenda? + +PSU MacAdmins is not your typical conference. There are loads of technical sessions diving deep into various aspects of macOS administration. You can get your hands dirty with topics like: + + +* system security—(think password management) +* file encryption +* network security +* macOS deployment +* configuration management +* software delivery + + +And that's not all. There are sessions on managing macOS systems better, where you can learn about user management, group policies, and troubleshooting. Plus, there's plenty of opportunity to explore the latest macOS tools and technologies—ever wanted to get the scoop on Device Management, Munki, or macOS Deployment? Well, now's your chance! + +One unique aspect I'm personally excited about is the _Hallway Track_. The hallway track, aka seeing fellow Mac Admins in passing between sessions 🤣, at meals, and at evening events, is a fantastic opportunity to interact with colleagues from different industries and skill levels. What better way to learn than through conversation and shared experiences, right? + +And it's not all work and no play. One of the dinners will be on Penn State's Building Business Meadow. I hear there will be various lawn games to play, but no lawn darts 🎯. Penn State's beautiful Arboretum is just across the street and will be open until dusk. Also, don't forget to stop by the [Berkey Creamery](https://creamery.psu.edu/) for a generous scoop of the highest butterfat ice cream 🍨 you will find (and, yes, you can have ice cream shipped home). + + +## Presenter highlight + +I'm excited to say that I'll be sharing some of my experiences at Fleet and beyond in a session on Thursday, July 20, at 9:00 am. My talk, "[Cross-platform open-source monitoring and reporting](https://sched.co/1MmXv)", will focus on how combining Fleet and osquery can provide real-time data from endpoints and proactively trigger support tickets or notifications when a computer encounters issues. It's a topic close to my heart, and I'm eager to share what I've learned. + +Another must-see is Greg Neagle's talk at 10:45 am the same day, titled "[The Past, Present, and Future of Munki](https://sched.co/1OIYF)." If you don't know, Greg manages macOS devices at Walt Disney Animation Studios and is the primary developer of Munki. He'll be sharing some fantastic insights into Munki's development journey, its current standing, and where it's headed next. I'm told there'll even be an opportunity for attendees to contribute to Munki's future, so make sure not to miss it! + + +## Come say hi to Fleet (and get some cool swag) + +Here at Fleet, we're super excited to be sponsoring this conference. Supporting the MacAdmin community is what we're all about. Not only are we sponsoring the conference, but we're also backing Greg's presentation. Don't forget to swing by our booth—we've got some fun stickers and swag you might like! + +In a nutshell, the [PSU MacAdmins Conference 2023](https://macadmins.psu.edu/) is shaping up to be an event packed with valuable insights and networking opportunities. I'm excited to see all of you there, and here's to learning and growing together in our macOS admin journey! + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/down-arrow.png b/assets/images/down-arrow.png deleted file mode 100644 index 443ecdcdd1..0000000000 Binary files a/assets/images/down-arrow.png and /dev/null differ diff --git a/assets/images/icon-accordion-collapse-black-16x16@2x.png b/assets/images/icon-accordion-collapse-black-16x16@2x.png deleted file mode 100644 index d89ec4e652..0000000000 Binary files a/assets/images/icon-accordion-collapse-black-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-accordion-collapse-blue-16x16@2x.png b/assets/images/icon-accordion-collapse-blue-16x16@2x.png deleted file mode 100644 index d89ec4e652..0000000000 Binary files a/assets/images/icon-accordion-collapse-blue-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-action-check-16x15@2x.png b/assets/images/icon-action-check-16x15@2x.png deleted file mode 100644 index 3211203c47..0000000000 Binary files a/assets/images/icon-action-check-16x15@2x.png and /dev/null differ diff --git a/assets/images/icon-action-disable-14x14@2x.png b/assets/images/icon-action-disable-14x14@2x.png deleted file mode 100644 index aad0332243..0000000000 Binary files a/assets/images/icon-action-disable-14x14@2x.png and /dev/null differ diff --git a/assets/images/icon-apple-black-24x24@2x.png b/assets/images/icon-apple-black-24x24@2x.png deleted file mode 100644 index 50f389ab87..0000000000 Binary files a/assets/images/icon-apple-black-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-apple-vibrant-blue-24x24@2x.png b/assets/images/icon-apple-vibrant-blue-24x24@2x.png deleted file mode 100644 index 4dd07cfd50..0000000000 Binary files a/assets/images/icon-apple-vibrant-blue-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-close-dark-blue-grey-16x16@2x.png b/assets/images/icon-close-dark-blue-grey-16x16@2x.png deleted file mode 100644 index c45d9d9c28..0000000000 Binary files a/assets/images/icon-close-dark-blue-grey-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-close-fleet-purple-16x16@2x.png b/assets/images/icon-close-fleet-purple-16x16@2x.png deleted file mode 100644 index 106ac9e468..0000000000 Binary files a/assets/images/icon-close-fleet-purple-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-collapse-blue-16x16@2x.png b/assets/images/icon-collapse-blue-16x16@2x.png deleted file mode 100644 index d89ec4e652..0000000000 Binary files a/assets/images/icon-collapse-blue-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-darwin-fleet-black-16x16@2x.png b/assets/images/icon-darwin-fleet-black-16x16@2x.png deleted file mode 100644 index 4e2c79585c..0000000000 Binary files a/assets/images/icon-darwin-fleet-black-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-issue-fleet-black-16x16@2x.png b/assets/images/icon-issue-fleet-black-16x16@2x.png deleted file mode 100644 index 7cb03cfc43..0000000000 Binary files a/assets/images/icon-issue-fleet-black-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-low-disk-space-32x19@2x.png b/assets/images/icon-low-disk-space-32x19@2x.png deleted file mode 100644 index 2328156453..0000000000 Binary files a/assets/images/icon-low-disk-space-32x19@2x.png and /dev/null differ diff --git a/assets/images/icon-mac-48x48@2x.png b/assets/images/icon-mac-48x48@2x.png deleted file mode 100644 index a27e2bd9e4..0000000000 Binary files a/assets/images/icon-mac-48x48@2x.png and /dev/null differ diff --git a/assets/images/icon-main-admin-white-24x24@2x.png b/assets/images/icon-main-admin-white-24x24@2x.png deleted file mode 100644 index 88b87a717c..0000000000 Binary files a/assets/images/icon-main-admin-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-main-help-white-24x24@2x.png b/assets/images/icon-main-help-white-24x24@2x.png deleted file mode 100644 index 6ea026223a..0000000000 Binary files a/assets/images/icon-main-help-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-main-hosts-white-24x24@2x.png b/assets/images/icon-main-hosts-white-24x24@2x.png deleted file mode 100644 index 548f5d8135..0000000000 Binary files a/assets/images/icon-main-hosts-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-main-hosts@2x-16x16@2x.png b/assets/images/icon-main-hosts@2x-16x16@2x.png deleted file mode 100644 index 915d4d80e0..0000000000 Binary files a/assets/images/icon-main-hosts@2x-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-main-logout-white-24x24@2x.png b/assets/images/icon-main-logout-white-24x24@2x.png deleted file mode 100644 index 5880603c58..0000000000 Binary files a/assets/images/icon-main-logout-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-main-packs-white-24x24@2x.png b/assets/images/icon-main-packs-white-24x24@2x.png deleted file mode 100644 index c4c8a4e1d9..0000000000 Binary files a/assets/images/icon-main-packs-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-main-packs@2x-16x16@2x.png b/assets/images/icon-main-packs@2x-16x16@2x.png deleted file mode 100644 index c26515bd22..0000000000 Binary files a/assets/images/icon-main-packs@2x-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-main-policies-16x16@2x.png b/assets/images/icon-main-policies-16x16@2x.png deleted file mode 100644 index 2ca393ed8f..0000000000 Binary files a/assets/images/icon-main-policies-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-main-queries@2x-16x16@2x.png b/assets/images/icon-main-queries@2x-16x16@2x.png deleted file mode 100644 index 2a14097813..0000000000 Binary files a/assets/images/icon-main-queries@2x-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-main-query-white-24x24@2x.png b/assets/images/icon-main-query-white-24x24@2x.png deleted file mode 100644 index b6def88877..0000000000 Binary files a/assets/images/icon-main-query-white-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-missing-hosts-28x24@2x.png b/assets/images/icon-missing-hosts-28x24@2x.png deleted file mode 100644 index 08a60ad65c..0000000000 Binary files a/assets/images/icon-missing-hosts-28x24@2x.png and /dev/null differ diff --git a/assets/images/icon-plus-purple-32x32@2x.png b/assets/images/icon-plus-purple-32x32@2x.png deleted file mode 100644 index 44997f7e83..0000000000 Binary files a/assets/images/icon-plus-purple-32x32@2x.png and /dev/null differ diff --git a/assets/images/icon-search-fleet-black-16x16@2x.png b/assets/images/icon-search-fleet-black-16x16@2x.png deleted file mode 100644 index 923a571e2e..0000000000 Binary files a/assets/images/icon-search-fleet-black-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-software-16x16@2x.png b/assets/images/icon-software-16x16@2x.png deleted file mode 100644 index f731b0b910..0000000000 Binary files a/assets/images/icon-software-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-windows-48x48@2x.png b/assets/images/icon-windows-48x48@2x.png deleted file mode 100644 index d325a309ff..0000000000 Binary files a/assets/images/icon-windows-48x48@2x.png and /dev/null differ diff --git a/assets/images/icon-windows-black-24x24@2x.png b/assets/images/icon-windows-black-24x24@2x.png deleted file mode 100644 index f0aa38c98f..0000000000 Binary files a/assets/images/icon-windows-black-24x24@2x.png and /dev/null differ diff --git a/assets/images/icon-windows-fleet-black-16x16@2x.png b/assets/images/icon-windows-fleet-black-16x16@2x.png deleted file mode 100644 index e3233d8a4c..0000000000 Binary files a/assets/images/icon-windows-fleet-black-16x16@2x.png and /dev/null differ diff --git a/assets/images/icon-windows-vibrant-blue-24x24@2x.png b/assets/images/icon-windows-vibrant-blue-24x24@2x.png deleted file mode 100644 index 5de3e9280f..0000000000 Binary files a/assets/images/icon-windows-vibrant-blue-24x24@2x.png and /dev/null differ diff --git a/changes/10292-optimize-macos-cis-query-5.1.5 b/changes/10292-optimize-macos-cis-query-5.1.5 deleted file mode 100644 index 63ff07e47a..0000000000 --- a/changes/10292-optimize-macos-cis-query-5.1.5 +++ /dev/null @@ -1 +0,0 @@ -* Optimize macOS CIS query "Ensure Appropriate Permissions Are Enabled for System Wide Applications" (5.1.5). diff --git a/changes/11037-privacy_preferences-chromeos-table b/changes/11037-privacy_preferences-chromeos-table deleted file mode 100644 index be1c659b88..0000000000 --- a/changes/11037-privacy_preferences-chromeos-table +++ /dev/null @@ -1 +0,0 @@ -* Implement the privacy_preferences table for the Fleetd Chrome extension diff --git a/changes/11355-software-page-rendering-bugs b/changes/11355-software-page-rendering-bugs deleted file mode 100644 index e0ee3a148c..0000000000 --- a/changes/11355-software-page-rendering-bugs +++ /dev/null @@ -1 +0,0 @@ -- Fix a bug where an extraneous loading spinner was rendered on the Software page. diff --git a/changes/11655-hide-osquery-table-info b/changes/11655-hide-osquery-table-info deleted file mode 100644 index abb6d9b3dc..0000000000 --- a/changes/11655-hide-osquery-table-info +++ /dev/null @@ -1 +0,0 @@ -- Hide any osquery tables or columns from Fleet UI that has hidden set to true to match Fleet website diff --git a/changes/11927-vuln-false-positive-icloud b/changes/11927-vuln-false-positive-icloud deleted file mode 100644 index 0ca4fcfd6b..0000000000 --- a/changes/11927-vuln-false-positive-icloud +++ /dev/null @@ -1,3 +0,0 @@ -- Added CPEMatchingRule type, used for correcting false positives caused by incorrect entries in the - NVD dataset. -- Fixed false positives for iCloud on macOS. diff --git a/changes/12310-setup-styling b/changes/12310-setup-styling deleted file mode 100644 index 4a17a3c5f3..0000000000 --- a/changes/12310-setup-styling +++ /dev/null @@ -1 +0,0 @@ -Fix styling bug on setup caused by new font being much wider diff --git a/changes/12368-copy-message b/changes/12368-copy-message deleted file mode 100644 index b2b7b17bfd..0000000000 --- a/changes/12368-copy-message +++ /dev/null @@ -1 +0,0 @@ -- Fix styling of copy message when copying fields diff --git a/changes/12420-handle-policies-with-invalid-queries-desktop-endpoint b/changes/12420-handle-policies-with-invalid-queries-desktop-endpoint new file mode 100644 index 0000000000..9385222293 --- /dev/null +++ b/changes/12420-handle-policies-with-invalid-queries-desktop-endpoint @@ -0,0 +1,2 @@ +- If a policy was defined with an invalid query, the desktop endpoint should count that policy as a + failed policy. diff --git a/changes/12480-puppet-module-changes b/changes/12480-puppet-module-changes new file mode 100644 index 0000000000..a9a7371b1a --- /dev/null +++ b/changes/12480-puppet-module-changes @@ -0,0 +1 @@ +* Improve the reporting of the puppet module to only report as changed profiles that actually changed during a run. diff --git a/changes/12481-profile-redelivery-v2 b/changes/12481-profile-redelivery-v2 new file mode 100644 index 0000000000..84612e93c9 --- /dev/null +++ b/changes/12481-profile-redelivery-v2 @@ -0,0 +1 @@ +* Improved delivery of Apple MDM profiles by not re-sending `InstallProfile` commands if a host switches teams but the profile contents are the same. diff --git a/changes/12532-puppet-module-team-assignment b/changes/12532-puppet-module-team-assignment new file mode 100644 index 0000000000..c3cb0564a8 --- /dev/null +++ b/changes/12532-puppet-module-team-assignment @@ -0,0 +1 @@ +* Changed how team assignment works for the Puppet module, for more details see the [README](https://github.com/fleetdm/fleet/blob/main/ee/tools/puppet/fleetdm/README.md) diff --git a/changes/12570-mask-webhook-url-logs b/changes/12570-mask-webhook-url-logs new file mode 100644 index 0000000000..b86d5d399a --- /dev/null +++ b/changes/12570-mask-webhook-url-logs @@ -0,0 +1 @@ +- Updated server logging for webhook requests to mask URL query values if the query param name includes "secret", "token", "key", "password". diff --git a/changes/12582-nudge-mdm b/changes/12582-nudge-mdm deleted file mode 100644 index 4656110d23..0000000000 --- a/changes/12582-nudge-mdm +++ /dev/null @@ -1 +0,0 @@ -* Fixed an issue that displayed Nudge to macOS hosts if MDM was configured but MDM features weren't turned on for the host diff --git a/changes/12608-force-fv b/changes/12608-force-fv new file mode 100644 index 0000000000..b0257ce47e --- /dev/null +++ b/changes/12608-force-fv @@ -0,0 +1 @@ +* Set `DeferForceAtUserLoginMaxBypassAttempts` to `1` in the default FileVault profile installed by Fleet. diff --git a/changes/bug-10720-ratelimits-should-return-proper-status-code b/changes/bug-10720-ratelimits-should-return-proper-status-code deleted file mode 100644 index 0ecf815db1..0000000000 --- a/changes/bug-10720-ratelimits-should-return-proper-status-code +++ /dev/null @@ -1,3 +0,0 @@ -- If the `fleet/forgot_password` endpoint is rate limited it should return the proper HTTP status - code. -- Fixed MaxBurst limit parameter for `fleet/forgot_password` endpoint. diff --git a/changes/bug-10867-output-warns-to-stdout b/changes/bug-10867-output-warns-to-stdout deleted file mode 100644 index 5fc2bf7263..0000000000 --- a/changes/bug-10867-output-warns-to-stdout +++ /dev/null @@ -1 +0,0 @@ -- Warnings in fleetctl should go to stderr instead of stdout. diff --git a/changes/bug-11636-vuln-dropdown b/changes/bug-11636-vuln-dropdown deleted file mode 100644 index db1a094732..0000000000 --- a/changes/bug-11636-vuln-dropdown +++ /dev/null @@ -1 +0,0 @@ -- Fix vuln dropdown that was hiding if no vulnerabilities diff --git a/changes/bug-11898-targets-selector-styling b/changes/bug-11898-targets-selector-styling deleted file mode 100644 index 74d487378b..0000000000 --- a/changes/bug-11898-targets-selector-styling +++ /dev/null @@ -1 +0,0 @@ -Cleaner UI for selecting targets diff --git a/changes/bug-12108-weird-scroll-behavior b/changes/bug-12108-weird-scroll-behavior deleted file mode 100644 index 91817c5b9a..0000000000 --- a/changes/bug-12108-weird-scroll-behavior +++ /dev/null @@ -1 +0,0 @@ -- Fix funky scroll behavior with disk encryption status diff --git a/changes/bug-12308-sandbox-software-image b/changes/bug-12308-sandbox-software-image deleted file mode 100644 index b1ac9b07d1..0000000000 --- a/changes/bug-12308-sandbox-software-image +++ /dev/null @@ -1 +0,0 @@ -- Fix empty software image in sandbox mode diff --git a/changes/bug-12332-dashboard-loading-state b/changes/bug-12332-dashboard-loading-state deleted file mode 100644 index 526bdfbdb7..0000000000 --- a/changes/bug-12332-dashboard-loading-state +++ /dev/null @@ -1 +0,0 @@ -- Clean up dashboard loading states diff --git a/changes/bug-12403-fix-post-eula-status-code b/changes/bug-12403-fix-post-eula-status-code deleted file mode 100644 index 4f444e35ca..0000000000 --- a/changes/bug-12403-fix-post-eula-status-code +++ /dev/null @@ -1 +0,0 @@ -* Fixed a bug where an empty file uploaded to `POST /api/latest/fleet/mdm/apple/setup/eula` resulted in a 500, now returns a 400 Bad Request. diff --git a/changes/bug-2642-fix-msrc-error b/changes/bug-2642-fix-msrc-error deleted file mode 100644 index 6b0465cb4e..0000000000 --- a/changes/bug-2642-fix-msrc-error +++ /dev/null @@ -1 +0,0 @@ -- Don't use the MSRC scanner on non-windows OS. diff --git a/changes/bug-2790-return-proper-status-code b/changes/bug-2790-return-proper-status-code deleted file mode 100644 index 61c1aaa0be..0000000000 --- a/changes/bug-2790-return-proper-status-code +++ /dev/null @@ -1 +0,0 @@ -- When creating a PolicySpec, return the proper HTTP status code if the Team is not found. \ No newline at end of file diff --git a/changes/bug-2888-return-proper-status-code-if-smtp-invalid b/changes/bug-2888-return-proper-status-code-if-smtp-invalid deleted file mode 100644 index 3e13217052..0000000000 --- a/changes/bug-2888-return-proper-status-code-if-smtp-invalid +++ /dev/null @@ -1 +0,0 @@ -- Return the proper HTTP status code if SMTP is invalid. diff --git a/changes/bug-2888-validate-metadataurl b/changes/bug-2888-validate-metadataurl deleted file mode 100644 index 2800f5931b..0000000000 --- a/changes/bug-2888-validate-metadataurl +++ /dev/null @@ -1 +0,0 @@ -- When setting up SSO, validate the scheme used in the Metadata URL diff --git a/changes/bug-add-mdm-feature-flag-in-modify-appconfig b/changes/bug-add-mdm-feature-flag-in-modify-appconfig deleted file mode 100644 index b9c1f6788e..0000000000 --- a/changes/bug-add-mdm-feature-flag-in-modify-appconfig +++ /dev/null @@ -1 +0,0 @@ -* Added the `mdm_enabled` feature flag information to the response payload of the `PATCH /config` endpoint. diff --git a/changes/critical-bug-12743-observer+-run-new-query b/changes/critical-bug-12743-observer+-run-new-query new file mode 100644 index 0000000000..d27222ccbf --- /dev/null +++ b/changes/critical-bug-12743-observer+-run-new-query @@ -0,0 +1 @@ +- UI Fix: Observer + should be able to run any query by clicking create new query diff --git a/changes/issue-11861-filevault-key b/changes/issue-11861-filevault-key deleted file mode 100644 index 75adc53f85..0000000000 --- a/changes/issue-11861-filevault-key +++ /dev/null @@ -1,2 +0,0 @@ -- Fixed bug when reading filevault key in osquery and created new Fleet osquery - extension table to read the file directly rather than via filelines table. diff --git a/changes/issue-11932-improve-abm-400-error b/changes/issue-11932-improve-abm-400-error deleted file mode 100644 index 3d6fc2a0cb..0000000000 --- a/changes/issue-11932-improve-abm-400-error +++ /dev/null @@ -1 +0,0 @@ -- add improved ABM 400 error message to the UI diff --git a/changes/issue-11952-UI-for-windows-mdm-on-off b/changes/issue-11952-UI-for-windows-mdm-on-off deleted file mode 100644 index 4a6eaa7f6e..0000000000 --- a/changes/issue-11952-UI-for-windows-mdm-on-off +++ /dev/null @@ -1 +0,0 @@ -- add ability to turn windows mdm on and off from the fleet UI diff --git a/changes/issue-12053-dark-and-light-mode-logo b/changes/issue-12053-dark-and-light-mode-logo new file mode 100644 index 0000000000..90074397fa --- /dev/null +++ b/changes/issue-12053-dark-and-light-mode-logo @@ -0,0 +1 @@ +- add dark and light mode logo uploads and show the appropriate logo to the macOS mdm migration flow diff --git a/changes/issue-12129-activity-transferred-hosts b/changes/issue-12129-activity-transferred-hosts deleted file mode 100644 index 11c8197385..0000000000 --- a/changes/issue-12129-activity-transferred-hosts +++ /dev/null @@ -1 +0,0 @@ -* Added a new activity `transferred_hosts` created when hosts are transferred to a new team (or no team). diff --git a/changes/issue-12168-update-macos-mdm-setup-uo b/changes/issue-12168-update-macos-mdm-setup-uo new file mode 100644 index 0000000000..71f41c686c --- /dev/null +++ b/changes/issue-12168-update-macos-mdm-setup-uo @@ -0,0 +1 @@ +- update macos mdm setup UI in fleet UI diff --git a/changes/issue-12257-windows-mdm-feature-flag b/changes/issue-12257-windows-mdm-feature-flag deleted file mode 100644 index 4c9f3d2392..0000000000 --- a/changes/issue-12257-windows-mdm-feature-flag +++ /dev/null @@ -1 +0,0 @@ -* Added the `FLEET_DEV_MDM_ENABLED` environment variable to enable the Windows MDM feature during its development and beta period. diff --git a/changes/issue-12259-windows-mdm-settings b/changes/issue-12259-windows-mdm-settings deleted file mode 100644 index ba95180f7e..0000000000 --- a/changes/issue-12259-windows-mdm-settings +++ /dev/null @@ -1 +0,0 @@ -* Added support to enable and configure Windows MDM and to notify devices that are able to programmatically enroll. diff --git a/changes/issue-12260-trigger-windows-mdm-enrollment b/changes/issue-12260-trigger-windows-mdm-enrollment deleted file mode 100644 index 771bed72e6..0000000000 --- a/changes/issue-12260-trigger-windows-mdm-enrollment +++ /dev/null @@ -1 +0,0 @@ -* Added execution of programmatic Windows MDM enrollment on eligible devices when Windows MDM is enabled. diff --git a/changes/issue-12261-microsoft-mdm-discovery-endpoint b/changes/issue-12261-microsoft-mdm-discovery-endpoint deleted file mode 100644 index 805a2e4907..0000000000 --- a/changes/issue-12261-microsoft-mdm-discovery-endpoint +++ /dev/null @@ -1 +0,0 @@ -* Microsoft MDM Enrollment Protocol: Added support for the DiscoveryRequest messages diff --git a/changes/issue-12262-microsoft-mdm-policy-endpoint b/changes/issue-12262-microsoft-mdm-policy-endpoint deleted file mode 100644 index 690d09b700..0000000000 --- a/changes/issue-12262-microsoft-mdm-policy-endpoint +++ /dev/null @@ -1 +0,0 @@ -* Microsoft MDM Enrollment Protocol: Added support for the GetPolicies messages diff --git a/changes/issue-12263-microsoft-mdm-enroll-endpoint b/changes/issue-12263-microsoft-mdm-enroll-endpoint deleted file mode 100644 index 06b1e2cb0b..0000000000 --- a/changes/issue-12263-microsoft-mdm-enroll-endpoint +++ /dev/null @@ -1 +0,0 @@ -* Microsoft MDM Enrollment Protocol: Added support for the RequestSecurityToken messages diff --git a/changes/issue-12288-windows-mdm-activities b/changes/issue-12288-windows-mdm-activities deleted file mode 100644 index 4b82ab8528..0000000000 --- a/changes/issue-12288-windows-mdm-activities +++ /dev/null @@ -1 +0,0 @@ -* Added `enabled_windows_mdm` and `disabled_windows_mdm` activities when a user turns on/off Windows MDM. diff --git a/changes/issue-12289-add-enable-disable-windows-activtiy-UI b/changes/issue-12289-add-enable-disable-windows-activtiy-UI deleted file mode 100644 index c270d96a9d..0000000000 --- a/changes/issue-12289-add-enable-disable-windows-activtiy-UI +++ /dev/null @@ -1 +0,0 @@ -- add enable and disable windows mdm activity UI diff --git a/changes/issue-12297-ui-transferred-hosts-activity b/changes/issue-12297-ui-transferred-hosts-activity deleted file mode 100644 index cbfcf1e25f..0000000000 --- a/changes/issue-12297-ui-transferred-hosts-activity +++ /dev/null @@ -1 +0,0 @@ -- Updated UI for transferred hosts activity items. \ No newline at end of file diff --git a/changes/issue-12330-mdm-verification-failed b/changes/issue-12330-mdm-verification-failed deleted file mode 100644 index afeba36053..0000000000 --- a/changes/issue-12330-mdm-verification-failed +++ /dev/null @@ -1,2 +0,0 @@ -- Updated MDM detail query ingestion to switch MDM profiles from "verifying" or "verified" - status to "failed" status when osquery reports that this profile is not installed on the host. diff --git a/changes/issue-12342-trigger-windows-mdm-unenrollment b/changes/issue-12342-trigger-windows-mdm-unenrollment deleted file mode 100644 index 78f4d9003f..0000000000 --- a/changes/issue-12342-trigger-windows-mdm-unenrollment +++ /dev/null @@ -1 +0,0 @@ -* Added notification and execution of programmatic Windows MDM unenrollment on eligible devices when Windows MDM is disabled. diff --git a/changes/issue-12392-use-primary b/changes/issue-12392-use-primary deleted file mode 100644 index 71a5c5e51a..0000000000 --- a/changes/issue-12392-use-primary +++ /dev/null @@ -1,2 +0,0 @@ -* Fixed a bug where reading from the replica would not read recent writes when matching a set of MDM profiles to a team (the `GET /mdm/apple/profiles/match` endpoint). -* Added milliseconds to the timestamp of auto-generated team name when creating a new team in `GET /mdm/apple/profiles/match`. diff --git a/changes/issue-12473-fix-tooltip-line-breaking-on-table-cell b/changes/issue-12473-fix-tooltip-line-breaking-on-table-cell deleted file mode 100644 index 55781d4974..0000000000 --- a/changes/issue-12473-fix-tooltip-line-breaking-on-table-cell +++ /dev/null @@ -1 +0,0 @@ -- fix tooltip word wrapping on the error cell in the macOS settings table diff --git a/changes/issue-12529-mdm-counts-off b/changes/issue-12529-mdm-counts-off new file mode 100644 index 0000000000..7d8236f3a4 --- /dev/null +++ b/changes/issue-12529-mdm-counts-off @@ -0,0 +1 @@ +- Updated ingestion of host detail queries for MDM so hosts that report empty results are counted as "Off". \ No newline at end of file diff --git a/changes/issue-12568-add-org-support-url-input b/changes/issue-12568-add-org-support-url-input deleted file mode 100644 index 586b727ab1..0000000000 --- a/changes/issue-12568-add-org-support-url-input +++ /dev/null @@ -1 +0,0 @@ -- add Organization support URL input on the setting page Organization info form. diff --git a/changes/issue-12589-host-details-software-search b/changes/issue-12589-host-details-software-search deleted file mode 100644 index fdbf0e75a2..0000000000 --- a/changes/issue-12589-host-details-software-search +++ /dev/null @@ -1,2 +0,0 @@ -- Fixed UI bug on host details and device user pages that caused the software search to not work - properly when searching by CVE. diff --git a/changes/issue-12600-windows-installer b/changes/issue-12600-windows-installer new file mode 100644 index 0000000000..a5cb01d398 --- /dev/null +++ b/changes/issue-12600-windows-installer @@ -0,0 +1 @@ +* Add MSI installer deployement support through MS-MDM diff --git a/changes/issue-12604-azure-tos-endpoint b/changes/issue-12604-azure-tos-endpoint new file mode 100644 index 0000000000..11f6f7a41f --- /dev/null +++ b/changes/issue-12604-azure-tos-endpoint @@ -0,0 +1 @@ +* Adding support for MDM TOS endpoint diff --git a/changes/issue-12613-azure-jwt-support b/changes/issue-12613-azure-jwt-support new file mode 100644 index 0000000000..75c117501c --- /dev/null +++ b/changes/issue-12613-azure-jwt-support @@ -0,0 +1 @@ +* Adding support for Azure JWT tokens diff --git a/changes/issue-12614-adding-support-for-sts-auth-endpoint b/changes/issue-12614-adding-support-for-sts-auth-endpoint new file mode 100644 index 0000000000..fc8b751257 --- /dev/null +++ b/changes/issue-12614-adding-support-for-sts-auth-endpoint @@ -0,0 +1 @@ +* Adding support for Windows MDM STS Auth Endpoint diff --git a/changes/mdm-turn-on b/changes/mdm-turn-on deleted file mode 100644 index 0fae47d7a1..0000000000 --- a/changes/mdm-turn-on +++ /dev/null @@ -1 +0,0 @@ -* Make sure that all configuration profiles and commands are sent to devices if MDM is turned on, even if the device never turned off MDM. diff --git a/changes/provide-feedback-fleetctl-login-when-using-env-vars b/changes/provide-feedback-fleetctl-login-when-using-env-vars deleted file mode 100644 index bb46f22c69..0000000000 --- a/changes/provide-feedback-fleetctl-login-when-using-env-vars +++ /dev/null @@ -1 +0,0 @@ -* Provide feedback to users when `fleetctl login` is using EMAIL and PASSWORD environment variables. diff --git a/changes/sso-display-name-case b/changes/sso-display-name-case deleted file mode 100644 index 9df3502db9..0000000000 --- a/changes/sso-display-name-case +++ /dev/null @@ -1 +0,0 @@ -- Ignore casing in SAML response for display name. For example the display name attribute can be provided now as `displayname` or `displayName`. diff --git a/changes/use-custom-table-for-macos-cis-5.1.6-and-5.1.7 b/changes/use-custom-table-for-macos-cis-5.1.6-and-5.1.7 deleted file mode 100644 index 8825608f96..0000000000 --- a/changes/use-custom-table-for-macos-cis-5.1.6-and-5.1.7 +++ /dev/null @@ -1 +0,0 @@ -* For performance reasons, update macOS CIS policies 5.1.6 and 5.1.7 to use a new fleetd table `find_cmd` instead of relying on the osquery `file` table. diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index c9763a9fd8..f2bf04d399 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,4 +8,4 @@ version: v5.0.1 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.33.1 +appVersion: v4.34.0 diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index acf07184f9..988330312e 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: v4.33.1 # Version of Fleet to deploy +imageTag: v4.34.0 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index cad7412781..c2175f87d6 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -128,7 +128,7 @@ func TestApplyTeamSpecs(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { team, ok := teamsByName[name] if !ok { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } return team, nil } @@ -1344,11 +1344,7 @@ func TestApplyMacosSetup(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { team, ok := teamsByName[name] if !ok { - // TeamByName in the real Datastore does not return notFoundError, it - // returns ErrNoRows directly, we're a bit inconsistent with that at - // the moment. This is important as ApplyTeamSpecs checks if TeamByName - // returns an error that wraps ErrNoRows (and not an IsNotFound). - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } clone := *team return &clone, nil @@ -2052,7 +2048,7 @@ func TestApplySpecs(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { team, ok := teamsByName[name] if !ok { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } return team, nil } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 2d61d42b26..e6ae712e28 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -1,114 +1,115 @@ { - "kind": "config", - "apiVersion": "v1", - "spec": { - "org_info": { - "org_name": "", - "org_logo_url": "", - "contact_url": "https://fleetdm.com/company/contact" - }, - "server_settings": { - "server_url": "", - "live_query_disabled": false, - "enable_analytics": false, - "deferred_save_host": false - }, - "smtp_settings": { - "enable_smtp": false, - "configured": false, - "sender_address": "", - "server": "", - "port": 0, - "authentication_type": "", - "user_name": "", - "password": "", - "enable_ssl_tls": false, - "authentication_method": "", - "domain": "", - "verify_ssl_certs": false, - "enable_start_tls": false - }, - "host_expiry_settings": { - "host_expiry_enabled": false, - "host_expiry_window": 0 - }, - "features": { - "enable_host_users": true, - "enable_software_inventory": false - }, - "sso_settings": { - "entity_id": "", - "issuer_uri": "", - "idp_image_url": "", - "metadata": "", - "metadata_url": "", - "idp_name": "", - "enable_jit_provisioning": false, - "enable_jit_role_sync": false, - "enable_sso": false, - "enable_sso_idp_login": false - }, - "fleet_desktop": { - "transparency_url": "https://fleetdm.com/transparency" - }, - "vulnerability_settings": { - "databases_path": "/some/path" - }, - "webhook_settings": { - "host_status_webhook": { - "enable_host_status_webhook": false, - "destination_url": "", - "host_percentage": 0, - "days_count": 0 - }, - "failing_policies_webhook": { - "enable_failing_policies_webhook": false, - "destination_url": "", - "policy_ids": null, - "host_batch_size": 0 - }, - "vulnerabilities_webhook": { - "enable_vulnerabilities_webhook": false, - "destination_url": "", - "host_batch_size": 0 - }, - "interval": "0s" - }, - "integrations": { - "jira": null, - "zendesk": null - }, - "mdm": { - "apple_bm_terms_expired": false, - "apple_bm_enabled_and_configured": false, - "enabled_and_configured": false, - "apple_bm_default_team": "", - "windows_enabled_and_configured": false, - "macos_updates": { - "minimum_version": null, - "deadline": null - }, - "macos_migration": { - "enable": false, - "mode": "", - "webhook_url": "" - }, - "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false - }, - "macos_setup": { - "bootstrap_package": null, - "enable_end_user_authentication": false, - "macos_setup_assistant": null - }, - "end_user_authentication": { - "entity_id": "", - "issuer_uri": "", - "metadata": "", - "metadata_url": "", - "idp_name": "" - } - } - } + "kind": "config", + "apiVersion": "v1", + "spec": { + "org_info": { + "org_name": "", + "org_logo_url": "", + "org_logo_url_light_background": "", + "contact_url": "https://fleetdm.com/company/contact" + }, + "server_settings": { + "server_url": "", + "live_query_disabled": false, + "enable_analytics": false, + "deferred_save_host": false + }, + "smtp_settings": { + "enable_smtp": false, + "configured": false, + "sender_address": "", + "server": "", + "port": 0, + "authentication_type": "", + "user_name": "", + "password": "", + "enable_ssl_tls": false, + "authentication_method": "", + "domain": "", + "verify_ssl_certs": false, + "enable_start_tls": false + }, + "host_expiry_settings": { + "host_expiry_enabled": false, + "host_expiry_window": 0 + }, + "features": { + "enable_host_users": true, + "enable_software_inventory": false + }, + "sso_settings": { + "entity_id": "", + "issuer_uri": "", + "idp_image_url": "", + "metadata": "", + "metadata_url": "", + "idp_name": "", + "enable_jit_provisioning": false, + "enable_jit_role_sync": false, + "enable_sso": false, + "enable_sso_idp_login": false + }, + "fleet_desktop": { + "transparency_url": "https://fleetdm.com/transparency" + }, + "vulnerability_settings": { + "databases_path": "/some/path" + }, + "webhook_settings": { + "host_status_webhook": { + "enable_host_status_webhook": false, + "destination_url": "", + "host_percentage": 0, + "days_count": 0 + }, + "failing_policies_webhook": { + "enable_failing_policies_webhook": false, + "destination_url": "", + "policy_ids": null, + "host_batch_size": 0 + }, + "vulnerabilities_webhook": { + "enable_vulnerabilities_webhook": false, + "destination_url": "", + "host_batch_size": 0 + }, + "interval": "0s" + }, + "integrations": { + "jira": null, + "zendesk": null + }, + "mdm": { + "apple_bm_terms_expired": false, + "apple_bm_enabled_and_configured": false, + "enabled_and_configured": false, + "apple_bm_default_team": "", + "windows_enabled_and_configured": false, + "macos_updates": { + "minimum_version": null, + "deadline": null + }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, + "macos_settings": { + "custom_settings": null, + "enable_disk_encryption": false + }, + "macos_setup": { + "bootstrap_package": null, + "enable_end_user_authentication": false, + "macos_setup_assistant": null + }, + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "metadata": "", + "metadata_url": "", + "idp_name": "" + } + } + } } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 8db7811ede..1c0d778685 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -41,6 +41,7 @@ spec: entity_id: "" org_info: org_logo_url: "" + org_logo_url_light_background: "" org_name: "" contact_url: https://fleetdm.com/company/contact server_settings: diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 6ff8616594..2030db5afe 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -1,176 +1,177 @@ { - "kind": "config", - "apiVersion": "v1", - "spec": { - "org_info": { - "org_name": "", - "org_logo_url": "", - "contact_url": "https://fleetdm.com/company/contact" - }, - "server_settings": { - "server_url": "", - "live_query_disabled": false, - "enable_analytics": false, - "deferred_save_host": false - }, - "smtp_settings": { - "enable_smtp": false, - "configured": false, - "sender_address": "", - "server": "", - "port": 0, - "authentication_type": "", - "user_name": "", - "password": "", - "enable_ssl_tls": false, - "authentication_method": "", - "domain": "", - "verify_ssl_certs": false, - "enable_start_tls": false - }, - "host_expiry_settings": { - "host_expiry_enabled": false, - "host_expiry_window": 0 - }, - "features": { - "enable_host_users": true, - "enable_software_inventory": false - }, - "mdm": { - "apple_bm_default_team": "", - "apple_bm_terms_expired": false, - "apple_bm_enabled_and_configured": false, - "enabled_and_configured": false, - "windows_enabled_and_configured": false, - "macos_updates": { - "minimum_version": null, - "deadline": null - }, - "macos_migration": { - "enable": false, - "mode": "", - "webhook_url": "" - }, - "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false - }, - "macos_setup": { - "bootstrap_package": null, - "enable_end_user_authentication": false, - "macos_setup_assistant": null - }, - "end_user_authentication": { - "entity_id": "", - "issuer_uri": "", - "metadata": "", - "metadata_url": "", - "idp_name": "" - } - }, - "sso_settings": { - "enable_jit_provisioning": false, - "enable_jit_role_sync": false, - "entity_id": "", - "issuer_uri": "", - "idp_image_url": "", - "metadata": "", - "metadata_url": "", - "idp_name": "", - "enable_sso": false, - "enable_sso_idp_login": false - }, - "fleet_desktop": { - "transparency_url": "https://fleetdm.com/transparency" - }, - "vulnerability_settings": { - "databases_path": "/some/path" - }, - "webhook_settings": { - "host_status_webhook": { - "enable_host_status_webhook": false, - "destination_url": "", - "host_percentage": 0, - "days_count": 0 - }, - "failing_policies_webhook": { - "enable_failing_policies_webhook": false, - "destination_url": "", - "policy_ids": null, - "host_batch_size": 0 - }, - "vulnerabilities_webhook": { - "enable_vulnerabilities_webhook": false, - "destination_url": "", - "host_batch_size": 0 - }, - "interval": "0s" - }, - "integrations": { - "jira": null, - "zendesk": null - }, - "update_interval": { - "osquery_detail": "1h0m0s", - "osquery_policy": "1h0m0s" - }, - "vulnerabilities": { - "databases_path": "", - "periodicity": "0s", - "cpe_database_url": "", - "cpe_translations_url": "", - "cve_feed_prefix_url": "", - "current_instance_checks": "", - "disable_data_sync": false, - "recent_vulnerability_max_age": "0s", - "disable_win_os_vulnerabilities": false - }, - "license": { - "tier": "free", - "expiration": "0001-01-01T00:00:00Z" - }, - "logging": { - "debug": true, - "json": false, - "result": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - }, - "status": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - }, - "audit": { - "plugin": "filesystem", - "config": { - "enable_log_compression": false, - "enable_log_rotation": false, - "result_log_file": "/dev/null", - "status_log_file": "/dev/null", - "audit_log_file": "/dev/null", - "max_size": 500, - "max_age": 0, - "max_backups": 0 - } - } - } - } + "kind": "config", + "apiVersion": "v1", + "spec": { + "org_info": { + "org_name": "", + "org_logo_url": "", + "org_logo_url_light_background": "", + "contact_url": "https://fleetdm.com/company/contact" + }, + "server_settings": { + "server_url": "", + "live_query_disabled": false, + "enable_analytics": false, + "deferred_save_host": false + }, + "smtp_settings": { + "enable_smtp": false, + "configured": false, + "sender_address": "", + "server": "", + "port": 0, + "authentication_type": "", + "user_name": "", + "password": "", + "enable_ssl_tls": false, + "authentication_method": "", + "domain": "", + "verify_ssl_certs": false, + "enable_start_tls": false + }, + "host_expiry_settings": { + "host_expiry_enabled": false, + "host_expiry_window": 0 + }, + "features": { + "enable_host_users": true, + "enable_software_inventory": false + }, + "mdm": { + "apple_bm_default_team": "", + "apple_bm_terms_expired": false, + "apple_bm_enabled_and_configured": false, + "enabled_and_configured": false, + "windows_enabled_and_configured": false, + "macos_updates": { + "minimum_version": null, + "deadline": null + }, + "macos_migration": { + "enable": false, + "mode": "", + "webhook_url": "" + }, + "macos_settings": { + "custom_settings": null, + "enable_disk_encryption": false + }, + "macos_setup": { + "bootstrap_package": null, + "enable_end_user_authentication": false, + "macos_setup_assistant": null + }, + "end_user_authentication": { + "entity_id": "", + "issuer_uri": "", + "metadata": "", + "metadata_url": "", + "idp_name": "" + } + }, + "sso_settings": { + "enable_jit_provisioning": false, + "enable_jit_role_sync": false, + "entity_id": "", + "issuer_uri": "", + "idp_image_url": "", + "metadata": "", + "metadata_url": "", + "idp_name": "", + "enable_sso": false, + "enable_sso_idp_login": false + }, + "fleet_desktop": { + "transparency_url": "https://fleetdm.com/transparency" + }, + "vulnerability_settings": { + "databases_path": "/some/path" + }, + "webhook_settings": { + "host_status_webhook": { + "enable_host_status_webhook": false, + "destination_url": "", + "host_percentage": 0, + "days_count": 0 + }, + "failing_policies_webhook": { + "enable_failing_policies_webhook": false, + "destination_url": "", + "policy_ids": null, + "host_batch_size": 0 + }, + "vulnerabilities_webhook": { + "enable_vulnerabilities_webhook": false, + "destination_url": "", + "host_batch_size": 0 + }, + "interval": "0s" + }, + "integrations": { + "jira": null, + "zendesk": null + }, + "update_interval": { + "osquery_detail": "1h0m0s", + "osquery_policy": "1h0m0s" + }, + "vulnerabilities": { + "databases_path": "", + "periodicity": "0s", + "cpe_database_url": "", + "cpe_translations_url": "", + "cve_feed_prefix_url": "", + "current_instance_checks": "", + "disable_data_sync": false, + "recent_vulnerability_max_age": "0s", + "disable_win_os_vulnerabilities": false + }, + "license": { + "tier": "free", + "expiration": "0001-01-01T00:00:00Z" + }, + "logging": { + "debug": true, + "json": false, + "result": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + }, + "status": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + }, + "audit": { + "plugin": "filesystem", + "config": { + "enable_log_compression": false, + "enable_log_rotation": false, + "result_log_file": "/dev/null", + "status_log_file": "/dev/null", + "audit_log_file": "/dev/null", + "max_size": 500, + "max_age": 0, + "max_backups": 0 + } + } + } + } } diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 1ca389bdb7..9d3bf00ace 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -80,6 +80,7 @@ spec: plugin: filesystem org_info: org_logo_url: "" + org_logo_url_light_background: "" org_name: "" contact_url: https://fleetdm.com/company/contact server_settings: diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 0fc824aad9..4fc311a8dd 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -41,6 +41,7 @@ spec: entity_id: "" org_info: org_logo_url: "" + org_logo_url_light_background: "" org_name: "Fleet" contact_url: "https://fleetdm.com/company/contact" server_settings: diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index f4a36b668c..72b5d2c599 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -41,6 +41,7 @@ spec: entity_id: "" org_info: org_logo_url: "" + org_logo_url_light_background: "" org_name: Fleet contact_url: https://fleetdm.com/company/contact server_settings: diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 529c9c7412..32504c8680 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -9,14 +9,10 @@ import ( "errors" "flag" "fmt" - "io" "log" "math/rand" "net/http" "os" - "path" - "path/filepath" - "runtime" "strconv" "strings" "sync" @@ -32,33 +28,77 @@ import ( "github.com/valyala/fasthttp" ) -//go:embed *.tmpl -var templatesFS embed.FS +var ( + //go:embed *.tmpl + templatesFS embed.FS -//go:embed *.software -var softwareFS embed.FS + //go:embed *.software + macOSVulnerableSoftwareFS embed.FS -var vulnerableSoftware []fleet.Software + //go:embed ubuntu_2204-software.json.bz2 + ubuntuSoftwareFS embed.FS + //go:embed windows_11-software.json.bz2 + windowsSoftwareFS embed.FS -func init() { - vulnerableSoftwareData, err := softwareFS.ReadFile("vulnerable.software") + macosVulnerableSoftware []fleet.Software + windowsSoftware []map[string]string + ubuntuSoftware []map[string]string +) + +func loadMacOSVulnerableSoftware() { + macOSVulnerableSoftwareData, err := macOSVulnerableSoftwareFS.ReadFile("macos_vulnerable.software") if err != nil { - log.Fatal("reading vulnerable software file: ", err) + log.Fatal("reading vulnerable macOS software file: ", err) } - lines := bytes.Split(vulnerableSoftwareData, []byte("\n")) + lines := bytes.Split(macOSVulnerableSoftwareData, []byte("\n")) for _, line := range lines { parts := bytes.Split(line, []byte("##")) if len(parts) < 2 { fmt.Println("skipping", string(line)) continue } - vulnerableSoftware = append(vulnerableSoftware, fleet.Software{ + macosVulnerableSoftware = append(macosVulnerableSoftware, fleet.Software{ Name: strings.TrimSpace(string(parts[0])), Version: strings.TrimSpace(string(parts[1])), Source: "apps", }) } - log.Printf("Loaded %d vulnerable software\n", len(vulnerableSoftware)) + log.Printf("Loaded %d vulnerable macOS software\n", len(macosVulnerableSoftware)) +} + +func loadSoftwareItems(fs embed.FS, path string) []map[string]string { + bz2, err := fs.Open(path) + if err != nil { + panic(err) + } + + type softwareJSON struct { + Name string `json:"name"` + Version string `json:"version"` + Release string `json:"release,omitempty"` + Arch string `json:"arch,omitempty"` + } + var softwareList []softwareJSON + // ignoring "G110: Potential DoS vulnerability via decompression bomb", as this is test code. + if err := json.NewDecoder(bzip2.NewReader(bz2)).Decode(&softwareList); err != nil { //nolint:gosec + panic(err) + } + + softwareRows := make([]map[string]string, 0, len(softwareList)) + for _, s := range softwareList { + softwareRows = append(softwareRows, map[string]string{ + "name": s.Name, + "version": s.Version, + "source": "programs", + }) + } + return softwareRows +} + +func init() { + loadMacOSVulnerableSoftware() + windowsSoftware = loadSoftwareItems(windowsSoftwareFS, "windows_11-software.json.bz2") + ubuntuSoftware = loadSoftwareItems(ubuntuSoftwareFS, "ubuntu_2204-software.json.bz2") } type Stats struct { @@ -748,99 +788,6 @@ func (a *agent) hostUsers() []map[string]string { return users } -func extract(src, dst string) { - srcF, err := os.Open(src) - if err != nil { - panic(err) - } - defer srcF.Close() - - dstF, err := os.Create(dst) - if err != nil { - panic(err) - } - defer dstF.Close() - - r := bzip2.NewReader(srcF) - // ignoring "G110: Potential DoS vulnerability via decompression bomb", as this is test code. - _, err = io.Copy(dstF, r) //nolint:gosec - if err != nil { - panic(err) - } -} - -func loadSoftware(platform string, ver string) []map[string]string { - _, exFilename, _, ok := runtime.Caller(0) - if !ok { - panic("No caller information") - } - exDir := path.Dir(exFilename) - - srcPath := filepath.Join( - exDir, - "..", - "..", - "server", - "vulnerabilities", - "testdata", - platform, - "software", - fmt.Sprintf("%s_%s-software.json.bz2", platform, ver), - ) - - tmpDir, err := os.MkdirTemp("", "osquery-perf") - if err != nil { - panic(err) - } - defer os.RemoveAll(tmpDir) - dstPath := filepath.Join(tmpDir, fmt.Sprintf("%s-software.json", ver)) - - extract(srcPath, dstPath) - - type softwareJSON struct { - Name string `json:"name"` - Version string `json:"version"` - Release string `json:"release,omitempty"` - Arch string `json:"arch,omitempty"` - } - - var software []softwareJSON - contents, err := os.ReadFile(dstPath) - if err != nil { - log.Printf("reading vuln software for %s %s: %s\n", platform, ver, err) - return nil - } - - err = json.Unmarshal(contents, &software) - if err != nil { - log.Printf("unmarshalling vuln software for %s %s:%s", platform, ver, err) - return nil - } - - var r []map[string]string - for i, fi := range software { - installedPath := "" - if i%2 == 0 { - installedPath = fmt.Sprintf("/some/path/%s", fi.Name) - } - r = append(r, map[string]string{ - "name": fi.Name, - "version": fi.Version, - "source": "osquery-perf", - "installed_path": installedPath, - }) - } - return r -} - -func (a *agent) softwareWindows11() []map[string]string { - return loadSoftware("windows", "11") -} - -func (a *agent) softwareUbuntu2204() []map[string]string { - return loadSoftware("ubuntu", "2204") -} - func (a *agent) softwareMacOS() []map[string]string { var lastOpenedCount int commonSoftware := make([]map[string]string, a.softwareCount.common) @@ -887,7 +834,7 @@ func (a *agent) softwareMacOS() []map[string]string { } randomVulnerableSoftware := make([]map[string]string, a.softwareCount.vulnerable) for i := 0; i < len(randomVulnerableSoftware); i++ { - sw := vulnerableSoftware[rand.Intn(len(vulnerableSoftware))] + sw := macosVulnerableSoftware[rand.Intn(len(macosVulnerableSoftware))] var lastOpenedAt string if l := a.genLastOpenedAt(&lastOpenedCount); l != nil { lastOpenedAt = l.Format(time.UnixDate) @@ -1245,7 +1192,7 @@ func (a *agent) processQuery(name, query string) (handled bool, results []map[st case name == hostDetailQueryPrefix+"software_windows": ss := fleet.OsqueryStatus(rand.Intn(2)) if ss == fleet.StatusOK { - results = a.softwareWindows11() + results = windowsSoftware } return true, results, &ss, nil case name == hostDetailQueryPrefix+"software_linux": @@ -1253,7 +1200,7 @@ func (a *agent) processQuery(name, query string) (handled bool, results []map[st if ss == fleet.StatusOK { switch a.os { case "ubuntu_22.04": - results = a.softwareUbuntu2204() + results = ubuntuSoftware } } return true, results, &ss, nil diff --git a/cmd/osquery-perf/vulnerable.software b/cmd/osquery-perf/macos_vulnerable.software similarity index 100% rename from cmd/osquery-perf/vulnerable.software rename to cmd/osquery-perf/macos_vulnerable.software diff --git a/cmd/osquery-perf/ubuntu_2204-software.json.bz2 b/cmd/osquery-perf/ubuntu_2204-software.json.bz2 new file mode 100644 index 0000000000..ea9cc3399d Binary files /dev/null and b/cmd/osquery-perf/ubuntu_2204-software.json.bz2 differ diff --git a/cmd/osquery-perf/windows_11-software.json.bz2 b/cmd/osquery-perf/windows_11-software.json.bz2 new file mode 100644 index 0000000000..589e2e500f Binary files /dev/null and b/cmd/osquery-perf/windows_11-software.json.bz2 differ diff --git a/docs/Contributing/API-Versioning.md b/docs/Contributing/API-Versioning.md index 32aa06626d..8dc8deeb1a 100644 --- a/docs/Contributing/API-Versioning.md +++ b/docs/Contributing/API-Versioning.md @@ -107,3 +107,4 @@ This will mean that the following are the only valid paths after this point: And the code doesn't have to specify `.StartingAtVersion("2021-12")` anymore. + diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index e95197f32a..881c7f8496 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -671,12 +671,13 @@ This endpoint stores a profile to be assigned to a host at some point in the fut #### Parameters -| Name | Type | In | Description | -| ------------ | ------ | ---- | ----------------------------------------------------------- | -| external_host_identifier | string | body | **Required**. The identifier of the host as generated by the external service (e.g. Puppet). | -| host_uuid | string | body | **Required**. The UUID of the host. | -| profile | string | body | **Required**. The base64-encoded .mobileconfig content of the MDM profile. | -| group | string | body | The group label associated with that profile. This information is used to generate team names if they need to be created. | +| Name | Type | In | Description | +| ------------ | ------- | ---- | ----------------------------------------------------------- | +| external_host_identifier | string | body | **Required**. The identifier of the host as generated by the external service (e.g. Puppet). | +| host_uuid | string | body | **Required**. The UUID of the host. | +| profile | string | body | **Required**. The base64-encoded .mobileconfig content of the MDM profile. | +| group | string | body | The group label associated with that profile. This information is used to generate team names if they need to be created. | +| exclude | boolean | body | Whether to skip delivering the profile to this host. | #### Example @@ -689,7 +690,8 @@ This endpoint stores a profile to be assigned to a host at some point in the fut "external_host_identifier": "id-01234", "host_uuid": "c0532a64-bec2-4cf9-aa37-96fe47ead814", "profile": "", - "group": "Workstations" + "group": "Workstations", + "exclude": false } ``` @@ -2222,6 +2224,7 @@ Device-authenticated routes are routes used by the Fleet Desktop application. Un - [Get device's transparency URL](#get-devices-transparency-url) - [Download device's MDM manual enrollment profile](#download-devices-mdm-manual-enrollment-profile) - [Migrate device to Fleet from another MDM solution](#migrate-device-to-fleet-from-another-mdm-solution) +- [Trigger FileVault key escrow](#trigger-filevault-key-escrow) #### Get device's host @@ -2656,6 +2659,28 @@ Signals the Fleet server to send a webbook request with the device UUID and seri --- +#### Trigger FileVault key escrow + +Sends a signal to Fleet Desktop to initiate a FileVault key escrow. This is useful for setting the escrow key initially as well as in scenarios where a token rotation is required. **Requires Fleet Premium license** + +`POST /api/v1/fleet/device/{token}/rotate_encryption_key` + +##### Parameters + +| Name | Type | In | Description | +| ----- | ------ | ---- | ---------------------------------- | +| token | string | path | The device's authentication token. | + +##### Example + +`POST /api/v1/fleet/device/abcdef012456789/rotate_encryption_key` + +##### Default response + +`Status: 204` + +--- + ## Downloadable installers @@ -2790,3 +2815,4 @@ If the Fleet instance is provided required parameters to complete setup. ``` + diff --git a/docs/Contributing/Adding-new-endpoints.md b/docs/Contributing/Adding-new-endpoints.md index 9fe56f6ba0..4a86d26ee6 100644 --- a/docs/Contributing/Adding-new-endpoints.md +++ b/docs/Contributing/Adding-new-endpoints.md @@ -269,3 +269,4 @@ The logic here is that if there are any parameters in the Request struct that ha expected, and the absence of it results in an error. + diff --git a/docs/Contributing/Automatically-generating-UI-component-boilerplate.md b/docs/Contributing/Automatically-generating-UI-component-boilerplate.md index 80ad32d3b8..9cef3a93a2 100644 --- a/docs/Contributing/Automatically-generating-UI-component-boilerplate.md +++ b/docs/Contributing/Automatically-generating-UI-component-boilerplate.md @@ -16,3 +16,4 @@ You can also run `./generate -h` for information about the other options availab specifying destination. + diff --git a/docs/Contributing/Building-Fleet.md b/docs/Contributing/Building-Fleet.md index bc1caa19c4..5f6348fcde 100644 --- a/docs/Contributing/Building-Fleet.md +++ b/docs/Contributing/Building-Fleet.md @@ -229,3 +229,4 @@ dlv debug --build-flags '-tags=full' --headless \ ``` + diff --git a/docs/Contributing/Committing-Changes.md b/docs/Contributing/Committing-Changes.md index bc02bc32c4..e34c4e75e1 100644 --- a/docs/Contributing/Committing-Changes.md +++ b/docs/Contributing/Committing-Changes.md @@ -102,3 +102,4 @@ Keep in mind that the commit title and description are what developers see when Keeping to around 80 character line lengths helps with rendering when folks have narrow, tiled terminal windows. + diff --git a/docs/Contributing/Configuration-for-contributors.md b/docs/Contributing/Configuration-for-contributors.md index ee768c96ed..0c978f82c5 100644 --- a/docs/Contributing/Configuration-for-contributors.md +++ b/docs/Contributing/Configuration-for-contributors.md @@ -362,3 +362,4 @@ Whether the SMTP server's SSL certificates should be verified. This can be turne ``` + diff --git a/docs/Contributing/Deploying-chrome-test-ext.md b/docs/Contributing/Deploying-chrome-test-ext.md new file mode 100644 index 0000000000..7a30649fb1 --- /dev/null +++ b/docs/Contributing/Deploying-chrome-test-ext.md @@ -0,0 +1,68 @@ +# Deploying ChromeOS test extensions to enrolled Chromebooks + +As part of validating any ChromeOS extension, run this process to force-install the extension on Chromebooks for debugging. + +## Build the extension + +### Bump the extension version + +Modify the version field at the top of the [`package.json`](https://github.com/fleetdm/fleet/blob/main/ee/fleetd-chrome/package.json) file in `ee/fleetd-chrome` + +Update the version in [`updates.xml`](https://github.com/fleetdm/fleet/blob/main/ee/fleetd-chrome/updates.xml) to match the `package.json` version. + +### Build the distribution folder + +``` +cd ee/fleetd-chrome +yarn run build +``` + +### Pack the extension + +Navigate to chrome://extensions in your Chrome web browser. +- In developer mode, select "Pack extension" +- Set "Extension root directory" to the newly-created `ee/fleetd-chrome/dist` folder +- Press "Pack extension" (key name will auto-generate) + +### Load the new extension to the Chrome web browser + +- Open the finder app +- Drag and drop the `ee/fleetd-chrome/dist.crx` binary file on top of a Chrome web browser window +- Press "Add Extension" +- Verify that the extension works +- **Copy the `appid` for later use** + +## Run a local server to make the new extension available + +### Edit update.xml +Open `ee/fleetd-chrome/update.xml` in your text editor and modify: +- The version. +- The `appid` (copied previously). This will only be done for debug versions. For production, we will keep the original ID we have. + +### Create the server + +``` +cd ee/fleetd-chrome +python3 -m http.server +``` +- Verify that it works by going to http://localhost:8000 to see the files. + +``` +cd ee/fleetd-chrome +npm install -g localtunnel +lt --port 8000 --subdomain test-new-tables +``` +- In your web browser go to: http://test-new-tables.loca.lt +- Click the hazard link on item number 1 (below the big button "Click To Submit"). From the new page, copy the IP and paste it into the previous page in the window. +- Open `ee/fleetd-chrome/update.xml` in your text editor and modify the codebase to use the newly created URL (in this example: http://test-new-tables.loca.lt/dist.crx). + +### Deploy the extension using Google Admin + +> Follow the instructions [here](https://fleetdm.com/docs/using-fleet/adding-hosts#add-chromebooks-with-the-fleetd-chrome-extension) for installing the fleetd Chrome extension, with the following modifications: +> + Select the "ChromeOSTesting" group. +> + For "Extension ID", use the ID previously copied. +> + For "Installation URL", use `http://test-new-tables.loca.lt/updates.xml`. +> + Remove the filters (the filters with our `appid`). +> + For "Policy for extensions", copy over the JSON from the original extension. + + diff --git a/docs/Contributing/FAQ.md b/docs/Contributing/FAQ.md index 59832d0346..2731f819b0 100644 --- a/docs/Contributing/FAQ.md +++ b/docs/Contributing/FAQ.md @@ -92,3 +92,5 @@ If you also have Orbit running on hosts, it will need access to these API endpoi * `/api/fleet/orbit/device_token` * `/api/fleet/orbit/ping` * `/api/osquery/log` + + \ No newline at end of file diff --git a/docs/Contributing/Fleet-UI-Testing.md b/docs/Contributing/Fleet-UI-Testing.md index 6be3293d15..5a52b53370 100644 --- a/docs/Contributing/Fleet-UI-Testing.md +++ b/docs/Contributing/Fleet-UI-Testing.md @@ -328,3 +328,4 @@ in that we believe tests should resemble real-world usage as closely as possible // TODO + diff --git a/docs/Contributing/Migrations.md b/docs/Contributing/Migrations.md index 139188f73d..deacec368c 100644 --- a/docs/Contributing/Migrations.md +++ b/docs/Contributing/Migrations.md @@ -46,3 +46,4 @@ Move the migration file from [server/datastore/mysql/migrations/tables/](https:/ Proceed as for table migrations, editing and running the newly created migration file. + diff --git a/docs/Contributing/Orbit-development-and-release-strategy.md b/docs/Contributing/Orbit-development-and-release-strategy.md index 544615da73..6fa407bd3d 100644 --- a/docs/Contributing/Orbit-development-and-release-strategy.md +++ b/docs/Contributing/Orbit-development-and-release-strategy.md @@ -29,4 +29,5 @@ This allows some flexibility when developing new features in Orbit and Fleet. 1. Orbit components (Orbit itself, Fleet Desktop and osqueryd) must be released to FleetDM's TUF before new Fleet server releases are available in Github. 2. When the new Fleet server version doesn't support older Orbit versions (see [Nice to have](#nice-to-have)), the release notes must document their minimum supported Orbit version. This is for users that use Orbit with auto-updates disabled or they pin to a specific channel. These users would need to first update Orbit in their devices and then proceed to upgrade Fleet server. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Contributing/README.md b/docs/Contributing/README.md index 4ce97c2171..ec4c669436 100644 --- a/docs/Contributing/README.md +++ b/docs/Contributing/README.md @@ -24,5 +24,8 @@ Learn how to add fake data to your development instance. ### [API for contributors](./API-for-contributors.md) Get to grips with Fleet API routes. This documentation is helpful for developing or contributing to Fleet. +### [Deploying ChromeOS test extensions](./Deploying-chrome-test-ext.md) +Learn how to deploy a test version of the fleetd Chrome extension for debug purposes. + ### [FAQ](./FAQ.md) Find commonly asked questions and answers about contributing to Fleet as part of our community. diff --git a/docs/Contributing/Releasing-Fleet.md b/docs/Contributing/Releasing-Fleet.md index 44558a41c6..826598d00e 100644 --- a/docs/Contributing/Releasing-Fleet.md +++ b/docs/Contributing/Releasing-Fleet.md @@ -145,3 +145,4 @@ A patch release is required when a critical bug is found. Critical bugs are defi TODO [#2850](https://github.com/fleetdm/fleet/issues/2850): Improve docs/tooling for this. + diff --git a/docs/Contributing/Run-Locally-Built-Orbit.md b/docs/Contributing/Run-Locally-Built-Orbit.md index bdb7264d4f..5f4695ee41 100644 --- a/docs/Contributing/Run-Locally-Built-Orbit.md +++ b/docs/Contributing/Run-Locally-Built-Orbit.md @@ -60,3 +60,4 @@ Double-Click this pkg file and install the local Orbit. + diff --git a/docs/Contributing/Seeding-Data.md b/docs/Contributing/Seeding-Data.md index fd90868917..6d744cb068 100644 --- a/docs/Contributing/Seeding-Data.md +++ b/docs/Contributing/Seeding-Data.md @@ -68,7 +68,6 @@ The `fleet/create_figma` script will generate an environment to reflect the mock Each user generated by the script has its password set to `password123#`. - ## Related actions @@ -88,4 +87,7 @@ Fleet supports [SSO users](https://fleetdm.com/docs/deploying/configuration#conf ### Create test hosts -To create a handful of test hosts, you can run containerized `osqueryd` [Docker test hosts](https://github.com/fleetdm/fleet/tree/main/tools/osquery). As these Docker test hosts are RAM intensive, alternatively, you can use `osquery-perf` to create thousands of [simulated test hosts](https://github.com/fleetdm/fleet/tree/main/cmd/osquery-perf). \ No newline at end of file +To create a handful of test hosts, you can run containerized `osqueryd` [Docker test hosts](https://github.com/fleetdm/fleet/tree/main/tools/osquery). As these Docker test hosts are RAM intensive, alternatively, you can use `osquery-perf` to create thousands of [simulated test hosts](https://github.com/fleetdm/fleet/tree/main/cmd/osquery-perf). + + + diff --git a/docs/Contributing/Simulate-slow-network.md b/docs/Contributing/Simulate-slow-network.md index bb912c00aa..3b342a0f3a 100644 --- a/docs/Contributing/Simulate-slow-network.md +++ b/docs/Contributing/Simulate-slow-network.md @@ -43,4 +43,5 @@ curl -s -XPOST -d '{"type" : "latency", "attributes" : {"latency" : 1000, "jitte {"attributes":{"latency":5000,"jitter":0},"name":"latency_downstream","type":"latency","stream":"downstream","toxicity":1}% ``` - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Contributing/Testing-and-local-development.md b/docs/Contributing/Testing-and-local-development.md index 50b2e34c0b..9b5489e381 100644 --- a/docs/Contributing/Testing-and-local-development.md +++ b/docs/Contributing/Testing-and-local-development.md @@ -673,3 +673,4 @@ The `pkg` file needs to be a signed "distribution package", you can find a dummy The dummy package linked above adds a Fleet logo in `/Library/FleetDM/fleet-logo.png`. To verify if the package was installed, you can open that folder and verify that the logo is there. + diff --git a/docs/Deploying/Configuration.md b/docs/Deploying/Configuration.md index 2dcc7f4c51..5e31b047f2 100644 --- a/docs/Deploying/Configuration.md +++ b/docs/Deploying/Configuration.md @@ -2517,7 +2517,6 @@ If set, then `Fleet serve` will capture errors and panics and push them to Sentr dsn: "https://somedsnprovidedby.sentry.com/" ``` - #### Prometheus @@ -3221,3 +3220,7 @@ The HTTP request headers are checked in the following order: 4. If none of the above headers are present in the HTTP request then Fleet will attempt to use the remote address of the TCP connection (note that on deployments with ingress proxies the remote address seen by Fleet is the IP of the ingress proxy). If the IP retrieved using the above heuristic belongs to a private range, then Fleet will ignore it and will not set the "Public IP address" field for the device. + + + + \ No newline at end of file diff --git a/docs/Deploying/Debugging.md b/docs/Deploying/Debugging.md index 178e855e16..be5a2b0afa 100644 --- a/docs/Deploying/Debugging.md +++ b/docs/Deploying/Debugging.md @@ -173,3 +173,4 @@ Make sure as well that your cloud provider is not having issues of their own. Fo [AWS](https://health.aws.amazon.com/health/status) for status. + diff --git a/docs/Deploying/FAQ.md b/docs/Deploying/FAQ.md index 87b1e29f44..b79c0f3797 100644 --- a/docs/Deploying/FAQ.md +++ b/docs/Deploying/FAQ.md @@ -218,3 +218,6 @@ Fleet is tested with Redis 5.0.14 and 6.2.7. Any version Redis after version 5 w ## Will my older version of Fleet work with Redis 6? Most likely, yes! While we'd definitely recommend keeping Fleet up to date in order to take advantage of new features and bug patches, most legacy versions should work with Redis 6. Just keep in mind that we likely haven't tested your particular combination so that you may run into some unforeseen hiccups. + + + + diff --git a/docs/Deploying/Load-testing.md b/docs/Deploying/Load-testing.md index 3f1e7e49e9..4aff6e487d 100644 --- a/docs/Deploying/Load-testing.md +++ b/docs/Deploying/Load-testing.md @@ -74,3 +74,4 @@ They are sized to be the smallest that Fargate allows, so it is still cost effec The [osquery-perf](https://github.com/fleetdm/fleet/tree/main/cmd/osquery-perf) tool doesn't simulate all data that's included when a real device communicates to a Fleet instance. For example, system users and software inventory data are not yet simulated by osquery-perf. + diff --git a/docs/Deploying/README.md b/docs/Deploying/README.md index 69ebf55aa5..40c63cac04 100644 --- a/docs/Deploying/README.md +++ b/docs/Deploying/README.md @@ -23,3 +23,5 @@ Information to gather as part of debugging an issue with a deployment. ### [FAQ](./FAQ.md) Includes commonly asked questions and answers about deployment from the Fleet community. + + diff --git a/docs/Deploying/Reference-Architectures.md b/docs/Deploying/Reference-Architectures.md index 398ee0887a..1c4613cb75 100644 --- a/docs/Deploying/Reference-Architectures.md +++ b/docs/Deploying/Reference-Architectures.md @@ -351,3 +351,4 @@ services: + diff --git a/docs/Deploying/Server-Installation.md b/docs/Deploying/Server-Installation.md index c6e356321f..8be3a6e778 100644 --- a/docs/Deploying/Server-Installation.md +++ b/docs/Deploying/Server-Installation.md @@ -34,7 +34,9 @@ In this guide, we're going to install Fleet and all of its application dependenc ### Setting up a host -Acquiring a CentOS host to use for this guide is largely an exercise for the reader. If you don't have a CentOS host readily available, feel free to use [Vagrant](https://www.vagrantup.com/). In a clean, temporary directory, you can run the following to create a vagrant box, start it, and log into it: +If you don't have a CentOS host readily available, Fleet recommends using [Vagrant](https://www.vagrantup.com/) for this guide. You can find installation instructions on Vagrant's [downloads page](https://developer.hashicorp.com/vagrant/downloads). + +Once you have installed Vagrant, run the following to create a Vagrant box, start it, and log into it: ``` echo 'Vagrant.configure("2") do |config| @@ -519,3 +521,4 @@ Below are some projects created by Fleet community members. These projects provi - [CptOfEvilMinions/FleetDM-Automation](https://github.com/CptOfEvilMinions/FleetDM-Automation) - Ansible and Docker code to set up Fleet + diff --git a/docs/Deploying/Upgrading-Fleet.md b/docs/Deploying/Upgrading-Fleet.md index 89cc83b796..863a68f80d 100644 --- a/docs/Deploying/Upgrading-Fleet.md +++ b/docs/Deploying/Upgrading-Fleet.md @@ -57,4 +57,5 @@ Once Fleet has been replaced with the newest version and the database migrations fleet serve ``` - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Deploying/cloudgov.md b/docs/Deploying/cloudgov.md index 516339a499..d5a3b7d2c6 100644 --- a/docs/Deploying/cloudgov.md +++ b/docs/Deploying/cloudgov.md @@ -112,3 +112,4 @@ variables](https://fleetdm.com/docs/deploying/configuration#using-only-environme + diff --git a/docs/Deploying/fleetctl-agent-updates.md b/docs/Deploying/fleetctl-agent-updates.md index 8f52335279..3c9ab7bab9 100644 --- a/docs/Deploying/fleetctl-agent-updates.md +++ b/docs/Deploying/fleetctl-agent-updates.md @@ -161,3 +161,4 @@ fleetctl updates rotate targets After the key(s) have been rotated, publish the repository in the same fashion as any other update. + diff --git a/docs/README.md b/docs/README.md index a4510f80df..681d2624a7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Fleet documentation -Welcome to the documentation for Fleet, the lightweight telemetry platform for servers and workstations. +Welcome to the documentation for Fleet, the lightweight management platform for laptops and servers. > You can also read the Fleet docs over at https://fleetdm.com/docs. diff --git a/docs/Using-Fleet/Adding-hosts.md b/docs/Using-Fleet/Adding-hosts.md index 7e5744cf77..9d12dc93f0 100644 --- a/docs/Using-Fleet/Adding-hosts.md +++ b/docs/Using-Fleet/Adding-hosts.md @@ -17,6 +17,7 @@ - [Migrating from plain osquery to osquery installer](#migrating-from-plain-osquery-to-osquery-installer) - [Generate installer](#generate-installer) - [Migrate](#migrate) + - [Add Chromebooks with the fleetd Chrome extension](#add-chromebooks-with-the-fleetd-chrome-extension) - [Grant full disk access to osquery on macOS](#grant-full-disk-access-to-osquery-on-macos) - [Creating the configuration profile](#creating-the-configuration-profile) - [Obtaining identifiers](#obtaining-identifiers) @@ -29,7 +30,7 @@ Fleet gathers information from an [osquery](https://github.com/osquery/osquery) You can also install plain osquery on your hosts and connect to Fleet using osquery's `TLS API` plugins. -> For ChromeOS hosts, the fleetd Chrome extension is installed instead of osquery. This Chrome browser extension is only supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). +> For ChromeOS hosts, the [fleetd Chrome extension](#add-chromebooks-with-the-fleetd-chrome-extension) is installed instead of osquery. ## Add hosts with Orbit @@ -279,7 +280,9 @@ installation should appear as the same host in the Fleet UI. If other settings a entries will appear in the Fleet UI. The older entries can be automatically cleaned up with the host expiration setting. To configure this setting, in the Fleet UI, head to **Settings > Organization settings > Advanced options**. -## Add Chromebooks with the Fleetd Chrome extension +## Add Chromebooks with the fleetd Chrome extension + +> The fleetd Chrome browser extension is supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). It is not intended for non-ChromeOS hosts with the Chrome browser installed. Visit the Google Admin console. In the navigation menu, visit Devices > Chrome > Apps & Extensions > Users & browsers. @@ -363,3 +366,4 @@ See the last hour of logs related to TCC permissions with this command: You can then look for `orbit` or `osquery` to narrow down results. + diff --git a/docs/Using-Fleet/Application-security.md b/docs/Using-Fleet/Application-security.md index d9dbe8e9b9..359f314e53 100644 --- a/docs/Using-Fleet/Application-security.md +++ b/docs/Using-Fleet/Application-security.md @@ -67,3 +67,4 @@ libraries and other vulnerabilities is available in our + diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md index 395f7dc61a..0af5c79dfa 100644 --- a/docs/Using-Fleet/Audit-Activities.md +++ b/docs/Using-Fleet/Audit-Activities.md @@ -1,5 +1,5 @@ -# Audit Activities +# Audit activities Fleet logs the following information for administrative actions (in JSON): @@ -553,6 +553,7 @@ This activity contains the following fields: - "host_serial": Serial number of the host. - "host_display_name": Display name of the host. - "installed_from_dep": Whether the host was enrolled via DEP. +- "mdm_platform": Used to distinguish between Apple and Microsoft enrollments. Can be "apple", "microsoft" or not present. If missing, this value is treated as "apple" for backwards compatibility. #### Example @@ -560,7 +561,8 @@ This activity contains the following fields: { "host_serial": "C08VQ2AXHT96", "host_display_name": "MacBookPro16,1 (C08VQ2AXHT96)", - "installed_from_dep": true + "installed_from_dep": true, + "mdm_platform": "apple" } ``` @@ -826,16 +828,17 @@ This activity contains the following fields: ### Type `enabled_windows_mdm` -Generated when a user turns on MDM features for all Windows hosts (servers excluded). +Windows MDM features are not ready for production and are currently in development. These features are disabled by default. Generated when a user turns on MDM features for all Windows hosts (servers excluded). This activity does not contain any detail fields. ### Type `disabled_windows_mdm` -Generated when a user turns off MDM features for all Windows hosts. +Windows MDM features are not ready for production and are currently in development. These features are disabled by default. Generated when a user turns off MDM features for all Windows hosts. This activity does not contain any detail fields. - \ No newline at end of file + + diff --git a/docs/Using-Fleet/Automations.md b/docs/Using-Fleet/Automations.md index c7d2db9fb8..27191afa2e 100644 --- a/docs/Using-Fleet/Automations.md +++ b/docs/Using-Fleet/Automations.md @@ -161,3 +161,4 @@ To enable and configure host status automations, navigate to **Settings > Organi status webhook** in the Fleet UI. + diff --git a/docs/Using-Fleet/CIS-Benchmarks.md b/docs/Using-Fleet/CIS-Benchmarks.md index 65b69cc106..712eb6c862 100644 --- a/docs/Using-Fleet/CIS-Benchmarks.md +++ b/docs/Using-Fleet/CIS-Benchmarks.md @@ -1,32 +1,34 @@ # CIS Benchmarks +> Available in Fleet Premium + ## Overview CIS Benchmarks represent the consensus-based effort of cybersecurity experts globally to help you protect your systems against threats more confidently. For more information about CIS Benchmarks check out [Center for Internet Security](https://www.cisecurity.org/cis-benchmarks)'s website. -Fleet has implemented native support for CIS benchmarks for the following platforms: +Fleet has implemented native support for CIS Benchmarks for the following platforms: - macOS 13.0 Ventura (96 checks) - Windows 10 Enterprise (496 checks) -[Where possible](#limitations), each CIS benchmark is implemented with a [policy query](./REST-API.md#policies) in Fleet. +[Where possible](#limitations), each CIS Benchmark is implemented with a [policy query](./REST-API.md#policies) in Fleet. ## Requirements Following are the requirements to use the CIS Benchmarks in Fleet: -- Fleet must be Premium or Ultimate licensed. -- Devices must be running [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. -- Devices must be enrolled to an MDM solution. +- To use these policies, Fleet must have an up-to-date paid license (≥Fleet Premium). +- Devices must be running [`fleetd`](https://fleetdm.com/docs/using-fleet/orbit), the lightweight agent that bundles the latest osqueryd. +- Some CIS Benchmarks explicitly involve verifying MDM-based controls, so devices must be enrolled to an MDM solution. (Any MDM solution works, it doesn't have to be Fleet.) - On macOS, the orbit executable in Fleetd must have "Full Disk Access", see [Grant Full Disk Access to Osquery on macOS](./Adding-hosts.md#grant-full-disk-access-to-osquery-on-macos). ### MDM required Some of the policies created by Fleet use the [managed_policies](https://www.fleetdm.com/tables/managed_policies) table. This checks whether an MDM solution has turned on the setting to enforce the policy. -Using MDM is the recommended way to manage and enforce CIS benchmarks. To learn how to set up MDM in Fleet, visit [here](/docs/using-fleet/mdm-setup). +Using MDM is the recommended way to manage and enforce CIS Benchmarks. To learn how to set up MDM in Fleet, visit [here](/docs/using-fleet/mdm-setup). ### Fleetd required -Fleet's CIS benchmarks require our [osquery manager, Fleetd](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). This is because Fleetd includes tables which are not part of vanilla osquery in order to accomplish auditing the benchmarks. +Fleet's CIS Benchmarks require our [osquery manager, Fleetd](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). This is because Fleetd includes tables which are not part of vanilla osquery in order to accomplish auditing the benchmarks. -## How to add CIS benchmarks +## How to add CIS Benchmarks All CIS policies are stored under our restricted licensed folder `ee/cis/`. @@ -258,3 +260,4 @@ Requires this GPO in place: 'Computer Configuration\Policies\Administrative Temp + diff --git a/docs/Using-Fleet/ChromeOS.md b/docs/Using-Fleet/ChromeOS.md index c8011d67c6..e2db8d5d46 100644 --- a/docs/Using-Fleet/ChromeOS.md +++ b/docs/Using-Fleet/ChromeOS.md @@ -1,12 +1,12 @@ # ChromeOS +For visibility on ChromeOS hosts, Fleet provides the fleetd Chrome extension which provides similar functionality as osquery on other operating systems. ## Adding ChromeOS hosts to Fleet -Fleet provides a Chrome extension which you can install via Google Admin. - -> For ChromeOS hosts, the fleetd Chrome extension is installed instead of osquery. This Chrome browser extension is only supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). To learn how to add ChromeOS hosts to Fleet, visit [here](https://fleetdm.com/docs/using-fleet/adding-hosts#add-chromebooks-with-the-fleetd-chrome-extension). +> The fleetd Chrome browser extension is supported on ChromeOS operating systems that are managed using [Google Admin](https://admin.google.com). It is not intended for non-ChromeOS hosts with the Chrome browser installed. + ## Available tables To see the available tables for ChromeOS, visit [here](https://fleetdm.com/tables/chrome_extensions?platformFilter=chrome). @@ -16,7 +16,10 @@ By default, the hostname for a Chromebook host will be blank. The hostname can b ## Current Limitations in ChromeOS - Scheduled queries are currently not available in ChromeOS - The Fleetd Chrome extension must be force-installed by enterprise policy in order to have full access to the host's data. -- More tables will be added in https://github.com/fleetdm/fleet/issues/11037 +- More tables that could be added: + - `disk_events`: https://github.com/fleetdm/fleet/issues/12405 + - `client_certificates`: https://github.com/fleetdm/fleet/issues/12465 + - `usb_devices`: https://github.com/fleetdm/fleet/issues/12780 ## Debugging ChromeOS To learn how to debug the Fleetd Chrome extension, visit [here](https://fleetdm.com/docs/contributing/testing-and-local-development#fleetd-chrome-extension). diff --git a/docs/Using-Fleet/Fleet-UI.md b/docs/Using-Fleet/Fleet-UI.md index 872798cd2a..fe3af2d7ff 100644 --- a/docs/Using-Fleet/Fleet-UI.md +++ b/docs/Using-Fleet/Fleet-UI.md @@ -98,5 +98,6 @@ To see all agent options, head to the [agent options documentation](https://flee The agents may take several seconds to update because Fleet has to wait for the hosts to check in. Additionally, hosts enrolled with removed enroll secrets must properly rotate their secret to have the new changes take effect. - + + diff --git a/docs/Using-Fleet/Fleet-desktop.md b/docs/Using-Fleet/Fleet-desktop.md index a2c1efa71d..283031ea00 100644 --- a/docs/Using-Fleet/Fleet-desktop.md +++ b/docs/Using-Fleet/Fleet-desktop.md @@ -59,3 +59,4 @@ This change is imperceptible to users, as clicking on the "My Device" tray item + diff --git a/docs/Using-Fleet/Learn-how-to-use-Fleet.md b/docs/Using-Fleet/Learn-how-to-use-Fleet.md index b258d8c260..730646d2c0 100644 --- a/docs/Using-Fleet/Learn-how-to-use-Fleet.md +++ b/docs/Using-Fleet/Learn-how-to-use-Fleet.md @@ -54,3 +54,4 @@ When the query has finished, you should see several columns in the "Results" tab - The "version" column answers: "What version of the installed operating system is on my device?" + diff --git a/docs/Using-Fleet/Log-destinations.md b/docs/Using-Fleet/Log-destinations.md index b11c752a12..9114dcd737 100644 --- a/docs/Using-Fleet/Log-destinations.md +++ b/docs/Using-Fleet/Log-destinations.md @@ -142,4 +142,5 @@ See the [osquery logging documentation](https://osquery.readthedocs.io/en/stable If `--logger_plugin=tls` is used with osquery clients, the following configuration can be applied on the Fleet server for handling the incoming logs. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Using-Fleet/MDM-commands.md b/docs/Using-Fleet/MDM-commands.md index 1792851186..ec0a2880bb 100644 --- a/docs/Using-Fleet/MDM-commands.md +++ b/docs/Using-Fleet/MDM-commands.md @@ -118,3 +118,4 @@ The command ID can be used to view command results as documented in [step 4 of t + diff --git a/docs/Using-Fleet/MDM-custom-macOS-settings.md b/docs/Using-Fleet/MDM-custom-macOS-settings.md index 3b73fbbf1f..62289de880 100644 --- a/docs/Using-Fleet/MDM-custom-macOS-settings.md +++ b/docs/Using-Fleet/MDM-custom-macOS-settings.md @@ -86,16 +86,19 @@ Learn more about configuration options for hosts that aren't assigned to a team 1. In the Fleet UI, head to the **Controls > macOS settings** tab. -2. In the top box, with "Latest," "Pending," and "Failing" statuses, click each status to view a list hosts: +2. In the top box, with "Verified," "Verifying," "Pending," and "Failed" statuses, click each status to view a list of hosts: -* Latest: hosts that applied the latest settings. +* Verified: hosts that installed all configuration profiles. Fleet has verified with osquery. -* Pending: hosts that will apply the latest settings when the hosts come online. +* Latest: hosts that have acknowledged all MDM commands to install configuration profiles. Fleet is verifying the profiles are installed with osquery. -* Failing: hosts that are failing to apply the latest settings. +* Verifying: hosts that will receive MDM commands to install configuration profiles when the hosts come online. + +* Failed: hosts that failed to install configuration profiles. 3. In the list of hosts, click on an individual host and click the **macOS settings** item to see the status for a specific setting. + diff --git a/docs/Using-Fleet/MDM-disk-encryption.md b/docs/Using-Fleet/MDM-disk-encryption.md index 89d5ab2ecc..4d0253eecb 100644 --- a/docs/Using-Fleet/MDM-disk-encryption.md +++ b/docs/Using-Fleet/MDM-disk-encryption.md @@ -116,3 +116,4 @@ How to reset a macOS host's password using the disk encryption key: + diff --git a/docs/Using-Fleet/MDM-macOS-setup.md b/docs/Using-Fleet/MDM-macOS-setup.md index c038106d1f..222089270b 100644 --- a/docs/Using-Fleet/MDM-macOS-setup.md +++ b/docs/Using-Fleet/MDM-macOS-setup.md @@ -4,9 +4,9 @@ _Available in Fleet Premium_ In Fleet, you can customize the out-of-the-box macOS setup experience for your end users: -* Require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new Mac +* Require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new Mac. -* Customize the macOS Setup Assistant by choosing to show or hide specific panes +* Customize the macOS Setup Assistant by choosing to show or hide specific panes. * Install a bootstrap package to gain full control over the setup experience by installing tools like Puppet, Munki, DEP notify, custom scrips, and more. @@ -150,7 +150,84 @@ You should see the URL for your bootstrap package as the value for `mdm.macos_se ## macOS Setup Assistant -> This feature is currently in development. +When an end user unboxes their new Mac, or starts up a freshly wiped Mac, they're presented with the macOS Setup Assistant. Here they see panes that allow them to configure accessibility, appearance, and more. + +In Fleet, you can customize the macOS Setup Assistant by using an automatic enrollment profile. + +To customize the macOS Setup Assistant, we will do the following steps: + +1. Create an automatic enrollment profile +2. Upload the profile to Fleet +3. Test the custom macOS Setup Assistant + +### Step 1: create an automatic enrollment profile + +1. Download Fleet's example automatic enrollment profile by navigating to the example [here on GitHub](https://github.com/fleetdm/fleet/blob/main/mdm_profiles/setup_assistant.json) and clicking the download icon. + +2. Open the automatic enrollment profile and replace the `profile_name` key with your organization's name. + +3. View the the list of macOS Setup Assistant properties (panes) [here in Apple's Device Management documentation](https://developer.apple.com/documentation/devicemanagement/skipkeys) and choose which panes to hide from your end users. + +4. In your automatic enrollment profile, edit the `skip_setup_items` array so that it includes the panes you want to hide. + +> You can modify properties other than `skip_setup_items`. These are documented by Apple [here](https://developer.apple.com/documentation/devicemanagement/profile). + +### Step 2: upload the profile to Fleet + +1. Choose which team you want to add the automatic enrollment profile to. + +In this example, let's assume you have a "Workstations" team as your [default team](./MDM-setup.md#step-6-optional-set-the-default-team-for-hosts-enrolled-via-abm) in Fleet and you want to test your profile before it's used in production. + +To do this, we'll create a new "Workstations (canary)" team and add the automatic enrollment profile to it. Only hosts that automatically enroll to this team will see the custom macOS Setup Assistant. + +2. Create a `workstations-canary-config.yaml` file: + +```yaml +apiVersion: v1 +kind: team +spec: + team: + name: Workstations (canary) + mdm: + macos_setup: + macos_setup_assistant: ./path/to/automatic_enrollment_profile.json + ... +``` + +Learn more about team configurations options [here](./configuration-files/README.md#teams). + +If you want to customize the macOS Setup Assistant for hosts that automatically enroll to "No team," we'll need to create a `fleet-config.yaml` file: + +```yaml +apiVersion: v1 +kind: config +spec: + mdm: + macos_setup: + macos_setup_assistant: ./path/to/automatic_enrollment_profile.json + ... +``` + +Learn more about configuration options for hosts that aren't assigned to a team [here](./configuration-files/README.md#organization-settings). + +3. Add an `mdm.macos_setup.macos_setup_assistant` key to your YAML document. This key accepts a path to your automatic enrollment profile. + +4. Run the `fleetctl apply -f workstations-canary-config.yml` command to upload the automatic enrollment profile to Fleet. + +### Step 3: test the custom macOS Setup Assistant + +Testing requires a test Mac that is present in your Apple Business Manager (ABM) account. We will wipe this Mac and use it to test the custom macOS Setup Assistant. + +1. Wipe the test Mac by selecting the Apple icon in top left corner of the screen, selecting **System Settings** or **System Preference**, and searching for "Erase all content and settings." Select **Erase All Content and Settings**. + +2. In Fleet, navigate to the Hosts page and find your Mac. Make sure that the host's **MDM status** is set to "Pending." + +> New Macs purchased through Apple Business Manager appear in Fleet with MDM status set to "Pending." Learn more about these hosts [here](./MDM-setup.md#pending-hosts). + +3. Transfer this host to the "Workstations (canary)" team by selecting the checkbox to the left of the host and selecting **Transfer** at the top of the table. In the modal, choose the Workstations (canary) team and select **Transfer**. + +4. Boot up your test Mac and complete the custom out-of-the-box setup experience. + diff --git a/docs/Using-Fleet/MDM-macOS-updates.md b/docs/Using-Fleet/MDM-macOS-updates.md index a3ef6b7eaf..21e132ac7a 100644 --- a/docs/Using-Fleet/MDM-macOS-updates.md +++ b/docs/Using-Fleet/MDM-macOS-updates.md @@ -45,7 +45,15 @@ On Intel Macs, Fleet triggers step 1 (downloading the macOS update) programmatic Step 2 (installing the update) always requires end user action. -### Known issue +### Known issues + +#### Apple Rapid Security Responses (RSRs) + +Currently, end user macOS update reminders via Nudge don't support RSR versions (ex. "13.4.1 (a)"). + +You can use custom MDM commands in Fleet to trigger built-in macOS update reminders for RSRs. Learn how [here](#end-user-macos-update-via-built-in-macos-notifications). + +#### Mac is up to date Sometimes after the end user clicks "update" on the Nudge window, the end user's Mac will say that macOS is up to date when it isn't. This known issue can create a frustrating experience for the end user. Ask the end user to follow the steps below to troubleshoot: @@ -63,7 +71,94 @@ Sometimes after the end user clicks "update" on the Nudge window, the end user's ## End user macOS update via built-in macOS notifications -Built-in macOS update reminders are available for all Fleet instances. To trigger these reminders, run the ["Schedule an OS update" MDM command](https://developer.apple.com/documentation/devicemanagement/schedule_an_os_update). +Built-in macOS update reminders are available in Fleet Free and Fleet Premium. + +To trigger these reminders, we will do the following steps: + +1. Force a macOS update scan + +2. List available macOS updates + +3. Trigger macOS update reminder + +### Step 1: force a macOS update scan + +Use the request payload below when running a custom MDM command with Fleet. Documentation on how to run a custom command is [here](./MDM-commands#custom-commands). + +Request payload: + +```xml + + + + + Command + + ForceUpdateScan + + RequestType + ScheduleOSUpdateScan + + + +``` + +### Step 2: list available macOS updates + +1. Run another custom MDM command using the request payload below. + +Request payload: + +```xml + + + + + Command + + RequestType + AvailableOSUpdates + + + +``` + +2. Copy the `ProductKey` from the command's results. Documentation on how to view a command's results is [here](./MDM-commands#step-4-view-the-commands-results). + +Example product key: `MSU_UPDATE_22F770820d_patch_13.4.1_rsr` + +### Step 3: trigger macOS update reminder + +Run another custom MDM command using the request payload below. Replace the product key with your product key. + +> This payload will trigger the "Install ASAP" behavior which displays a macOS notification with a 60 seconds timer before the Mac automatically restarts. The end user can dismiss the timer. To trigger different behavior, update the `InstallAction`. Options are documented by Apple [here](https://developer.apple.com/documentation/devicemanagement/scheduleosupdatecommand/command/updatesitem). + +Request payload: + +```xml + + + + + Command + + RequestType + ScheduleOSUpdate + Updates + + + InstallAction + InstallASAP + ProductKey + MSU_UPDATE_22F770820d_patch_13.4.1_rsr + + + + + +``` + + diff --git a/docs/Using-Fleet/MDM-migration-guide.md b/docs/Using-Fleet/MDM-migration-guide.md index 9489830e79..a8757262e7 100644 --- a/docs/Using-Fleet/MDM-migration-guide.md +++ b/docs/Using-Fleet/MDM-migration-guide.md @@ -117,3 +117,4 @@ Want to know what your organization can see? Read about [transparency](https://f + diff --git a/docs/Using-Fleet/MDM-setup.md b/docs/Using-Fleet/MDM-setup.md index 332d101cad..eb68a3484f 100644 --- a/docs/Using-Fleet/MDM-setup.md +++ b/docs/Using-Fleet/MDM-setup.md @@ -282,3 +282,4 @@ To renew the token: + diff --git a/docs/Using-Fleet/Monitoring-Fleet.md b/docs/Using-Fleet/Monitoring-Fleet.md index de9d80ffc0..7b6f575118 100644 --- a/docs/Using-Fleet/Monitoring-Fleet.md +++ b/docs/Using-Fleet/Monitoring-Fleet.md @@ -102,3 +102,4 @@ fleetctl debug archive --context server-a The `fleetctl debug archive` command retrieves information generated by Go's [`net/http/pprof`](https://golang.org/pkg/net/http/pprof/) package. In most scenarios this should not include sensitive information, however it does include command line arguments to the Fleet server. If the Fleet server receives sensitive credentials via CLI argument (not environment variables or config file), this information should be scrubbed from the archive in the `cmdline` file. + diff --git a/docs/Using-Fleet/Osquery-process.md b/docs/Using-Fleet/Osquery-process.md index 2f1cfeba5d..76aa4e3f47 100644 --- a/docs/Using-Fleet/Osquery-process.md +++ b/docs/Using-Fleet/Osquery-process.md @@ -25,4 +25,5 @@ If the managed extension is `Non-existent` (either because it was `Non-existent` Lastly, we check the state of the watcher process itself. If it is deemed unhealthy because of resource contention, then the osquery process is shut down. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md index 8a6d2d723c..5aa124d52a 100644 --- a/docs/Using-Fleet/Permissions.md +++ b/docs/Using-Fleet/Permissions.md @@ -155,3 +155,4 @@ Users that are members of multiple teams can be assigned different roles for eac \** Team observers can view all queries but the UI and fleetctl only list the ones they can run (**observer can run**). + diff --git a/docs/Using-Fleet/Process-File-Events.md b/docs/Using-Fleet/Process-File-Events.md index 086c6d2aa4..dbc3a18c6f 100644 --- a/docs/Using-Fleet/Process-File-Events.md +++ b/docs/Using-Fleet/Process-File-Events.md @@ -176,4 +176,5 @@ auditdnetlink.cpp:354 The Audit publisher has throttled reading records from Net Some events might get lost due to system load or low CPU/memory resources. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 018fb8e40b..8dd5b74628 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -3122,6 +3122,46 @@ Retrieves the disk encryption key for a host. } ``` +### Get configuration profiles assigned to a host + +Requires Fleet's MDM properly [enabled and configured](./Mobile-device-management.md). + +Retrieves a list of the configuration profiles assigned to a host. + +`GET /api/v1/fleet/mdm/hosts/:id/profiles` + +#### Parameters + +| Name | Type | In | Description | +| ---- | ------- | ---- | -------------------------------- | +| id | integer | path | **Required**. The ID of the host | + + +#### Example + +`GET /api/v1/fleet/mdm/hosts/8/profiles` + +##### Default response + +`Status: 200` + +```json +{ + "host_id": 8, + "profiles": [ + { + "profile_id": 1337, + "team_id": 0, + "name": "Example profile", + "identifier": "com.example.profile", + "created_at": "2023-03-31T00:00:00Z", + "updated_at": "2023-03-31T00:00:00Z", + "checksum": "dGVzdAo=" + } + ] +} +``` + --- @@ -3701,12 +3741,13 @@ List all configuration profiles for macOS hosts enrolled to Fleet's MDM that are { "profiles": [ { - "profile_id": 1337, - "team_id": 0, - "name": "Example profile", - "identifier": "com.example.profile", - "created_at": "2023-03-31T00:00:00Z", - "updated_at": "2023-03-31T00:00:00Z" + "profile_id": 1337, + "team_id": 0, + "name": "Example profile", + "identifier": "com.example.profile", + "created_at": "2023-03-31T00:00:00Z", + "updated_at": "2023-03-31T00:00:00Z", + "checksum": "dGVzdAo=" } ] } @@ -7572,3 +7613,4 @@ Response: --- + \ No newline at end of file diff --git a/docs/Using-Fleet/Security-audits.md b/docs/Using-Fleet/Security-audits.md index 397508a391..b105c74608 100644 --- a/docs/Using-Fleet/Security-audits.md +++ b/docs/Using-Fleet/Security-audits.md @@ -224,3 +224,5 @@ Our goal with this audit was to ensure that our auto-updater mechanism, built wi improvements to make it more robust and resilient to compromise. + + diff --git a/docs/Using-Fleet/Supported-browsers.md b/docs/Using-Fleet/Supported-browsers.md index 252669831a..3f941f9c8b 100644 --- a/docs/Using-Fleet/Supported-browsers.md +++ b/docs/Using-Fleet/Supported-browsers.md @@ -24,3 +24,4 @@ We test each browser on Windows whenever possible, because our engineering team > - The Fleet user interface [may not be fully supported](https://github.com/fleetdm/fleet/issues/969) in Google Chrome when the browser is running on ChromeOS + diff --git a/docs/Using-Fleet/Supported-host-operating-systems.md b/docs/Using-Fleet/Supported-host-operating-systems.md index af6babe2fb..44c3b409d1 100644 --- a/docs/Using-Fleet/Supported-host-operating-systems.md +++ b/docs/Using-Fleet/Supported-host-operating-systems.md @@ -27,3 +27,4 @@ If you aren't sure what version of `glibc` your distribution is using, [DistroWa + diff --git a/docs/Using-Fleet/Teams.md b/docs/Using-Fleet/Teams.md index 275e3005ce..9006277bac 100644 --- a/docs/Using-Fleet/Teams.md +++ b/docs/Using-Fleet/Teams.md @@ -130,4 +130,5 @@ To delete a team: 3. On the right side, select "Delete team" and confirm the action. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Using-Fleet/Troubleshooting-live-queries.md b/docs/Using-Fleet/Troubleshooting-live-queries.md index d2737662ea..ab180cb56d 100644 --- a/docs/Using-Fleet/Troubleshooting-live-queries.md +++ b/docs/Using-Fleet/Troubleshooting-live-queries.md @@ -137,4 +137,5 @@ fleetctl query \ If this works and the browser is not working then it might be a rendering issue on the browser. You should also try running the live query on different browsers. - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/Using-Fleet/Usage-statistics.md b/docs/Using-Fleet/Usage-statistics.md index ceddde34d3..87469f29ed 100644 --- a/docs/Using-Fleet/Usage-statistics.md +++ b/docs/Using-Fleet/Usage-statistics.md @@ -127,3 +127,4 @@ To disable usage statistics: 3. Uncheck the "Enable usage statistics" checkbox and then select "Update settings." + diff --git a/docs/Using-Fleet/Vulnerability-Processing.md b/docs/Using-Fleet/Vulnerability-Processing.md index a1e8bed259..178dfc690e 100644 --- a/docs/Using-Fleet/Vulnerability-Processing.md +++ b/docs/Using-Fleet/Vulnerability-Processing.md @@ -409,3 +409,4 @@ The CPE translation. Used to match CPEs in the CPE database. Fields are are AND' Once we have a good CPE, we can match it against the CVE database. We download the data streams locally and match each CPE to the whole list. The matching is done using the [nvdtools implementation](https://github.com/facebookincubator/nvdtools). + diff --git a/docs/Using-Fleet/configuration-files/README.md b/docs/Using-Fleet/configuration-files/README.md index 4fe3cbf074..d98a650c72 100644 --- a/docs/Using-Fleet/configuration-files/README.md +++ b/docs/Using-Fleet/configuration-files/README.md @@ -290,12 +290,10 @@ integrations webhook_settings ``` -You can bypass these errors by removing the key from your YAML or adding the `--force` flag. This flag will force application of the changes without validation. Proceed with caution. +You can bypass these errors by removing the key from your YAML or adding the `--force` flag. This flag will apply the changes without validation and should be used with caution. ### Mobile device management (MDM) settings for teams -> MDM features are not ready for production and are currently in development. These features are disabled by default. - The `mdm` section of this configuration YAML lets you control MDM settings for each team in Fleet. To specify Team MDM configuration, as opposed to [Organization-wide MDM configuration](#mobile-device-management-mdm-settings), follow the below YAML format. Note the `kind: team` field, as well as the `name` and `mdm` fields under `team`. @@ -1360,8 +1358,6 @@ agent_options: #### Mobile device management (MDM) settings -> MDM features are not ready for production and are currently in development. These features are disabled by default. - The `mdm` section of the configuration YAML lets you control MDM settings in Fleet. ##### mdm.apple_bm_default_team @@ -1379,6 +1375,8 @@ Set name of default team to use with Apple Business Manager. ##### mdm.windows_enabled_and_configured +> Windows MDM features are not ready for production and are currently in development. These features are disabled by default. + Enables or disables Windows MDM support. - Default value: false @@ -1467,3 +1465,5 @@ If you're using Fleet Premium, this enforces disk encryption on all hosts assign #### Advanced configuration > **Note:** More settings are included in the [contributor documentation](https://fleetdm.com/docs/contributing/configuration-for-contributors). It's possible, although not recommended, to configure these settings in the YAML configuration file. + + diff --git a/docs/Using-Fleet/fleetctl-CLI.md b/docs/Using-Fleet/fleetctl-CLI.md index 7214499bcb..d833f8dba1 100644 --- a/docs/Using-Fleet/fleetctl-CLI.md +++ b/docs/Using-Fleet/fleetctl-CLI.md @@ -432,3 +432,4 @@ This will generate a `tar.gz` file with: - Files containing database-specific information. + diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index b2b5370792..f8dab0f3cc 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -110,6 +110,7 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu // organization information sum.Config.OrgInfo.OrgName = appCfg.OrgInfo.OrgName sum.Config.OrgInfo.OrgLogoURL = appCfg.OrgInfo.OrgLogoURL + sum.Config.OrgInfo.OrgLogoURLLightBackground = appCfg.OrgInfo.OrgLogoURLLightBackground sum.Config.OrgInfo.ContactURL = appCfg.OrgInfo.ContactURL // mdm information diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 0c1484801c..89a9729b25 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -4,16 +4,15 @@ import ( "bytes" "context" "crypto/sha256" + "database/sql" "encoding/base64" "encoding/json" "errors" - "fmt" "io" "net/http" "net/url" "sort" "strings" - "time" "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/server/authz" @@ -147,6 +146,30 @@ func (svc *Service) MDMAppleEraseDevice(ctx context.Context, hostID uint) error return nil } +func (svc *Service) MDMListHostConfigurationProfiles(ctx context.Context, hostID uint) ([]*fleet.MDMAppleConfigProfile, error) { + if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { + return nil, err + } + + host, err := svc.ds.HostLite(ctx, hostID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "find host to list profiles") + } + + var tmID uint + if host.TeamID != nil { + tmID = *host.TeamID + } + + // NOTE: the service method also does all the right authorization checks + sums, err := svc.ListMDMAppleConfigProfiles(ctx, tmID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "list config profiles") + } + + return sums, nil +} + func (svc *Service) MDMAppleEnableFileVaultAndEscrow(ctx context.Context, teamID *uint) error { cert, _, _, err := svc.config.MDM.AppleSCEP() if err != nil { @@ -786,46 +809,35 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost return err } - // Collect the profiles' hashes and look for a team with exactly that set. - // Also collect the profiles' groups in case we need to create a new team, + // Collect the profiles' groups in case we need to create a new team, // and the list of raw profiles bytes. - hashes, groups, rawProfiles := make([]string, 0, len(profs.Profiles)), - make([]string, 0, len(profs.Profiles)), + groups, rawProfiles := make([]string, 0, len(profs.Profiles)), make([][]byte, 0, len(profs.Profiles)) for _, prof := range profs.Profiles { - hashes = append(hashes, prof.HexMD5Hash) - rawProfiles = append(rawProfiles, prof.Profile) if prof.Group != "" { groups = append(groups, prof.Group) } - } - // find a team with exactly that set of profiles - teamIDs, err := svc.ds.MatchMDMAppleConfigProfiles(ctx, hashes) - if err != nil { - return err + if !prof.Exclude { + rawProfiles = append(rawProfiles, prof.Profile) + } } + + teamName := teamNameFromPreassignGroups(groups) + team, err := svc.ds.TeamByName(ctx, teamName) - var targetTeamID uint - if len(teamIDs) > 0 { - // if the host is already in one of those valid teams, nothing to do. - if host.TeamID != nil { - for _, tmID := range teamIDs { - if *host.TeamID == tmID { - return nil - } - } + if err != nil { + // TODO: update to use fleet.IsNotFound once + // https://github.com/fleetdm/fleet/pull/12620 is merged + if !errors.Is(err, sql.ErrNoRows) { + return err } - // else assign the host to the first valid team - targetTeamID = teamIDs[0] - } else { // Create a new team with this set of profiles. Creating via the service // call so that it properly assigns the agent options and creates audit // activities, etc. - teamName := teamNameFromPreassignGroups(groups) payload := fleet.TeamPayload{Name: &teamName} - tm, err := svc.NewTeam(ctx, payload) + team, err = svc.NewTeam(ctx, payload) if err != nil { return err } @@ -842,27 +854,24 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost // TODO: seems like we don't support enabling disk encryption // on team creation? // see https://github.com/fleetdm/fleet/issues/12220 - tm, err = svc.ModifyTeam(ctx, tm.ID, payload) + team, err = svc.ModifyTeam(ctx, team.ID, payload) if err != nil { return err } + } - // create profiles for that team via the service call, so that uniqueness - // of profile identifier/name is verified, activity created, etc. - // NOTE: this will use the read replica to load the team, which was just - // created above, could lead to not found issues with slow replication. - if err := svc.BatchSetMDMAppleProfiles(ctx, &tm.ID, nil, rawProfiles, false); err != nil { - return err - } - - targetTeamID = tm.ID + // create profiles for that team via the service call, so that uniqueness + // of profile identifier/name is verified, activity created, etc. + if err := svc.BatchSetMDMAppleProfiles(ctx, &team.ID, nil, rawProfiles, false); err != nil { + return err } // assign host to that team via the service call, which will trigger // deployment of the profiles. - if err := svc.AddHostsToTeam(ctx, &targetTeamID, []uint{host.ID}); err != nil { + if err := svc.AddHostsToTeam(ctx, &team.ID, []uint{host.ID}); err != nil { return err } + return nil } @@ -870,8 +879,7 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost // created to match the set of profiles preassigned to a host. The team name is // derived from the "group" field provided with each request to pre-assign a // profile to a host (in fleet.MDMApplePreassignProfilePayload). That field is -// optional, and empty groups are ignored. The current timestamp is appended to -// the team's name to help avoid duplicates. +// optional, and empty groups are ignored. func teamNameFromPreassignGroups(groups []string) string { const defaultName = "default" @@ -891,5 +899,5 @@ func teamNameFromPreassignGroups(groups []string) string { groups = []string{defaultName} } - return fmt.Sprintf("%s (%s)", strings.Join(groups, " - "), time.Now().UTC().Format("2006-01-02:15:04:05.000")) + return strings.Join(groups, " - ") } diff --git a/ee/server/service/mdm_profiles.go b/ee/server/service/mdm_profiles.go index 6c7782fe29..fba921ba0f 100644 --- a/ee/server/service/mdm_profiles.go +++ b/ee/server/service/mdm_profiles.go @@ -30,6 +30,8 @@ var fileVaultProfileTemplate = template.Must(template.New("").Option("missingkey 1 ShowRecoveryKey + DeferForceAtUserLoginMaxBypassAttempts + 1 EncryptCertPayloadUUID @@ -64,17 +66,17 @@ var fileVaultProfileTemplate = template.Must(template.New("").Option("missingkey 1 - dontAllowFDEDisable - - PayloadIdentifier - com.apple.MCX.62024f29-105E-497A-A724-1D5BA4D9E854 - PayloadType - com.apple.MCX - PayloadUUID - 62024f29-105E-497A-A724-1D5BA4D9E854 - PayloadVersion - 1 - + dontAllowFDEDisable + + PayloadIdentifier + com.apple.MCX.62024f29-105E-497A-A724-1D5BA4D9E854 + PayloadType + com.apple.MCX + PayloadUUID + 62024f29-105E-497A-A724-1D5BA4D9E854 + PayloadVersion + 1 + PayloadDisplayName Disk encryption diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 524c7eb3ef..4d0119ccca 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -3,9 +3,7 @@ package service import ( "bytes" "context" - "database/sql" "encoding/json" - "errors" "fmt" "net/http" @@ -605,14 +603,6 @@ func (svc *Service) teamByIDOrName(ctx context.Context, id *uint, name *string) } else if name != nil { tm, err = svc.ds.TeamByName(ctx, *name) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - // this should really be handled in TeamByName so that it returns a - // notFound error as is usually the case for this scenario, but - // changing it causes a number of test failures that indicates this - // might be tricky and even maybe a breaking change in some places. For - // now, handling it here. - return nil, notFoundError{} - } return nil, err } } @@ -635,7 +625,7 @@ func (svc *Service) checkAuthorizationForTeams(ctx context.Context, specs []*fle for _, spec := range specs { team, err := svc.ds.TeamByName(ctx, spec.Name) if err != nil { - if err := ctxerr.Cause(err); err == sql.ErrNoRows { + if fleet.IsNotFound(err) { // Can the user create a new team? if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionWrite); err != nil { return err @@ -688,7 +678,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, switch { case err == nil: // OK - case ctxerr.Cause(err) == sql.ErrNoRows: + case fleet.IsNotFound(err): if spec.Name == "" { return nil, fleet.NewInvalidArgumentError("name", "name may not be empty") } diff --git a/ee/tools/puppet/fleetdm/README.md b/ee/tools/puppet/fleetdm/README.md index 126b17a852..069a220bba 100644 --- a/ee/tools/puppet/fleetdm/README.md +++ b/ee/tools/puppet/fleetdm/README.md @@ -60,15 +60,50 @@ node default { } ``` +The `group` parameter is used to create/match profiles with teams in +Fleet. In the example above, all devices will be assigned to a team named +`workstations`. + +You can use this feature along with the `ensure` param to create teams that +**don't** contain specific profiles, for example given the following manifest: + +```pp +node default { + fleetdm::profile { 'com.apple.universalaccess': + template => template('fleetdm/profile-template.mobileconfig.erb'), + group => 'workstations', + } + + if $facts['architecture'] == 'x86_64' { + fleetdm::profile { 'my.arm.only.profile': + ensure => absent, + template => template('fleetdm/my-arm-only-profile.mobileconfig.erb'), + group => 'amd64', + } + } else { + fleetdm::profile { 'my.arm.only.profile': + template => template('fleetdm/my-arm-only-profile.mobileconfig.erb'), + group => 'workstations', + } + } +} +``` + +Assuming you have devices with both architectures checking in, you'll end up +with the following two teams in Fleet: + +- `workstations`: with two profiles, `com.apple.universalaccess` and `my.arm.only.profile` +- `workstations - amd64`: with only one profile, `com.apple.universalaccess` + ### Sending a custom MDM Command You can use the `fleetdm::command_xml` function to send any custom MDM command to the device: -``` - $host_uuid = $facts['system_profiler']['hardware_uuid'] - $command_uuid = generate('/usr/bin/uuidgen').strip +```pp +$host_uuid = $facts['system_profiler']['hardware_uuid'] +$command_uuid = generate('/usr/bin/uuidgen').strip - $xml_data = " +$xml_data = " @@ -82,12 +117,12 @@ You can use the `fleetdm::command_xml` function to send any custom MDM command t " - $response = fleetdm::command_xml($host_uuid, $xml_data) - $err = $response['error'] +$response = fleetdm::command_xml($host_uuid, $xml_data) +$err = $response['error'] - if $err != '' { - notify { "Error sending MDM command: ${err}": } - } +if $err != '' { + notify { "Error sending MDM command: ${err}": } +} ``` ### Releasing a device from await configuration diff --git a/ee/tools/puppet/fleetdm/examples/multiple-teams.pp b/ee/tools/puppet/fleetdm/examples/multiple-teams.pp index bfeb583ded..699e19eb35 100644 --- a/ee/tools/puppet/fleetdm/examples/multiple-teams.pp +++ b/ee/tools/puppet/fleetdm/examples/multiple-teams.pp @@ -1,10 +1,10 @@ node default { - fleetdm::profile { 'cis.macOSBenchmark.section2.BluetoothSharing': + fleetdm::profile { 'com.apple.SoftwareUpdate': template => template('fleetdm/automatic_updates.mobileconfig.erb'), group => 'base', } - fleetdm::profile { 'com.apple.SoftwareUpdate': + fleetdm::profile { 'cis.macOSBenchmark.section2.BluetoothSharing': template => template('fleetdm/disable_bluetooth_file_sharing.mobileconfig.erb'), group => 'workstations', } diff --git a/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/preassign_profile.rb b/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/preassign_profile.rb index 0e5b0837c6..1c33e1626f 100644 --- a/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/preassign_profile.rb +++ b/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/preassign_profile.rb @@ -8,19 +8,29 @@ Puppet::Functions.create_function(:"fleetdm::preassign_profile") do param 'String', :host_uuid param 'String', :template optional_param 'String', :group + optional_param 'Enum[absent, present]', :ensure end - def preassign_profile(profile_identifier, host_uuid, template, group = 'default') - host = call_function('lookup', 'fleetdm::host') - token = call_function('lookup', 'fleetdm::token') - client = Puppet::Util::FleetClient.new(host, token) + def preassign_profile(profile_identifier, host_uuid, template, group = 'default', ensure_profile = 'present') + client = Puppet::Util::FleetClient.instance run_identifier = "#{closure_scope.catalog.catalog_uuid}-#{Puppet[:node_name_value]}" - response = client.preassign_profile(run_identifier, host_uuid, template, group) + response = client.preassign_profile(run_identifier, host_uuid, template, group, ensure_profile) if response['error'].empty? - Puppet.info("successfully pre-assigned profile #{profile_identifier}") + base64_checksum = Digest::MD5.base64digest(template) + host = client.get_host_by_identifier(host_uuid) + host_profiles = client.get_host_profiles(host['body']['host']['id']) + + if host_profiles['error'].empty? + Puppet.info("successfully pre-set profile #{profile_identifier} as #{ensure_profile}") + + has_profile = host_profiles['body']['profiles'].any? { |p| p['checksum'] == base64_checksum } + if (has_profile && ensure_profile == 'absent') || (!has_profile && ensure_profile == 'present') + response['resource_changed'] = true + end + end else - Puppet.err("error pre-assigning profile #{profile_identifier}: #{response['error']} \n\n #{template}") + Puppet.err("error pre-setting profile #{profile_identifier} (ensure #{ensure_profile}): #{response['error']} \n\n #{template}") end response diff --git a/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/release_device.rb b/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/release_device.rb index 7cc7da580f..7c88936038 100644 --- a/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/release_device.rb +++ b/ee/tools/puppet/fleetdm/lib/puppet/functions/fleetdm/release_device.rb @@ -29,9 +29,7 @@ Puppet::Functions.create_function(:"fleetdm::release_device") do COMMAND_TEMPLATE - host = call_function('lookup', 'fleetdm::host') - token = call_function('lookup', 'fleetdm::token') - client = Puppet::Util::FleetClient.new(host, token) + client = Puppet::Util::FleetClient.instance response = client.send_mdm_command(uuid, command_xml) if response['error'].empty? diff --git a/ee/tools/puppet/fleetdm/lib/puppet/reports/fleetdm.rb b/ee/tools/puppet/fleetdm/lib/puppet/reports/fleetdm.rb index 2de2d7698f..06d353bb4b 100644 --- a/ee/tools/puppet/fleetdm/lib/puppet/reports/fleetdm.rb +++ b/ee/tools/puppet/fleetdm/lib/puppet/reports/fleetdm.rb @@ -8,15 +8,8 @@ Puppet::Reports.register_report(:fleetdm) do def process return if noop + client = Puppet::Util::FleetClient.instance node_name = Puppet[:node_name_value] - node = Puppet::Node.new(node_name) - compiler = Puppet::Parser::Compiler.new(node) - scope = Puppet::Parser::Scope.new(compiler) - lookup_invocation = Puppet::Pops::Lookup::Invocation.new(scope, {}, {}, nil) - host = Puppet::Pops::Lookup.lookup('fleetdm::host', nil, '', false, nil, lookup_invocation) - token = Puppet::Pops::Lookup.lookup('fleetdm::token', nil, '', false, nil, lookup_invocation) - - client = Puppet::Util::FleetClient.new(host, token) run_identifier = "#{catalog_uuid}-#{node_name}" response = client.match_profiles(run_identifier) diff --git a/ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb b/ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb index d39fd81a7b..603b0ae19c 100644 --- a/ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb +++ b/ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb @@ -7,9 +7,33 @@ require 'hiera_puppet' module Puppet::Util # FleetClient provides an interface for making HTTP requests to a Fleet server. class FleetClient - def initialize(host, token) - @host = host - @token = token + include Singleton + + # NOTE: the Puppet server supports [multithread mode][1], but it's a beta + # feature subject to change. Still adding a mutex to control instances and + # the cache just in case. + # + # [1]: https://www.puppet.com/docs/puppet/8/server/config_file_puppetserver.html + @instance_mutex = Mutex.new + + def self.instance + return @instance if @instance + @instance_mutex.synchronize do + @instance ||= new + end + @instance + end + + def initialize + node_name = Puppet[:node_name_value] + node = Puppet::Node.new(node_name) + compiler = Puppet::Parser::Compiler.new(node) + scope = Puppet::Parser::Scope.new(compiler) + lookup_invocation = Puppet::Pops::Lookup::Invocation.new(scope, {}, {}, nil) + @host = Puppet::Pops::Lookup.lookup('fleetdm::host', nil, '', false, nil, lookup_invocation) + @token = Puppet::Pops::Lookup.lookup('fleetdm::token', nil, '', false, nil, lookup_invocation) + @cache = {} + @cache_mutex = Mutex.new end # Pre-assigns a profile to a host. Note that the profile assignment is not @@ -20,14 +44,16 @@ module Puppet::Util # @param profile_xml [String] Raw XML with the configuration profile. # @param group [String] Used to construct a team name. # @return [Hash] The response status code, headers, and body. - def preassign_profile(run_identifier, uuid, profile_xml, group) - post( - '/api/latest/fleet/mdm/apple/profiles/preassign', - { + def preassign_profile(run_identifier, uuid, profile_xml, group, ensure_profile) + req( + method: :post, + path: '/api/latest/fleet/mdm/apple/profiles/preassign', + body: { 'external_host_identifier' => run_identifier, 'host_uuid' => uuid, 'profile' => Base64.strict_encode64(profile_xml), 'group' => group, + 'exclude' => ensure_profile == 'absent', }, ) end @@ -42,10 +68,11 @@ module Puppet::Util # pre-assigned profiles. # @return [Hash] The response status code, headers, and body. def match_profiles(run_identifier) - post('/api/latest/fleet/mdm/apple/profiles/match', - { - 'external_host_identifier' => run_identifier, - }) + req( + method: :post, + path: '/api/latest/fleet/mdm/apple/profiles/match', + body: { 'external_host_identifier' => run_identifier }, + ) end # Sends an MDM command to the host with the specified UUID. @@ -54,8 +81,8 @@ module Puppet::Util # @param command_xml [String] Raw XML with the MDM command. # @return [Hash] The response status code, headers, and body. def send_mdm_command(uuid, command_xml) - post('/api/latest/fleet/mdm/apple/enqueue', - { + req(method: :post, path: '/api/latest/fleet/mdm/apple/enqueue', + body: { # For some reason, the enqueue function expects the command to be # base64 encoded using _raw encoding_ (without padding, as defined in RFC # 4648 section 3.2) @@ -67,20 +94,50 @@ module Puppet::Util }) end - # Sends an HTTP POST request to the specified path. + # Get profiles assigned to the host. # - # @param path [String] The path of the resource to post to. - # @param body [Object] (optional) The request body to send. - # @param headers [Hash] (optional) Additional headers to include in the request. + # @param host_id [Number] Fleet's internal host id. # @return [Hash] The response status code, headers, and body. - def post(path, body = nil, headers = {}) + def get_host_profiles(host_id) + req(method: :get, path: "/api/latest/fleet/mdm/hosts/#{host_id}/profiles", cached: false) + end + + # Gets host details by host identifier. + # + # @param identifier [String] The host identifier, can be + # osquery_host_identifier, node_key, UUID, or hostname. + # @return [Hash] The response status code, headers, and body. + def get_host_by_identifier(identifier) + req(method: :get, path: "/api/latest/fleet/hosts/identifier/#{identifier}", cached: true) + end + + private + + def req(method: :get, path: '', body: nil, headers: {}, cached: false) + if cached + @cache_mutex.synchronize do + unless @cache[path].nil? + return @cache[path] + end + end + end + out = { 'error' => '' } uri = URI.parse("#{@host}#{path}") + uri.path.squeeze! '/' + uri.path.chomp! '/' http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme == 'https' - request = Net::HTTP::Post.new(uri.request_uri) + case method + when :get + request = Net::HTTP::Get.new(uri.request_uri) + when :post + request = Net::HTTP::Post.new(uri.request_uri) + else + throw "HTTP method #{method} not implemented" + end headers['Authorization'] = "Bearer #{@token}" headers.each { |key, value| request[key] = value } @@ -89,6 +146,12 @@ module Puppet::Util begin response = http.request(request) out = parse_response(response) + + if cached && out['error'].empty? + @cache_mutex.synchronize do + @cache[path] = out + end + end rescue => e out['error'] = e end @@ -96,14 +159,17 @@ module Puppet::Util out end - private - def parse_response(response) out = { 'status' => response.code.to_i, - 'error' => '' + 'error' => '', + 'body' => {} } + if response.body + out['body'] = JSON.parse(response.body) + end + if (400...600).cover?(response.code.to_i) message = 'server returned a non-ok status code without an error' @@ -113,7 +179,7 @@ module Puppet::Util unless body['errors'].nil? error_messages = body['errors'].map { |e| "#{e['name']} #{e['reason']}" } - message = [message, *error_messages].join(': ') + message = [message, *error_messages].join(' : ').delete_prefix(' : ') end end diff --git a/ee/tools/puppet/fleetdm/manifests/profile.pp b/ee/tools/puppet/fleetdm/manifests/profile.pp index bc51c86b8d..5cd7265fca 100644 --- a/ee/tools/puppet/fleetdm/manifests/profile.pp +++ b/ee/tools/puppet/fleetdm/manifests/profile.pp @@ -17,12 +17,18 @@ # Fleet keeps track of each time this resource is # declared with a group name, the final team name # will be a concatenation of all unique group names. +# @param ensure +# Whether the profile should be present or not. +# Set to `absent` along with a distinct `group` +# name to create a new team that doesn't have the +# configuration profile. # # @example # fleetdm::profile { 'identifier': } define fleetdm::profile ( String $template, String $group = 'default', + Enum['absent', 'present'] $ensure = 'present', ) { if $facts["clientnoop"] { notice('noop mode: skipping profile definition in the Fleet server') @@ -36,15 +42,18 @@ define fleetdm::profile ( } $host_uuid = $facts['system_profiler']['hardware_uuid'] - $response = fleetdm::preassign_profile($name, $host_uuid, $template, $group) + $response = fleetdm::preassign_profile($name, $host_uuid, $template, $group, $ensure) $err = $response['error'] + $changed = $response['resource_changed'] if $err != '' { - notify { "error pre-assigning profile ${$name}: ${$err}": + notify { "error pre-setting profile ${name} as ${ensure}: ${err}": loglevel => 'err', } - } else { - notify { "successfully pre-assigned profile ${$name}": } + } elsif $changed { + # NOTE: sending a notification also marks the + # 'fleetdm::profile' as changed in the reports. + notify { "successfully pre-set profile ${name} as ${ensure}": } } } } diff --git a/ee/tools/puppet/fleetdm/metadata.json b/ee/tools/puppet/fleetdm/metadata.json index 3935a2d21b..17e0d1f44e 100644 --- a/ee/tools/puppet/fleetdm/metadata.json +++ b/ee/tools/puppet/fleetdm/metadata.json @@ -1,6 +1,6 @@ { "name": "fleetdm-fleetdm", - "version": "0.1.2", + "version": "0.2.1", "author": "Fleet Device Management Inc", "summary": "MDM management and profile assignment using FleetDM", "license": "proprietary", diff --git a/ee/tools/puppet/fleetdm/spec/defines/profile_spec.rb b/ee/tools/puppet/fleetdm/spec/defines/profile_spec.rb index cd528a7da8..beb940683f 100644 --- a/ee/tools/puppet/fleetdm/spec/defines/profile_spec.rb +++ b/ee/tools/puppet/fleetdm/spec/defines/profile_spec.rb @@ -10,6 +10,7 @@ describe 'fleetdm::profile' do let(:node_name) { Puppet[:node_name_value] } let(:catalog_uuid) { '827a74c8-cf98-44da-9ff7-18c5e4bee41e' } let(:run_identifier) { "#{catalog_uuid}-#{node_name}" } + let(:host_response) { { 'host' => { 'id' => 1 } } } let(:params) do { 'template' => template, 'group' => group } end @@ -17,7 +18,7 @@ describe 'fleetdm::profile' do before(:each) do fleet_client_class = class_spy('Puppet::Util::FleetClient') stub_const('Puppet::Util::FleetClient', fleet_client_class) - allow(fleet_client_class).to receive(:new).with('https://example.com', 'test_token') { fleet_client_mock } + allow(fleet_client_class).to receive(:instance) { fleet_client_mock } allow(SecureRandom).to receive(:uuid).and_return(catalog_uuid) end @@ -27,7 +28,18 @@ describe 'fleetdm::profile' do it 'compiles' do uuid = os_facts[:system_profiler]['hardware_uuid'] - expect(fleet_client_mock).to receive(:preassign_profile).with(run_identifier, uuid, template, group).and_return({ 'error' => '' }) + expect(fleet_client_mock) + .to receive(:get_host_by_identifier) + .with(uuid) + .and_return({ 'error' => '', 'body' => host_response }) + expect(fleet_client_mock) + .to receive(:get_host_profiles) + .with(host_response['host']['id']) + .and_return({ 'error' => '', 'body' => { 'profiles' => [] } }) + expect(fleet_client_mock) + .to receive(:preassign_profile) + .with(run_identifier, uuid, template, group, 'present') + .and_return({ 'error' => '' }) is_expected.to compile end @@ -55,6 +67,14 @@ describe 'fleetdm::profile' do it { is_expected.to compile.and_raise_error(%r{invalid group}) } end + context 'invalid ensure' do + let(:params) do + { 'template' => template, 'ensure' => 'nothing' } + end + + it { is_expected.to compile.and_raise_error(%r{'ensure' expects a match for Enum\['absent', 'present'\]}) } + end + context 'without group' do let(:params) do { 'template' => template } @@ -62,7 +82,41 @@ describe 'fleetdm::profile' do it 'compiles' do uuid = os_facts[:system_profiler]['hardware_uuid'] - expect(fleet_client_mock).to receive(:preassign_profile).with(run_identifier, uuid, template, 'default').and_return({ 'error' => '' }) + expect(fleet_client_mock) + .to receive(:get_host_by_identifier) + .with(uuid) + .and_return({ 'error' => '', 'body' => host_response }) + expect(fleet_client_mock) + .to receive(:get_host_profiles) + .with(host_response['host']['id']) + .and_return({ 'error' => '', 'body' => { 'profiles' => [] } }) + expect(fleet_client_mock) + .to receive(:preassign_profile) + .with(run_identifier, uuid, template, 'default', 'present') + .and_return({ 'error' => '' }) + is_expected.to compile + end + end + + context 'ensure => absent' do + let(:params) do + { 'template' => template, 'ensure' => 'absent' } + end + + it 'compiles' do + uuid = os_facts[:system_profiler]['hardware_uuid'] + expect(fleet_client_mock) + .to receive(:get_host_by_identifier) + .with(uuid) + .and_return({ 'error' => '', 'body' => host_response }) + expect(fleet_client_mock) + .to receive(:get_host_profiles) + .with(host_response['host']['id']) + .and_return({ 'error' => '', 'body' => { 'profiles' => [] } }) + expect(fleet_client_mock) + .to receive(:preassign_profile) + .with(run_identifier, uuid, template, 'default', 'absent') + .and_return({ 'error' => '' }) is_expected.to compile end end diff --git a/ee/tools/puppet/fleetdm/spec/functions/fleet_client_spec.rb b/ee/tools/puppet/fleetdm/spec/functions/fleet_client_spec.rb index b67d52d644..b5cd9ed51c 100644 --- a/ee/tools/puppet/fleetdm/spec/functions/fleet_client_spec.rb +++ b/ee/tools/puppet/fleetdm/spec/functions/fleet_client_spec.rb @@ -3,13 +3,96 @@ require 'spec_helper' describe 'Puppet::Util::FleetClient' do - let(:client) { Puppet::Util::FleetClient.new('https://example.com', 'token') } + let(:client) { Puppet::Util::FleetClient.instance } + let(:host) { 'https://test.example.com' } + let(:token) { 'supersecret' } + let(:identifier) { 'test_ident' } - it 'handles POST with 204 responses' do - response = Net::HTTPSuccess.new(1.0, '204', 'OK') - expect_any_instance_of(Net::HTTP).to receive(:request) { response } # rubocop:disable RSpec/AnyInstance + before(:each) do + stub_const( + 'Puppet::Parser::Compiler', + class_spy('Puppet::Parser::Compiler'), + ) - result = client.post('/example') - expect(result[:body]).to be(nil) + stub_const( + 'Puppet::Parser::Scope', + class_spy('Puppet::Parser::Scope'), + ) + + lookup = class_spy('Puppet::Pops::Lookup') + stub_const('Puppet::Pops::Lookup', lookup) + allow(lookup) + .to receive(:lookup) + .with('fleetdm::host', anything, anything, anything, anything, anything) { host } + + allow(lookup) + .to receive(:lookup) + .with('fleetdm::token', anything, anything, anything, anything, anything) { token } + + stub_const( + 'Puppet::Pops::Lookup::Invocation', + class_spy('Puppet::Pops::Lookup::Invocation'), + ) + end + + def mock_http_post(uri: '', request_body: {}, response: nil) + mock_net_http = instance_double('Net:HTTP') + mock_net_http_post = instance_double('Net::HTTP::POST') + allow(Net::HTTP).to receive(:new).and_return(mock_net_http) + allow(mock_net_http).to receive(:use_ssl=).with(true) + allow(Net::HTTP::Post).to receive(:new).with(uri).and_return(mock_net_http_post) + allow(mock_net_http_post).to receive(:[]=).with('Authorization', "Bearer #{token}") + allow(mock_net_http_post).to receive(:body=).with(request_body.to_json) + allow(mock_net_http).to receive(:request).with(mock_net_http_post) { response } + end + + def mock_http_get(uri: '', response: instance_double(Net::HTTPSuccess, code: 204, body: nil)) + mock_net_http = instance_double('Net:HTTP') + mock_net_http_get = instance_double('Net::HTTP::POST') + allow(Net::HTTP).to receive(:new).and_return(mock_net_http) + allow(mock_net_http).to receive(:use_ssl=).with(true) + allow(Net::HTTP::Get).to receive(:new).with(uri).and_return(mock_net_http_get) + allow(mock_net_http_get).to receive(:[]=).with('Authorization', "Bearer #{token}") + allow(mock_net_http).to receive(:request).with(mock_net_http_get) { response } + end + + describe '#match_profiles' do + describe 'successful response' do + subject :result do + mock_http_post( + uri: '/api/latest/fleet/mdm/apple/profiles/match', + request_body: { 'external_host_identifier' => identifier }, + response: instance_double(Net::HTTPSuccess, code: 204, body: nil), + ) + client.match_profiles(identifier) + end + + it { expect(result['body']).to eq({}) } + it { expect(result['error']).to eq('') } + it { expect(result['status']).to eq(204) } + end + + describe 'response with errors' do + subject :result do + mock_http_post( + uri: '/api/latest/fleet/mdm/apple/profiles/match', + request_body: { 'external_host_identifier' => identifier }, + response: instance_double( + Net::HTTPServerError, + code: 500, + body: body.to_json, + ), + ) + client.match_profiles(identifier) + end + + let(:body) do + { 'errors' => [{ 'name' => 'server error', 'reason' => 'unknown' }] } + end + + it { expect(result['body']).to eq(body) } + it { expect(result['error']).to eq('server error unknown') } + it { expect(result['status']).to eq(500) } + end end end diff --git a/ee/tools/puppet/fleetdm/spec/functions/preassign_profile_spec.rb b/ee/tools/puppet/fleetdm/spec/functions/preassign_profile_spec.rb index 0ae5d424fb..b4b374134e 100644 --- a/ee/tools/puppet/fleetdm/spec/functions/preassign_profile_spec.rb +++ b/ee/tools/puppet/fleetdm/spec/functions/preassign_profile_spec.rb @@ -7,27 +7,51 @@ describe 'fleetdm::preassign_profile' do let(:device_uuid) { 'device-uuid' } let(:template) { 'template' } let(:group) { 'group' } + let(:ensure_profile) { 'absent' } let(:node_name) { Puppet[:node_name_value] } let(:catalog_uuid) { '827a74c8-cf98-44da-9ff7-18c5e4bee41e' } let(:run_identifier) { "#{catalog_uuid}-#{node_name}" } let(:profile_identifier) { 'test.example.com' } + let(:host_response) { { 'host' => { 'id' => 1 } } } before(:each) do fleet_client_class = class_spy('Puppet::Util::FleetClient') stub_const('Puppet::Util::FleetClient', fleet_client_class) - allow(fleet_client_class).to receive(:new).with('https://example.com', 'test_token') { fleet_client_mock } + allow(fleet_client_class).to receive(:instance) { fleet_client_mock } allow(SecureRandom).to receive(:uuid).and_return(catalog_uuid) end it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } it 'performs an API call to Fleet with the right parameters' do - expect(fleet_client_mock).to receive(:preassign_profile).with(run_identifier, device_uuid, template, group).and_return({ 'error' => '' }) - is_expected.to run.with_params(profile_identifier, device_uuid, template, group) + expect(fleet_client_mock) + .to receive(:get_host_by_identifier) + .with(device_uuid) + .and_return({ 'error' => '', 'body' => host_response }) + expect(fleet_client_mock) + .to receive(:get_host_profiles) + .with(host_response['host']['id']) + .and_return({ 'error' => '', 'body' => { 'profiles' => [] } }) + expect(fleet_client_mock) + .to receive(:preassign_profile) + .with(run_identifier, device_uuid, template, group, ensure_profile) + .and_return({ 'error' => '' }) + is_expected.to run.with_params(profile_identifier, device_uuid, template, group, ensure_profile) end - it 'has a default value if group is not provided' do - expect(fleet_client_mock).to receive(:preassign_profile).with(run_identifier, device_uuid, template, 'default').and_return({ 'error' => '' }) + it 'has default values for `group` and `ensure`' do + expect(fleet_client_mock) + .to receive(:get_host_by_identifier) + .with(device_uuid) + .and_return({ 'error' => '', 'body' => host_response }) + expect(fleet_client_mock) + .to receive(:get_host_profiles) + .with(host_response['host']['id']) + .and_return({ 'error' => '', 'body' => { 'profiles' => [] } }) + expect(fleet_client_mock) + .to receive(:preassign_profile) + .with(run_identifier, device_uuid, template, 'default', 'present') + .and_return({ 'error' => '' }) is_expected.to run.with_params(profile_identifier, device_uuid, template) end end diff --git a/ee/tools/puppet/fleetdm/spec/functions/release_device_spec.rb b/ee/tools/puppet/fleetdm/spec/functions/release_device_spec.rb index 727dfb9018..3cbe7db5c3 100644 --- a/ee/tools/puppet/fleetdm/spec/functions/release_device_spec.rb +++ b/ee/tools/puppet/fleetdm/spec/functions/release_device_spec.rb @@ -10,7 +10,7 @@ describe 'fleetdm::release_device' do before(:each) do fleet_client_class = class_spy('Puppet::Util::FleetClient') stub_const('Puppet::Util::FleetClient', fleet_client_class) - allow(fleet_client_class).to receive(:new).with('https://example.com', 'test_token') { fleet_client_mock } + allow(fleet_client_class).to receive(:instance) { fleet_client_mock } end it { is_expected.to run.with_params(nil).and_raise_error(StandardError) } diff --git a/frontend/__mocks__/appleMdm.ts b/frontend/__mocks__/appleMdm.ts new file mode 100644 index 0000000000..1ed7964056 --- /dev/null +++ b/frontend/__mocks__/appleMdm.ts @@ -0,0 +1,16 @@ +import { IMdmApple } from "interfaces/mdm"; + +const DEFAULT_MDM_APPLE_MOCK: IMdmApple = { + common_name: "APSP:12345", + serial_number: "12345", + issuer: "Test Certification Authority", + renew_date: "2023-03-24T22:13:59Z", +}; + +export const createMockMdmApple = ( + overrides?: Partial +): IMdmApple => { + return { ...DEFAULT_MDM_APPLE_MOCK, ...overrides }; +}; + +export default createMockMdmApple; diff --git a/frontend/__mocks__/axiosError.ts b/frontend/__mocks__/axiosError.ts new file mode 100644 index 0000000000..7e11bd2c7b --- /dev/null +++ b/frontend/__mocks__/axiosError.ts @@ -0,0 +1,14 @@ +import { AxiosError } from "axios"; + +const DEFAULT_AXIOS_ERROR_MOCK: AxiosError = { + isAxiosError: true, + toJSON: () => ({}), + name: "Error", + message: "error message", +}; + +const createMockAxiosError = (overrides?: Partial): AxiosError => { + return { ...DEFAULT_AXIOS_ERROR_MOCK, ...overrides }; +}; + +export default createMockAxiosError; diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index 5feb718f0c..c51d2895d5 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -4,6 +4,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = { org_info: { org_name: "fleet", org_logo_url: "", + org_logo_url_light_background: "", contact_url: "https://fleetdm.com/company/contact", }, server_settings: { diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss index a33061b394..1aa73266d8 100644 --- a/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss +++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/_styles.scss @@ -26,7 +26,7 @@ // allows for the tooltip text to break on a word instead of a character &.tooltip-break-on-word { .truncated-tooltip { - word-break: normal + word-break: normal; } } @@ -43,13 +43,13 @@ } } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .truncated-tooltip { max-width: 400px; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .truncated-tooltip { max-width: 800px; } diff --git a/frontend/components/TableContainer/_styles.scss b/frontend/components/TableContainer/_styles.scss index fc3c656a4c..1cb21065f6 100644 --- a/frontend/components/TableContainer/_styles.scss +++ b/frontend/components/TableContainer/_styles.scss @@ -15,12 +15,12 @@ flex-direction: column-reverse; align-items: start; - @media (min-width: $break-768) { + @media (min-width: $break-xs) { flex-direction: row; align-items: end; justify-content: space-between; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { align-items: center; } } @@ -36,7 +36,7 @@ flex-direction: column-reverse; align-items: start; - @media (min-width: $break-990) { + @media (min-width: $break-md) { flex-direction: row; justify-content: space-between; align-items: center; @@ -66,7 +66,7 @@ &.stack-table-controls { padding-top: $pad-large; - @media (min-width: $break-990) { + @media (min-width: $break-md) { padding-top: 0; } } @@ -87,10 +87,10 @@ .search-field__input-wrapper { width: 250px; margin-bottom: 0; - @media (min-width: $break-768) { + @media (min-width: $break-xs) { width: 300px; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { width: 344px; } } @@ -99,23 +99,12 @@ padding-bottom: $pad-large; margin-left: 0; - @media (min-width: $break-768) { + @media (min-width: $break-xs) { margin-left: $pad-medium; padding-bottom: 0; } } - &::before { - display: inline-block; - position: absolute; - padding: 5px 0 0 0; // centers spin - content: url(../assets/images/icon-search-fleet-black-16x16@2x.png); - transform: scale(0.5); - height: 20px; - top: 3px; - left: 8px; - } - .input-field { padding-left: 42px; width: 100%; diff --git a/frontend/components/buttons/ActionButtons/_styles.scss b/frontend/components/buttons/ActionButtons/_styles.scss index 0fa87ee5af..4ef7cfc0a7 100644 --- a/frontend/components/buttons/ActionButtons/_styles.scss +++ b/frontend/components/buttons/ActionButtons/_styles.scss @@ -7,7 +7,7 @@ &__secondary-buttons { display: none; - @media (min-width: 990px) { + @media (min-width: $break-md) { display: flex; } } @@ -35,7 +35,7 @@ } } } - @media (min-width: 990px) { + @media (min-width: $break-md) { display: none; } } diff --git a/frontend/components/forms/ConfirmInviteForm/ConfirmInviteForm.jsx b/frontend/components/forms/ConfirmInviteForm/ConfirmInviteForm.jsx index 653fb10ef9..57dde25c35 100644 --- a/frontend/components/forms/ConfirmInviteForm/ConfirmInviteForm.jsx +++ b/frontend/components/forms/ConfirmInviteForm/ConfirmInviteForm.jsx @@ -32,6 +32,8 @@ class ConfirmInviteForm extends Component { diff --git a/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.jsx b/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.jsx index 39f4b2ac2a..83a20af976 100644 --- a/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.jsx +++ b/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.jsx @@ -4,12 +4,11 @@ import PropTypes from "prop-types"; import Button from "components/buttons/Button"; import Form from "components/forms/Form"; import formFieldInterface from "interfaces/form_field"; -import helpers from "components/forms/ForgotPasswordForm/helpers"; import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon"; +import validate from "./validate"; const baseClass = "forgot-password-form"; const fieldNames = ["email"]; -const { validate } = helpers; class ForgotPasswordForm extends Component { static propTypes = { @@ -26,7 +25,12 @@ class ForgotPasswordForm extends Component { return (
{baseError &&
{baseError}
} - +
+ +
  • +

    2. Go to your email to download your CSR.

    +
  • +
  • +

    + 3.{" "} + +
    + If you don't have an Apple ID, select Create yours now. +

    +
  • +
  • +

    + 4. In Apple Push Certificates Portal, select{" "} + Create a Certificate, upload your CSR, and download your APNs + certificate. +

    +
  • +
  • +

    + 5. Deploy Fleet with mdm configuration.{" "} + +

    +
  • + +
    + ); +}; + +interface IApplePushCertificatePortalSetupInfoProps { + appleAPNInfo: IMdmApple; +} + +const ApplePushCertificatePortalSetupInfo = ({ + appleAPNInfo, +}: IApplePushCertificatePortalSetupInfoProps) => { + return ( +
    +
    +
    Common name (CN)
    +
    {appleAPNInfo.common_name}
    +
    +
    +
    Serial number
    +
    {appleAPNInfo.serial_number}
    +
    +
    +
    Issuer
    +
    {appleAPNInfo.issuer}
    +
    +
    +
    Renew date
    +
    {readableDate(appleAPNInfo.renew_date)}
    +
    +
    + ); +}; + +const MacOSMdmPage = () => { + const { config } = useContext(AppContext); + const [showRequestCSRModal, setShowRequestCSRModal] = useState(false); + + // Currently the status of this API call is what determines various UI states on + // this page. Because of this we will not render any of this components UI until this API + // call has completed. + const { + data: appleAPNInfo, + isLoading: isLoadingMdmApple, + error: errorMdmApple, + } = useQuery( + ["appleAPNInfo"], + () => mdmAppleAPI.getAppleAPNInfo(), + { + retry: (tries, error) => error.status !== 404 && tries <= 3, + enabled: config?.mdm.enabled_and_configured, + staleTime: 5000, + } + ); + + const toggleRequestCSRModal = () => { + setShowRequestCSRModal((prevState) => !prevState); + }; + + const renderPageContent = () => { + // The API returns a 404 error if APNs is not configured yet, in that case we + // want to prompt the user to download the certs and keys to configure the + // server instead of the default error message. + const showMdmAppleError = errorMdmApple && errorMdmApple.status !== 404; + + if (showMdmAppleError) { + return ; + } + + if (!appleAPNInfo) { + return ( + + ); + } + + return ; + }; + + return ( + + <> + +

    Apple Push Certificate Portal

    + {isLoadingMdmApple ? : renderPageContent()} + {showRequestCSRModal && ( + + )} + +
    + ); +}; + +export default MacOSMdmPage; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/_styles.scss new file mode 100644 index 0000000000..484d3ba023 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/_styles.scss @@ -0,0 +1,58 @@ +.mac-os-mdm-page { + &__back-to-mdm { + margin-bottom: $pad-xlarge; + } + + h1 { + margin-bottom: $pad-xxlarge; + font-size: $x-large; + } + + h4 { + margin-bottom: 0; + } + + p { + font-size: $x-small; + margin: 0 0 $pad-large; + } + + &__page-content { + font-size: $x-small; + } + + &__setup-content { + display: flex; + flex-direction: column; + gap: $pad-large; + color: $core-fleet-black; + + p { + margin: 0; + } + } + + &__setup-instructions-list { + padding: 0; + margin: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: $pad-large; + }; + + &__request-button { + margin-top: $pad-small; + } + + &__apc-info { + display: flex; + flex-direction: column; + gap: $pad-medium; + + dt { + font-weight: $bold; + margin-bottom: $pad-xsmall; + } + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/index.ts new file mode 100644 index 0000000000..6a6ac996b1 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage/index.ts @@ -0,0 +1 @@ +export { default } from "./MacOSMdmPage"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx index faba86e35b..c3a4b44601 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/MdmSettings.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext } from "react"; import { useQuery } from "react-query"; import { AxiosError } from "axios"; import { InjectedRouter } from "react-router"; @@ -8,16 +8,12 @@ import { AppContext } from "context/app"; import mdmAppleAPI from "services/entities/mdm_apple"; import { IMdmApple } from "interfaces/mdm"; -import { readableDate } from "utilities/helpers"; import PATHS from "router/paths"; -import Button from "components/buttons/Button"; -import CustomLink from "components/CustomLink"; import Spinner from "components/Spinner"; -import DataError from "components/DataError"; -import RequestCSRModal from "./components/RequestCSRModal"; import EndUserMigrationSection from "./components/EndUserMigrationSection/EndUserMigrationSection"; -import WindowsMdmSection from "./components/WindowsMdmSection/WindowsMdmSection"; +import WindowsMdmCard from "./components/WindowsMdmCard/WindowsMdmCard"; +import MacOSMdmCard from "./components/MacOSMdmCard/MacOSMdmCard"; const baseClass = "mdm-settings"; @@ -28,8 +24,9 @@ interface IMdmSettingsProps { const MdmSettings = ({ router }: IMdmSettingsProps) => { const { isPremiumTier, config } = useContext(AppContext); - const [showRequestCSRModal, setShowRequestCSRModal] = useState(false); - + // Currently the status of this API call is what determines various UI states on + // this page. Because of this we will not render any of this components UI until this API + // call has completed. const { data: appleAPNInfo, isLoading: isLoadingMdmApple, @@ -44,111 +41,42 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => { } ); - const toggleRequestCSRModal = () => { - setShowRequestCSRModal(!showRequestCSRModal); + const navigateToMacOSMdm = () => { + router.push(PATHS.ADMIN_INTEGRATIONS_MDM_MAC); }; const navigateToWindowsMdm = () => { router.push(PATHS.ADMIN_INTEGRATIONS_MDM_WINDOWS); }; - // The API returns a 404 error if APNs is not configured yet, in that case we - // want to prompt the user to download the certs and keys to configure the - // server instead of the default error message. - const showMdmAppleError = errorMdmApple && errorMdmApple.status !== 404; - - const renderMdmAppleSection = () => { - if (showMdmAppleError) { - return ; - } - - if (!appleAPNInfo) { - return ( - <> -
    - Connect Fleet to Apple Push Certificates Portal to change settings - and install software on your macOS hosts. -
    -
    -

    - 1. Request a certificate signing request (CSR) and key for Apple - Push Notification Service (APNs) and a certificate and key for - Simple Certificate Enrollment Protocol (SCEP). -

    - -

    2. Go to your email to download your CSR.

    -

    - 3.{" "} - -
    - If you don’t have an Apple ID, select Create yours now. -

    -

    - 4. In Apple Push Certificates Portal, select{" "} - Create a Certificate, upload your CSR, and download your - APNs certificate. -

    -

    - 5. Deploy Fleet with mdm configuration.{" "} - -

    -
    - - ); - } - - return ( - <> -
    - To change settings and install software on your macOS hosts, Apple - Inc. requires an Apple Push Notification service (APNs) certificate. -
    -
    -

    Common name (CN)

    -

    {appleAPNInfo.common_name}

    -

    Serial number

    -

    {appleAPNInfo.serial_number}

    -

    Issuer

    -

    {appleAPNInfo.issuer}

    -

    Renew date

    -

    {readableDate(appleAPNInfo.renew_date)}

    -
    - - ); - }; - return (
    -
    -

    Apple Push Certificates Portal

    - {isLoadingMdmApple ? : renderMdmAppleSection()} +
    +

    Mobile device management (MDM)

    + {isLoadingMdmApple ? ( + + ) : ( + <> + + {/* TODO: remove conditional rendering when windows MDM is released. */} + {config?.mdm_enabled && ( + + )} + + )}
    - {/* TODO: remove conditional rendering when windows MDM is released. */} - {config?.mdm_enabled && ( - - )} - {isPremiumTier && ( - <> -
    - -
    - - )} - {showRequestCSRModal && ( - + {isPremiumTier && appleAPNInfo && ( +
    + +
    )}
    ); diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx index be2e6861b6..121f102b12 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx @@ -62,11 +62,9 @@ const WindowsMdmOnContent = ({ router }: IWindowsMdmOnContentProps) => { return ( <>

    Turn on Windows MDM

    -

    - This will turn MDM on for Windows hosts with fleetd, overriding existing - MDM solutions. -

    -

    MDM won't be turned on for Windows servers

    +

    This will turn MDM on for Windows hosts with fleetd.

    +

    Hosts connected to another MDM solution won't be migrated.

    +

    MDM won't be turned on for Windows servers.

    ); @@ -87,7 +85,10 @@ const WindowsMdmOffContent = ({ router }: IWindowsMdmOffContentProps) => { return ( <>

    Turn off Windows MDM

    -

    This will turn off MDM on each Windows host.

    +

    + MDM will no longer be turned on for Windows hosts that enroll to Fleet. +

    +

    Hosts with MDM already turned on MDM will not have MDM removed.

    ); diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss index 840c887eeb..ecf4b4c49c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss @@ -6,6 +6,7 @@ h1 { margin-bottom: $pad-xxlarge; + font-size: $x-large; } p { diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss index 474d8b2f3d..b15f5037ee 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/_styles.scss @@ -5,20 +5,15 @@ gap: 80px; &__section { - margin: 0 0 $pad-large; h2 { + margin-bottom: 0; padding-bottom: $pad-small; max-width: 100%; font-size: $medium; font-weight: $regular; color: $core-fleet-black; border-bottom: solid 1px $ui-fleet-black-10; - margin: 0 0 $pad-xxlarge; - } - - h4 { - margin-bottom: 0; } .mdm-settings-team-btn { @@ -34,17 +29,9 @@ } } - &__section-description, - &__section-instructions, - &__section-information { - font-size: $x-small; - color: $core-fleet-black; - width: 100%; - } - - &__section-information { - p { - margin: 0; - } + &__mdm-section { + display: flex; + flex-direction: column; + gap: 40px; } } diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx index 5556e2f675..7f9477e38b 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/EndUserMigrationSection/EndUserMigrationSection.tsx @@ -139,8 +139,8 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {

    End user migration workflow

    - Control the end user migration workflow for hosts that automatically - enrolled to your old MDM solution. + Control the end user migration workflow for macOS hosts that + automatically enrolled to your old MDM solution.

    { value={formData.isEnabled} onChange={toggleMigrationEnabled} activeText="Enabled" - inactiveText="Diabled" + inactiveText="Disabled" className={`${baseClass}__enabled-slider`} />
    diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tests.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tests.tsx new file mode 100644 index 0000000000..06042587cc --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tests.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { noop } from "lodash"; +import { render, screen } from "@testing-library/react"; + +import createMockMdmApple from "__mocks__/appleMdm"; +import createMockAxiosError from "__mocks__/axiosError"; + +import MacOSMdmCard from "./MacOSMdmCard"; + +describe("MacOSMdmCard", () => { + it("renders the turn on macOs mdm state when there is no appleAPNInfo", () => { + render( + + ); + + expect(screen.getByText("Turn on macOS MDM")).toBeInTheDocument(); + }); + + it("renders the show details state when there is appleAPNInfo", () => { + render( + + ); + + expect(screen.getByText("macOS MDM turned on")).toBeInTheDocument(); + }); + + it("renders the error state when there is a non 404 error", () => { + render( + + ); + + expect(screen.getByText(/Something's gone wrong/)).toBeInTheDocument(); + + render( + + ); + }); +}); diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tsx new file mode 100644 index 0000000000..cbab69ba7c --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/MacOSMdmCard.tsx @@ -0,0 +1,88 @@ +import React from "react"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; +import Card from "components/Card"; +import DataError from "components/DataError"; +import { AxiosError } from "axios"; +import { IMdmApple } from "interfaces/mdm"; + +const baseClass = "mac-os-mdm-card"; + +interface ITurnOnMacOSMdmProps { + onClickTurnOn: () => void; +} + +const TurnOnMacOSMdm = ({ onClickTurnOn }: ITurnOnMacOSMdmProps) => { + return ( +
    +
    +

    Turn on macOS MDM

    +

    + Connect Fleet to Apple Push Certificates Portal to change settings and + install software on your macOS hosts. +

    +
    + +
    + ); +}; + +interface ITurnOffMacOSMdmProps { + onClickDetails: () => void; +} + +const SeeDetailsMacOSMdm = ({ onClickDetails }: ITurnOffMacOSMdmProps) => { + return ( +
    +
    + +

    macOS MDM turned on

    +
    + +
    + ); +}; + +interface IMacOSMdmCardProps { + appleAPNInfo: IMdmApple | undefined; + errorData: AxiosError | null; + turnOnMacOSMdm: () => void; + viewDetails: () => void; +} + +/** + * This compoent is responsible for showing the correct UI for the macOS MDM card. + * We pass in the appleAPNInfo and errorData from the MdmSettings component because + * we need to make that API call higher up in the component tree to correctly show + * loading states on the page. + */ +const MacOSMdmCard = ({ + appleAPNInfo, + errorData, + turnOnMacOSMdm, + viewDetails, +}: IMacOSMdmCardProps) => { + // The API returns a 404 error if APNS is not configured yet. If there is any + // other error we will show the DataError component. + const showError = errorData !== null && errorData.status !== 404; + + if (showError) { + return ; + } + + return ( + + {appleAPNInfo !== undefined ? ( + + ) : ( + + )} + + ); +}; + +export default MacOSMdmCard; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss new file mode 100644 index 0000000000..302a4beb18 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/_styles.scss @@ -0,0 +1,37 @@ +.mac-os-mdm-card { + font-size: $x-small; + + p { + margin: 0; + } + + &__turn-on-mac-os, + &__turn-off-mac-os { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__turn-on-mac-os { + h3 { + font-size: $x-small; + font-weight: $bold; + margin: 0 0 $pad-xsmall; + } + + p { + max-width: 520px; + } + } + + &__turn-off-mac-os { + >div { + display: flex; + align-items: center; + } + + p { + margin-left: $pad-small; + } + } +} diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/index.ts new file mode 100644 index 0000000000..c49eac27ed --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/MacOSMdmCard/index.ts @@ -0,0 +1 @@ +export { default } from "./MacOSMdmCard"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/WindowsMdmSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/WindowsMdmCard.tsx similarity index 87% rename from frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/WindowsMdmSection.tsx rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/WindowsMdmCard.tsx index cf4f2da0ad..ec7a2d3fce 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/WindowsMdmSection.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/WindowsMdmCard.tsx @@ -6,15 +6,16 @@ import Card from "components/Card/Card"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; -const baseClass = "windows-mdm-section"; +const baseClass = "windows-mdm-card"; interface ITurnOnWindowsMdmProps { onClickTurnOn: () => void; } + const TurnOnWindowsMdm = ({ onClickTurnOn }: ITurnOnWindowsMdmProps) => { return (
    -
    +

    Turn on Windows MDM

    Turn MDM on for Windows hosts with fleetd.

    @@ -42,15 +43,15 @@ const TurnOffWindowsMdm = ({ onClickEdit }: ITurnOffWindowsMdmProps) => { ); }; -interface IWindowsMdmSectionProps { +interface IWindowsMdmCardProps { turnOnWindowsMdm: () => void; editWindowsMdm: () => void; } -const WindowsMdmSection = ({ +const WindowsMdmCard = ({ turnOnWindowsMdm, editWindowsMdm, -}: IWindowsMdmSectionProps) => { +}: IWindowsMdmCardProps) => { const { config } = useContext(AppContext); const isWindowsMdmEnabled = @@ -67,4 +68,4 @@ const WindowsMdmSection = ({ ); }; -export default WindowsMdmSection; +export default WindowsMdmCard; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss similarity index 94% rename from frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/_styles.scss rename to frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss index 3ed546487a..f157bb1178 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/_styles.scss @@ -1,4 +1,4 @@ -.windows-mdm-section { +.windows-mdm-card { font-size: $x-small; p { @@ -29,5 +29,4 @@ margin-left: $pad-small; } } - } diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/index.ts new file mode 100644 index 0000000000..79f55056f7 --- /dev/null +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmCard/index.ts @@ -0,0 +1 @@ +export { default } from "./WindowsMdmCard"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/index.ts b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/index.ts deleted file mode 100644 index 2e04c3a5cc..0000000000 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/WindowsMdmSection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./WindowsMdmSection"; diff --git a/frontend/pages/admin/OrgSettingsPage/_styles.scss b/frontend/pages/admin/OrgSettingsPage/_styles.scss index 47122a0217..1091d0c1dc 100644 --- a/frontend/pages/admin/OrgSettingsPage/_styles.scss +++ b/frontend/pages/admin/OrgSettingsPage/_styles.scss @@ -18,12 +18,6 @@ } } - .org-info .app-config-form { - &__inputs { - width: 60%; - } - } - .info-banner { margin-top: $pad-medium; } @@ -72,7 +66,7 @@ border-bottom: solid 1px $ui-fleet-black-10; margin: 0 0 $pad-xxlarge; - @media (min-width: $break-990) { + @media (min-width: $break-md) { max-width: 65%; } } @@ -85,7 +79,7 @@ .empty-table__container { margin: 96px 0 1.5rem; - @media (min-width: $break-990) { + @media (min-width: $break-md) { max-width: 65%; } } @@ -119,7 +113,7 @@ color: $core-fleet-black; width: 100%; - @media (min-width: $break-990) { + @media (min-width: $break-md) { width: 60%; } } @@ -130,7 +124,7 @@ padding-right: $pad-small; box-sizing: border-box; - @media (min-width: $break-990) { + @media (min-width: $break-md) { width: 60%; } @@ -180,47 +174,6 @@ } } - &__details { - float: right; - width: 40%; - height: 87px; - - .icon-tooltip { - margin: $pad-xlarge 0; - } - - .hint { - color: $core-fleet-black; - - &--brand { - color: $core-vibrant-blue; - } - } - } - - &__avatar-preview { - text-align: center; - - img { - border-radius: $border-radius; - max-height: 80px; - max-width: 150px; - border: 1px solid $ui-fleet-black-10; - background-color: $ui-light-grey; - position: relative; - bottom: -29px; - padding: $small; - transform: initial; - } - - p { - color: $core-fleet-purple; - font-size: 18px; - font-weight: $bold; - margin-top: 0; - } - } - &__smtp-section { @include clearfix; } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx index b369c04806..be76f1d7b1 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import classnames from "classnames"; import Button from "components/buttons/Button"; // @ts-ignore @@ -16,10 +17,14 @@ import { interface IOrgInfoFormData { orgName: string; orgLogoURL: string; + orgLogoURLLightBackground: string; orgSupportURL: string; } +// TODO: change base classes to these cards to follow the same pattern as the +// other components in the app. const baseClass = "app-config-form"; +const cardClass = "org-info"; const Info = ({ appConfig, @@ -29,11 +34,18 @@ const Info = ({ const [formData, setFormData] = useState({ orgName: appConfig.org_info.org_name || "", orgLogoURL: appConfig.org_info.org_logo_url || "", + orgLogoURLLightBackground: + appConfig.org_info.org_logo_url_light_background || "", orgSupportURL: appConfig.org_info.contact_url || "https://fleetdm.com/company/contact", }); - const { orgName, orgLogoURL, orgSupportURL } = formData; + const { + orgName, + orgLogoURL, + orgLogoURLLightBackground, + orgSupportURL, + } = formData; const [formErrors, setFormErrors] = useState({}); @@ -65,10 +77,10 @@ const Info = ({ const onFormSubmit = (evt: React.MouseEvent) => { evt.preventDefault(); - // Formatting of API not UI const formDataToSubmit = { org_info: { org_logo_url: orgLogoURL, + org_logo_url_light_background: orgLogoURLLightBackground, org_name: orgName, contact_url: orgSupportURL, }, @@ -77,8 +89,10 @@ const Info = ({ handleSubmit(formDataToSubmit); }; + const classNames = classnames(baseClass, cardClass); + return ( - +

    Organization info

    @@ -91,15 +105,6 @@ const Info = ({ onBlur={validateForm} error={formErrors.org_name} /> - -
    -
    - +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    -
    {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && ( @@ -160,12 +162,14 @@ const SelectQueryModal = ({
    -
    {(!isOnlyObserver || isObserverPlus || isHostsTeamObserverPlus) && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss index a97628ecd9..9cff3c71b3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss @@ -43,17 +43,6 @@ flex-grow: 3; position: relative; - &::before { - display: inline-block; - position: absolute; - padding: 5px 0 0 0; // centers spin - content: url(../assets/images/icon-search-fleet-black-16x16@2x.png); - transform: scale(0.5); - height: 20px; - top: 3px; - left: 8px; - } - .form-field { margin-bottom: 0; } diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss index 47d153e024..3cf71ec2aa 100644 --- a/frontend/pages/hosts/details/_styles.scss +++ b/frontend/pages/hosts/details/_styles.scss @@ -85,7 +85,7 @@ column-gap: $pad-xxlarge; row-gap: $pad-medium; - @media (min-width: $break-990) { + @media (min-width: $break-md) { grid-template-columns: repeat(4, max-content); grid-template-rows: repeat(3, 1fr); } diff --git a/frontend/pages/hosts/details/cards/Packs/_styles.scss b/frontend/pages/hosts/details/cards/Packs/_styles.scss index 3213409056..e4f9cdc939 100644 --- a/frontend/pages/hosts/details/cards/Packs/_styles.scss +++ b/frontend/pages/hosts/details/cards/Packs/_styles.scss @@ -16,7 +16,7 @@ display: none; width: 0; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .last_run__header { display: table-cell; } @@ -33,7 +33,7 @@ display: none; width: 0; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .last_run__cell { display: table-cell; } diff --git a/frontend/pages/hosts/details/cards/Software/_styles.scss b/frontend/pages/hosts/details/cards/Software/_styles.scss index 1f9046960f..5d379b6caa 100644 --- a/frontend/pages/hosts/details/cards/Software/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/_styles.scss @@ -57,7 +57,7 @@ .version__header { width: $col-xs; display: none; - @media (min-width: $break-880) { + @media (min-width: $break-sm) { display: table-cell; } } @@ -77,7 +77,7 @@ .linkToFilteredHosts__header { width: 115px; } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .version__header { width: $col-md; } @@ -112,7 +112,7 @@ white-space: nowrap; text-overflow: ellipsis; display: none; - @media (min-width: $break-880) { + @media (min-width: $break-sm) { display: table-cell; } } @@ -158,7 +158,7 @@ .last_opened_at__cell { display: none; } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .source__cell { display: table-cell; width: $col-sm; @@ -195,7 +195,7 @@ // table header content responsive styles // NOTE: 990px is a custom breakpoint to deal with responsiveness of the // table controls. - @media (max-width: 990px) { + @media (max-width: $break-md) { thead .name__header { width: $col-md; min-width: 252px; @@ -239,12 +239,12 @@ } .data-table-block .data-table__table { - @media (min-width: $break-990) { + @media (min-width: $break-md) { thead .version__header { width: $col-sm; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { thead { .last_opened_at__header { display: table-cell; @@ -259,7 +259,7 @@ } } - @media (min-width: $break-1500) { + @media (min-width: $break-xl) { thead { .installed_paths__header { display: table-cell; diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 408ea2cbf5..d88be01d9c 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -111,13 +111,13 @@ display: none; width: 0; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .author_name__header { display: table-cell; width: auto; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .author_name__header { width: $col-md; } @@ -141,7 +141,7 @@ } } - @media (max-width: $break-990) { + @media (max-width: $break-md) { .name__cell { .w400 { max-width: calc(400px - 81px); @@ -171,12 +171,12 @@ display: none; max-width: $col-md; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .author_name__cell { display: table-cell; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .updated_at__cell { display: table-cell; } diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index a503e00d47..a3f7b80ad0 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -142,8 +142,6 @@ const QueryForm = ({ storedQuery.author_id === currentUser.id : isAnyTeamMaintainerOrTeamAdmin; - const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer; - const onLoad = (editor: IAceEditor) => { editor.setOptions({ enableLinking: true, @@ -396,7 +394,8 @@ const QueryForm = ({ return null; }; - const renderRunForObserver = ( + // Observers and observer+ of existing query + const renderNonEditableForm = (
    @@ -453,7 +452,10 @@ const QueryForm = ({ ); - const renderForGlobalAdminOrAnyMaintainer = ( + const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer; + + // Global admin, any maintainer, any observer+ on new query + const renderEditableQueryForm = ( <>
    @@ -491,7 +493,7 @@ const QueryForm = ({ Observers can run

    - Users with the Observer role will be able to run this query on + Users with the observer role will be able to run this query on hosts where they have access.

    @@ -580,17 +582,21 @@ const QueryForm = ({ return ; } - if ( - (isOnlyObserver || - isGlobalObserver || - isObserverPlus || - isAnyTeamObserverPlus) && - !isAnyTeamMaintainerOrTeamAdmin - ) { - return renderRunForObserver; + const noEditPermissions = + (isGlobalObserver && !isObserverPlus) || // Global observer but not Observer+ + (isObserverPlus && queryIdForEdit !== 0) || // Global observer+ on existing query + (isOnlyObserver && !isAnyTeamObserverPlus && !isGlobalObserver) || // Only team observer but not team Observer+ + (isAnyTeamObserverPlus && // Team Observer+ on existing query + !isAnyTeamMaintainerOrTeamAdmin && + queryIdForEdit !== 0); + + // Render non-editable form only + if (noEditPermissions) { + return renderNonEditableForm; } - return renderForGlobalAdminOrAnyMaintainer; + // Render default editable form + return renderEditableQueryForm; }; export default QueryForm; diff --git a/frontend/pages/schedule/ManageSchedulePage/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/_styles.scss index 62195284b1..f93847b34b 100644 --- a/frontend/pages/schedule/ManageSchedulePage/_styles.scss +++ b/frontend/pages/schedule/ManageSchedulePage/_styles.scss @@ -83,7 +83,7 @@ .actions__header { width: auto; } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .interval__header { width: 0; } @@ -100,7 +100,7 @@ .actions__cell { width: auto; } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .interval_cell { width: 0; } diff --git a/frontend/pages/software/ManageSoftwarePage/_styles.scss b/frontend/pages/software/ManageSoftwarePage/_styles.scss index a6dbee2bf9..944c41b964 100644 --- a/frontend/pages/software/ManageSoftwarePage/_styles.scss +++ b/frontend/pages/software/ManageSoftwarePage/_styles.scss @@ -37,7 +37,7 @@ margin: 0; margin-bottom: $pad-large; max-width: 75%; - @media (min-width: $break-990) { + @media (min-width: $break-md) { max-width: none; } @@ -97,7 +97,7 @@ .search-field__input-wrapper { width: 100%; } - @media (min-width: $break-768) { + @media (min-width: $break-xs) { width: auto; .search-field__input-wrapper { width: 411px; @@ -139,17 +139,17 @@ width: auto; border-right: 0; } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .vulnerabilities__header { display: table-cell; } } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .version__header { width: $col-md; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .source__header { display: table-cell; } @@ -194,17 +194,17 @@ } } } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .version_cell { width: $col-md; } } - @media (min-width: $break-990) { + @media (min-width: $break-md) { .vulnerabilities__cell { display: table-cell; } } - @media (min-width: $break-1400) { + @media (min-width: $break-lg) { .source__cell { display: table-cell; } diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss index 811a0e0cde..a42e892e85 100644 --- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss +++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/_styles.scss @@ -35,7 +35,7 @@ width: $col-sm; } - @media (max-width: $tooltip-break-1000) { + @media (max-width: $tooltip-break-md) { .cisa_known_exploit__header { .component__tooltip-wrapper__tip-text { max-width: 200px; // Prevents horizontal scrolling off viewport diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 69909599bd..d0e716d5a8 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -53,7 +53,8 @@ import AgentOptionsPage from "pages/admin/TeamManagementPage/TeamDetailsWrapper/ import MacOSUpdates from "pages/ManageControlsPage/MacOSUpdates"; import MacOSSettings from "pages/ManageControlsPage/MacOSSettings"; import MacOSSetup from "pages/ManageControlsPage/MacOSSetup/MacOSSetup"; -import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage"; +import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage"; +import MacOSMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage"; import PATHS from "router/paths"; @@ -138,6 +139,7 @@ const routes = ( + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index b6230e9f98..8afb68c524 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -22,6 +22,7 @@ export default { ADMIN_INTEGRATIONS: `${URL_PREFIX}/settings/integrations`, ADMIN_INTEGRATIONS_TICKET_DESTINATIONS: `${URL_PREFIX}/settings/integrations/ticket-destinations`, ADMIN_INTEGRATIONS_MDM: `${URL_PREFIX}/settings/integrations/mdm`, + ADMIN_INTEGRATIONS_MDM_MAC: `${URL_PREFIX}/settings/integrations/mdm/apple`, ADMIN_INTEGRATIONS_MDM_WINDOWS: `${URL_PREFIX}/settings/integrations/mdm/windows`, ADMIN_INTEGRATIONS_AUTOMATIC_ENROLLMENT: `${URL_PREFIX}/settings/integrations/automatic-enrollment`, ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`, diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index 7057647eea..4036c9b8d5 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -136,3 +136,12 @@ hr { border: none; border-bottom: 1px solid $ui-fleet-black-10; } + +dl { + margin: 0; + padding: 0; +} + +dd { + margin: 0; +} diff --git a/frontend/styles/var/breakpoints.scss b/frontend/styles/var/breakpoints.scss index 4da794622f..6d0fa51fbb 100644 --- a/frontend/styles/var/breakpoints.scss +++ b/frontend/styles/var/breakpoints.scss @@ -1,8 +1,7 @@ -// TODO: Rename these to xs, sm, md, lg, xl, xxl, etc. -$break-1600: 1600px; -$break-1500: 1500px; -$break-1400: 1400px; -$break-990: 990px; -$break-880: 880px; -$break-768: 768px; -$tooltip-break-1000: 1000px; // Prevents horizontal scrolling off viewport +$break-xxl: 1600px; +$break-xl: 1500px; +$break-lg: 1400px; +$break-md: 990px; +$break-sm: 880px; +$break-xs: 768px; +$tooltip-break-md: 1000px; // Prevents horizontal scrolling off viewport diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index 6d28be75df..a7e8215dfa 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -1,4 +1,4 @@ -$min-width: 768px; +$min-width: $break-xs; $medium-width: 1024px; $desktop-width: 1200px; $max-width: 2560px; diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md index e5ed04e03e..a09c029d62 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -14,7 +14,7 @@ We use the Zoom add-on for Google Calendar to schedule Zoom meetings when we cre We configure our Zoom meetings to let participants join before the host starts the meeting. We do this to make sure meetings start on time, even if the host isn't there. #### Internal meeting scheduling -Use the Google Calendar "[Find a meeting time](https://support.google.com/calendar/answer/37161?hl=en&co=GENIE.Platform%3DDesktop#zippy=%2Cfind-a-meeting-time)" feature to coordinate meetings with Fleet team members. Enter the `@fleetdm.com` emails for each +Use the Google Calendar "[Find a meeting time](https://support.google.com/calendar/answer/72143?hl=en&ref_topic=10510646&sjid=7187599067132459840-NA#zippy=%2Cclick-an-empty-time-in-your-calendar)" feature to coordinate meetings with Fleet team members. Enter the `@fleetdm.com` emails for each participant into the "Meet with..." box in Google Calendar, and the calendar availability for each participant will appear in your view. Then, when you select a meeting time, those participants will automatically be invited, and a video conference will be attached to the invite. @@ -96,7 +96,7 @@ Operations will review the expense and reach out to the team member if they have What matters most is your results, which are driven by your focus, your availability to collaborate, and the time and consideration you put into your work. Fleet offers all team members unlimited time off. Whether you're sick, you want to take a trip, you are eager for some time to relax, or you need to get some chores done around the house, any reason is a good reason. For team members working in jurisdictions that require certain mandatory sick leave or PTO policies, Fleet complies to the extent required by law. -### Taking time off +#### Taking time off When you take any time off, you should follow this process: - Let your manager and team know as soon as possible (i.e., post a message in your team's Slack channel with when and how long). - Find someone to cover anything that needs covering while you're out and communicate what they need to take over the responsibilities as well as who to refer to for help (e.g., meetings, planned tasks, unfinished business, important Slack/email threads, anything where someone might be depending on you). @@ -117,6 +117,13 @@ Either way, it's up to you to make sure that your responsibilities are covered, ### New parent leave Fleet gives new parents six weeks of paid leave. After six weeks, if you don't feel ready to return yet, we'll set up a quick call to discuss and work together to come up with a plan to help you return to work gradually or when you're ready. +### Retirement contributions +#### US based team members +Commencing in August 2023, Fleet offers the ability for US based team members to contribute to a 401(k) retirement plan directly from their salary. Team members will be auto-enrolled in our plan with Guideline at a default 1% contribution unless they opt out or change their contribution amount within 30 days of commencement. Fleet currently does not match any contributions made by team members to 401(k) plans. + +#### Non-US team members +Fleet meets the relevant country's retirement contribution requirements for team members outside the US. + ### Relocating When Fleeties relocate, there are vendors that need to be notified of the change. @@ -135,11 +142,6 @@ Fleet's founders [evaluate and update compensation decisions yearly](#workiversa Compensation at Fleet is determined by benchmarking (we use [Pave](https://pave.com)) with role, experience, location, and performance. Annual raises are not guaranteed, particularly when compensation is already strong relative to benchmarks. -## CEO handbook -The [CEO handbook](./ceo-handbook.md) details processes specific to Mike McNeil, CEO of Fleet. - - - ## Team member onboarding ### Before the start date @@ -269,7 +271,7 @@ There are a number of tools that are used throughout Fleet. Some of these tools Here is an overview of a few of the most important and generally-applicable tools we use at Fleet: ### Slack -At Fleet, we do not send internal emails to each other. Instead, we prefer to use Slack to communicate with other folks who work at Fleet. +At Fleet, we do not send internal emails to each other. Instead, we prefer to use [Slack](https://www.linkedin.com/pulse/remote-work-how-set-boundaries-when-office-your-house-lora-vaughn/) to communicate with other folks who work at Fleet. We use threads in Slack as much as possible. Threads help limit noise for other people following the channel and reduce notification overload. We configure our [working hours in Slack](https://slack.com/help/articles/360025054173-Set-up-Slack-for-work-hours-) to make sure everyone knows when they can get in touch with others. @@ -476,7 +478,7 @@ This means that outbound recruiting, 3rd party recruiters, and references from t #### Receiving job applications Every job description page ends with a "call to action", including a link that candidates can click to apply for the job. Fleet replies to all candidates within **1 business day** and always provides either a **rejection** or **decisive next steps**; even if the next step is just a promise. For example: -> "We are still working our way through applications and _still_ have not been able to review yours yet. We think we will be able to review and give you an update about your application update by Thursday at the latest. I'll let you know as soon as I have news. I'll assume we're both still in the running if I don't hear from you, so please let me know if anything comes up." +> "We are still working our way through applications and _still_ have not been able to review yours yet. We think we will be able to review and give you an update about your application by Thursday at the latest. I'll let you know as soon as I have news. I'll assume we're both still in the running if I don't hear from you, so please let me know if anything comes up." When a candidate clicks applies for a job at Fleet, they are taken to a generic Typeform. When they submit their job application, the Typeform triggers a Zapier automation that will posts the submission to `g-business-operations` in Slack. The candidate's job application answers are then forwarded to the applicable `#hiring-xxxxx-202x` Slack channel and the hiring manager is @mentioned. @@ -517,14 +519,10 @@ Here are the steps hiring managers follow to get an offer out to a candidate: - GitHub username _(Every candidate must have a GitHub account in "Fleeties" before the company makes them an offer. If the the candidate does not have a GitHub account, ask them to create one, and make sure it's tracked in "Fleeties".)_ > _**Tip:** A revealing live interview question can be to ask a candidate to quickly share their screen, sign up for GitHub, and then hit the "Edit" button on one of the pages in [the Fleet handbook](https://fleetdm.com/handbook) to make their first pull request. This should not take more than 5 minutes._ 2. **Call references:** Ask the candidate for at least 2+ references and contact each reference in parallel using the instructions and tips in [Fleet's reference check template](https://docs.google.com/document/d/1LMOUkLJlAohuFykdgxTPL0RjAQxWkypzEYP_AT-bUAw/edit?usp=sharing). Be respectful and keep these calls very short. -3. **Schedule CEO interview:** Schedule 30m for the CEO to interview the candidate, if they haven't already done so. - - At Fleet, the CEO interviews every new team member at least once before Fleet extends an offer. (We plan to continue this practice until headcount reaches 100.) - - No need to check with the CEO first. You can just book the meeting on their calendar. - - Schedule the meeting directly on the CEO's calendar during a time they and the candidate are both explicitly available according to that calendar. Available means whitespace. - - Either use Google Calendar directly, or offer to use the CEO's 30m Calendly link. _It is up to you, the hiring manager, to get this meeting scheduled and showing up at a time on the CEO's calendar._ - - _If this is an engineering position_, before the CEO interview, please also be sure that the candidate has already been interviewed by Zach Wasserman. (If not, include Zach in this final interview.) - - The personal email the candidate uses for this calendar event is where they will receive their offer or rejection email. - - Make sure that the agenda doc for the 30m final interview with CEO is in an outline format, located in the "Meeting notes" folder, and contains a discussion point about asking the candidate to verify that 2FA is enabled in their GitHub account. +3. **Schedule CEO interview:** Book a quick chat so our CEO can get to know the future Fleetie. + - No need to check with the CEO first. You can [book the meeting directly](https://fleetdm.com/handbook/business-operations#internal-meeting-scheduling) on the CEO's calendar during a time they and the candidate are both available. + - Set the Google Calendar description of the calendar event to: `Agenda: https://docs.google.com/document/d/1yARlH6iZY-cP9cQbmL3z6TbMy-Ii7lO64RbuolpWQzI/edit`. + - The personal email you use for the candidate in this calendar event is where they will receive their offer or rejection email. 4. **Confirm intent to offer:** Compile feedback about the candidate into a single document and share that document (the "interview packet") with the Head of Business Operations via Google Drive. _This will be interpreted as a signal that you are ready for them to make an offer to this candidate._ - _Compile feedback into a single doc:_ Include feedback from interviews, reference checks, and challenge submissions. Include any other notes you can think of offhand, and embed links to any supporting documents that were impactful in your final decision-making, such as portfolios or challenge submissions. - _Share_ this single document with the Head of Business Operations via email. @@ -676,18 +674,6 @@ Recurring expenses related to a particular team member, such as coworking fees, ## Celebrations - -### Weekly updates -We like to open about milestones and announcements. - - Every Friday, e-group members [report their KPIs for the week](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit) - - Friday nights, Mike McNeil will post a short update in #general including: - - a link to view KPIs - - who was on-call that week - - fleeties who are currently onboarding - - planned hires who haven't started yet - - fleeties who had their lady day that week - - The weekly update uses the format of the previous week's update, which is in the KPIs spreadsheet. The best way to start is by copying and pasting the previous week's upfate and modifying it. - - After posting to #general, the weekly update is also saved in thr KPI spreadsheet. ### Workiversaries We're happy you've ventured a trip around the sun with Fleet. Let's celebrate! @@ -857,7 +843,40 @@ The steps for doing this are highlighted in this loom, TODO. ## Legal -Please submit legal questions and requests to [Business Operations department](https://fleetdm.com/handbook>/business-operations#intake). +Please submit legal questions and requests to [Business Operations department](https://fleetdm.com/handbook/business-operations#intake). +> **Note:** Escalate first-of-its-kind agreements to the CEO. Mike will review business terms and consult with lawyers as necessary. + + +## Getting a contract signed + +If a contract is ready for signature and requires no review or revision, the requestor logins into DocuSign using hello@ from the 1Password vault and routes the agreement to the CEO for signature. + +When a contract is going to be routed for signature by someone outside of Fleet (i.e. the vendor or customer), the requestor is responsible for working with the other party to make sure the document gets routed to the CEO for signature. + +The SLA for contract signature is **2 business days**. Please do not follow up on signature unless this time has elapsed. + +> _**Note:** Signature open time for the CEO is not currently measured, to avoid the overhead of creating separate signature issues to measure open and close time. This may change as signature volume increases._ + +## Getting a contract reviewed + +> If a document is ready for signature and does not need to be reviewed or negotiated, you can skip the review process and use the signature process documented above. + +To get a contract reviewed, upload the agreement to [Google Drive](https://drive.google.com/drive/folders/1G1JTpFxhKZZzmn2L2RppohCX5Bv_CQ9c). + +Complete the [contract review issue template in GitHub](https://fleetdm.com/handbook/business-operations#intake), being sure to include the link to the document you uploaded and using the Calendly link in the issue template to schedule time to discuss the agreement with Nathan Holliday (allowing for sufficient time for him to have reviewed the contract prior to the call). + +Follow up comments should be made in the GitHub issue and in the document itself so it is all in the same place. + +The SLA for contract review is **2 business days**. + +Once the review is complete, the issue will be closed. + +If an agreement requires an additional review during the negotiation process, the requestor will need to follow these steps again. Uploading the new draft and creating a new issue in GitHub. + +When no further review or action is required for an agreement and the document is ready to be signed, the requestor is then responsible for routing the document for signature. + + + ## Taxes and compliance @@ -948,6 +967,7 @@ To make a request of the business operations department, [create an issue using > If you're not sure that your request can wait that long, then please ask for urgent help in our group Slack channel: `#g-business-operations`. Only use this approach or at-mention contributors in business operations directly in urgent situations. Otherwise, create an issue. + ## Slack channels These groups maintain the following [Slack channels](https://fleetdm.com/handbook/company/why-this-way#why-group-slack-channels): @@ -969,5 +989,9 @@ The following stubs are included only to make links backward compatible. Please see [handbook/company#open-positions](https://fleetdm.com/handbook/company#open-positions) for a list of open job postings at Fleet. +##### Weekly updates + +Please see [handbook/company/ceo-handbook#weekly-updates](https://fleetdm.com/handbook/company/ceo-handbook#weekly-updates) + diff --git a/handbook/business-operations/security-policies.md b/handbook/business-operations/security-policies.md index 0a2a800e77..772f91dd32 100644 --- a/handbook/business-operations/security-policies.md +++ b/handbook/business-operations/security-policies.md @@ -150,7 +150,17 @@ Fleet policy requires that: #### Line of Succession -The following order of succession to make sure that decision-making authority for the Fleet Contingency Plan is uninterrupted. The Chief Executive Officer (CEO) is responsible for ensuring the safety of personnel and the execution of procedures documented within this Fleet Contingency Plan. The CTO is responsible for the recovery of Fleet technical environments. If the CEO or Head of Engineering cannot function as the overall authority or choose to delegate this responsibility to a successor, the board of directors shall serve as that authority or choose an alternative delegate. +The following order of succession to make sure that decision-making authority for the Fleet Contingency Plan is uninterrupted. The Chief Executive Officer (CEO) is responsible for ensuring the safety of personnel and the execution of procedures documented within this Fleet Contingency Plan. The CTO is responsible for the recovery of Fleet technical environments. If the CEO or Head of Engineering cannot function as the overall authority or choose to delegate this responsibility to a successor, the board of directors shall serve as that authority or choose an alternative delegate. + +For technical incidents: +1. CTO (Zach Wasserman) +2. Director of Product Engineering (Luke Heath) +3. CEO (Mike McNeil) + +For business/operational incidents: +1. CEO (Mike McNeil) +2. Head of Business Operations (Joanne Stableford) +3. CTO (Zach Wasserman) ### Response Teams and Responsibilities @@ -172,7 +182,7 @@ Current Fleet continuity leadership team members include the CEO and CTO. #### Notification and Activation Phase -This phase addresses the initial actions taken to detect and assess the damage inflicted by a disruption to Fleet Device Management or the Fleet automatic updater service. Based on the assessment of the Event, sometimes, according to the Fleet Incident Response Policy, the Contingency Plan may be activated by either the CEO or CTO. The Contingency Plan may also be triggered by the Head of Security in the event of a cyber disaster. +This phase addresses the initial actions taken to detect and assess the damage inflicted by a disruption to Fleet Device Management. Based on the assessment of the Event, sometimes, according to the Fleet Incident Response Policy, the Contingency Plan may be activated by either the CEO or CTO. The Contingency Plan may also be triggered by the Head of Security in the event of a cyber disaster. The notification sequence is listed below: @@ -193,7 +203,7 @@ The notification sequence is listed below: #### Reconstitution Phase -This section discusses activities necessary for restoring full Fleet automatic updater service operations at the original or new site. The goal is to restore full operations within 24 hours of a disaster or outage. The goal is to provide a seamless transition of operations. +This section discusses activities necessary for restoring full Fleet operations at the original or new site. The goal is to restore full operations within 24 hours of a disaster or outage. The goal is to provide a seamless transition of operations. 1. Contact Partners and Customers affected to begin initial communication - CTO 2. Assess damage to the environment - Infrastructure @@ -208,7 +218,7 @@ This section discusses activities necessary for restoring full Fleet automatic u #### Plan Deactivation -If the Fleet automatic updater environment has been restored, the continuity plan can be deactivated. If the disaster impacted the company and not the service or both, make sure that any leftover systems created temporarily are destroyed. +If the Fleet environment has been restored, the continuity plan can be deactivated. If the disaster impacted the company and not the service or both, make sure that any leftover systems created temporarily are destroyed. ## Data management policy > _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_ @@ -363,6 +373,12 @@ Encryption and key management for local disk encryption of end-user devices foll 4. Transmission encryption keys are limited to use for one year and then must be regenerated. +### Authorized Sub-Processors for Fleet Cloud services + +| Sub-processor Name | Purpose | Location | +| ------------------ | ------- | -------- | +| Amazon Web Services, Inc. and sub-processors located at https://aws.amazon.com/compliance/sub-processors/ | Database hosting platform | USA | + ## Human resources security policy > _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_ @@ -377,7 +393,7 @@ Fleet policy requires all workforce members to comply with the HR Security Polic Fleet policy requires that: -1. Background verification checks on candidates for employees and contractors with production access to the Fleet automatic updater service must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk. +1. Background verification checks on candidates for employees and contractors with production access to the Fleet infrastructure resources must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk. 2. Employees, contractors, and third-party users must agree to and sign the terms and conditions of their employment contract and comply with acceptable use. @@ -397,7 +413,7 @@ Fleet policy requires that: 10. Fleet will publish job descriptions for available positions and conduct interviews to assess a candidate's technical skills as well as soft skills prior to hiring. -11. Background checks of an employee or contractor must be performed by operations and/or the hiring team before we grant the new employee or contractor access to the Fleet automatic updater environment. +11. Background checks of an employee or contractor must be performed by operations and/or the hiring team before we grant the new employee or contractor access to the Fleet production environment. 12. A list of employees and contractors will be maintained, including their titles and managers, and made available to everyone internally. diff --git a/handbook/business-operations/vendor-questionnaires.md b/handbook/business-operations/vendor-questionnaires.md index 654f42cfa2..653196dda7 100644 --- a/handbook/business-operations/vendor-questionnaires.md +++ b/handbook/business-operations/vendor-questionnaires.md @@ -7,18 +7,29 @@ ## Application security +Please also see [Application security](https://fleetdm.com/docs/using-fleet/application-security#application-security) | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Does Fleet use any third party code, including open source code in the development of the scoped application(s)? If yes, please explain. | Yes. All third party code is managed through standard dependency management tools (Go, Yarn, NPM) and audited for vulnerabilities using GitHub vulnerability scanning. | ## Data security +Please also see [Data security](https://fleetdm.com/handbook/business-operations/security-policies#data-management-policy) | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Should the need arise during an active relationship, how can our Data be removed from the Fleet's environment? | Customer data is primarially stored in RDS, S3, and Cloudwatch logs. Deleting these resources will remove the vast majority of customer data. Fleet can take further steps to remove data on demand, including deleting individual records in monitoring systems if requested. | | Does Fleet support secure deletion (e.g., degaussing/cryptographic wiping) of archived and backed-up data as determined by the tenant? | Since all data is encrypted at rest, Fleet's secure deletion practice is to delete the encryption key. Fleet does not host customer services on-premise, so hardware specific deletion methods (such as degaussing) do not apply. | | Does Fleet have a Data Loss Prevention (DLP) solution or compensating controls established to mitigate the risk of data leakage? | In addition to data controls enforced by Google Workspace on corporate endpoints, Fleet applies appropiate security controls for data depending on the requirements of the data, including but not limited to minimum access requirements. | +| Can your organization provide a certificate of data destruction if required? | No, physical media related to a certificate of data destruction is managed by AWS. Media storage devices used to store customer data are classified by AWS as critical and treated accordingly, as high impact, throughout their life-cycles. AWS has exacting standards on how to install, service, and eventually destroy the devices when they are no longer useful. When a storage device has reached the end of its useful life, AWS decommissions media using techniques detailed in NIST 800-88. Media that stored customer data is not removed from AWS control until it has been securely decommissioned. | + +## Service monitoring and logging +| Question | Answer | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| Does your service system/application write/export logs to a SIEM or cloud-based log management solution? | Yes, Fleet Cloud service logs are written to AWS Cloudwatch | +| How are logs managed (stored, secured, retained)? | Alerting triggers manual review of the logs on an as-needed basis. Logs are retained for a period of 30 days by default. Logging access is enabled by IAM rules within AWS. | +| Can Fleet customers access service logs? | Logs will not be accessible by default, but can be provided upon request. | ## Encryption and key management +Please also see [Encryption and key management](https://fleetdm.com/handbook/business-operations/security-policies#encryption-policy) | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Does Fleet have a cryptographic key management process (generation, exchange, storage, safeguards, use, vetting, and replacement), that is documented and currently implemented, for all system components? (e.g. database, system, web, etc.) | All data is encrypted at rest using methods appropiate for the system (ie KMS for AWS based resources). Data going over the internet is encrypted using TLS or other appropiate transport security. | @@ -27,7 +38,13 @@ ## Governance and risk management | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| Does Fleet have documented information security baselines for every component of the infrastructure (e.g., hypervisors, operating systems, routers, DNS servers, etc.)? | YWe follow best practices for the given system. For instance, with AWS we utilize AWS best practices for security including GuardDuty, CloudTrail, etc. | +| Does Fleet have documented information security baselines for every component of the infrastructure (e.g., hypervisors, operating systems, routers, DNS servers, etc.)? | Fleet follows best practices for the given system. For instance, with AWS we utilize AWS best practices for security including GuardDuty, CloudTrail, etc. | + +## Business continuity +Please also see [Business continuity](https://fleetdm.com/handbook/business-operations/security-policies#business-continuity-plan) +| Question | Answer | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| Please provide your application/solution disaster recovery RTO/RPO | RTO and RPO intervals differ depending on the service that is impacted. Please refer to https://fleetdm.com/handbook/business-operations/security-policies#business-continuity-and-disaster-recovery-policy | ## Network security | Question | Answer | @@ -35,6 +52,7 @@ | Does Fleet have the following employed in their production environment? File integrity Monitoring (FIM), Host Intrusion Detection Systems (HIDS), Network Based Indrusion Detection Systems (NIDS), OTHER? | Fleet utilizes several security monitoring solutions depending on the requirements of the system. For instance, given the highly containerized and serverless environment, FIM would not apply. But, we do use tools such as (but not limited to) AWS GuardDuty, AWS CloudTrail, and VPC Flow Logs to actively monitor the security of our environments. | ## Privacy +Please also see [privacy](https://fleetdm.com/legal/privacy) | Question | Answer | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Is Fleet a processor, controller, or joint controller in its relationship with its customer? | Fleet is a processor. | diff --git a/handbook/company/README.md b/handbook/company/README.md index 5534a55515..d48afc4afe 100644 --- a/handbook/company/README.md +++ b/handbook/company/README.md @@ -6,15 +6,15 @@ Fleet Device Management Inc is an [open-core company](https://fleetdm.com/handbo We are dedicated to: -- 🧑‍🚀 automating IT and security with a living, breathing API. -- 🪟 privacy, transparency, and trust through open-source software. -- 💻 a better way to manage computers. +- 🔌 making security and IT interoperable and easy to automate +- 🚪 an inviting (outsider-friendly) way to manage computers +- 🪟 clarity and trust through open-source software ## Culture ### All remote -Fleet Device Management Inc. is an all-remote company with 30+ team members spread across four continents and eight time zones. The broader team of contributors [worldwide](https://github.com/fleetdm/fleet/graphs/contributors) submits patches, bug reports, troubleshooting tips, improvements, and real-world insights to Fleet's open-source code base, documentation, website, and [company handbook](https://fleetdm.com/handbook/company/why-this-way#why-handbook-first-strategy). +Fleet Device Management Inc. is an all-remote company with 40+ team members spread across four continents and nine time zones. The broader team of contributors [worldwide](https://github.com/fleetdm/fleet/graphs/contributors) submits patches, bug reports, troubleshooting tips, improvements, and real-world insights to Fleet's open-source code base, documentation, website, and [company handbook](https://fleetdm.com/handbook/company/why-this-way#why-handbook-first-strategy). ### Open source The majority of the code, documentation, and content we create at Fleet is public and [source-available](https://fleetdm.com/handbook/company/why-this-way#why-open-source). The Fleet handbook is the central guide for how we run the company, and even it is open to the world. We [strive to be open](https://fleetdm.com/handbook/company#openness) and transparent in the way we run the business, as much as [confidentiality](https://fleetdm.com/handbook/company#levels-of-confidentiality) agreements (and time) allow. We perform better with an audience, and our audience performs better with us. @@ -27,16 +27,18 @@ At Fleet, we write things down. Even when we might be wrong. This helps us mov Fleet is currently hiring for the following positions: -- 🚀 Senior Software Engineer (Golang) - [🐋 Head of Public Sector](https://fleetdm.com/handbook/company/head-of-public-sector) - [🐋 Solutions Consultant](https://fleetdm.com/handbook/company/solutions-consultant) +- [🐋 Customer Support Engineer](https://fleetdm.com/handbook/company/customer-support-engineer) +- [🐋 Account Executive](https://fleetdm.com/handbook/company/account-executive) +- [🚀 Software Engineer](https://fleetdm.com/handbook/company/software-engineer) > **🛸 Join us!**  Interested in joining the team at Fleet, or know someone who might be? Click one of the positions to read the job description and apply. Or [copy a direct link to this page](https://fleetdm.com/handbook/company#open-positions) to share a short summary about the company, including our vision, values, history, and all currently open positions. Thank you for the help! ### Is it any good? Here are a few reasons to work at Fleet: -- Work from anywhere with good internet. (We're 100% remote. No office. No commute.) Everyone works remote, but you don't feel remote. There is no 'headquarters'. You are free to travel and move. Organize your workday to fit your lifestyle. Take breaks. Go to the dentist. +- Work from anywhere with good internet. ([We're 100% remote](https://www.linkedin.com/pulse/remote-work-how-set-boundaries-when-office-your-house-lora-vaughn/), No office. No commute.) Everyone works remote, but you don't feel remote. There is no 'headquarters'. You are free to travel and move. Organize your workday to fit your lifestyle. Take breaks. Go to the dentist. - Fleet can offer you a competitive salary, significant equity, and an independent, outsider-friendly culture. Work with helpful, kind, and motivated people who know what they're doing. - At Fleet, we value focus, iteration, and meaningful results – not [60 hour work weeks](https://fleetdm.com/handbook/company#results). We are non-judgmental and laser-focused on growing the company. - Work closely with experienced, well-funded founders and a great team, including the people who created osquery and Sails. We care about openness and transparency. @@ -59,39 +61,39 @@ Empathy leads to smarter decisions. Take an interest in what people are going t - **Assume positive intent.** Think and say [positive things](https://www.theatlantic.com/family/archive/2018/06/mr-rogers-neighborhood-talking-to-kids/562352/), and [assume](https://about.gitlab.com/handbook/values/#assume-positive-intent) others are doing the same. - **Be a helper.** Take care of customers first. But give hospitality and [service with a smile](https://en.m.wikipedia.org/wiki/Fred_Rogers#Legacy) to everyone you can. -- **Roleplay.** Read what you write. [Again.](http://www.paulgraham.com/useful.html) Use your imagination to see situations from different perspectives. -- **Get curious.** Genuinely wonder. Ask questions. Listen closely to the answers. +- **Read what you write.** [Shorten](http://www.paulgraham.com/writing44.html) it. [Repeat](http://www.paulgraham.com/useful.html). +- **Get curious.** Wonder about things. Ask people genuine questions, and listen closely. ### 🟠 Ownership It takes a fully-activated mind to achieve ambitious goals. Think like an owner of the company. -- **Be responsive.** Reply quickly, consistently, whether or not you can take immediate action. Especially GitHub, Slack, and emails. -- **Assume responsibility.** Own up to mistakes. There's no time for finger-pointing, [just fix it](https://about.gitlab.com/handbook/values/#bias-for-action). Follow through on commitments quickly. -- **No one is coming.** Take initiative. Take care of [things that need doing](https://fleetdm.com/handbook/business-operations#spending-company-money), or loop in [the right people](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) fast. Understand [Fleet's goals](https://fleetdm.com/handbook/company#strategy) yourself. Look for [bottlenecks](https://en.wikipedia.org/wiki/Theory_of_constraints). -- **Think long term.** Remember [the big picture](https://fleetdm.com/handbook/company#purpose) beyond your department's goals. +- **Be reliable.** Reply quickly to email, Slack, and GitHub mentions. Arrive in meetings on time. +- **Finish what you start.** Follow through on commitments. Take responsibility for mistakes. There's no time for finger-pointing, [just fix it](https://about.gitlab.com/handbook/values/#bias-for-action). +- **No one is coming.** Take care of [things that need doing](https://fleetdm.com/handbook/business-operations#spending-company-money), or loop in [the right people](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) fast. It's up to you. +- **Think long term.** Understand [Fleet's priorities](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit), beyond your department's goals. Contribute to [the big picture](https://fleetdm.com/handbook/company#purpose). ### 🟢 Results We work to get results. How we work determines what we get. Aim to deliver results daily. -- **Iterate.** [Look for ways](https://youtu.be/BW6TWwNZzIQ) to make smaller changes, more often. Always be cutting scope. But finish what you bite off. -- **Move quickly.** Resist [gold plating](https://en.wikipedia.org/wiki/Gold_plating_(project_management)) and [bike-shedding](https://en.wikipedia.org/wiki/Law_of_triviality). Between overthinking and rushing, there is a [golden mean](https://en.wikipedia.org/wiki/Golden_mean_%28philosophy%29). -- **Keep things simple.** Focus on fewer tasks. Choose ["boring solutions"](https://about.gitlab.com/blog/2020/08/18/boring-solutions-faster-iteration/). Use [fewer words](http://www.paulgraham.com/writing44.html). Avoid preemptive structure. -- **Be realistic.** When you can't take on [more work](http://www.paulgraham.com/hwh.html), clarify your boundaries. Schedule [time off](https://fleetdm.com/handbook/business-operations#taking-time-off) to recharge. Practice self-care. +- **Iterate.** [Look for ways](https://youtu.be/BW6TWwNZzIQ) to make smaller changes, more often. Always publish. +- **Start quickly.** Resist [bike-shedding](https://en.wikipedia.org/wiki/Law_of_triviality). Between overthinking and rushing, there is a [golden mean](https://en.wikipedia.org/wiki/Golden_mean_%28philosophy%29). +- **Keep it simple.** Avoid preemptive structure. Choose ["boring solutions"](https://about.gitlab.com/blog/2020/08/18/boring-solutions-faster-iteration/). +- **Be realistic.** Focus on one or two tasks at a time. When you can't take on [more work](http://www.paulgraham.com/hwh.html), clarify your boundaries. Schedule [time off](https://fleetdm.com/handbook/business-operations#taking-time-off) to recharge. ### 🔵 Objectivity To reach our goals, we need to [see reality clearly](https://en.wikipedia.org/wiki/Intellectual_honesty). -- **Be humble.** You might be wrong. When something isn't working, stop assuming. Experiment with one variable at a time. -- **Seek the truth.** [Change your mind](https://about.gitlab.com/handbook/values/#articulate-when-you-change-your-mind) in the face of new evidence. Escape the [sunk cost fallacy](https://en.wikipedia.org/wiki/Sunk_cost). +- **Assume nothing.** When something isn't working, change only one variable at a time. Find [the bottleneck](https://en.wikipedia.org/wiki/Theory_of_constraints). +- **Change your mind.** [Be willing to reconsider](https://about.gitlab.com/handbook/values/#articulate-when-you-change-your-mind) in the face of new evidence. Escape the [sunk cost fallacy](https://en.wikipedia.org/wiki/Sunk_cost). - **Interrogate luck.** A lucky fix can do more harm than good. Understand why it's broken first. -- **Think for yourself.** Remember how often [conventional wisdom](http://www.paulgraham.com/think.html) isn't. +- **Think for yourself.** Remember how often [conventional wisdom](http://www.paulgraham.com/think.html) isn't. ### 🟣 Openness Take the time to make [yourself](https://fleetdm.com/handbook/business-operations#meetings) and [your work](https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible) visible. This also takes courage. - **Write it down.** Let people [find](https://about.gitlab.com/handbook/values/#findability) and [reproduce](https://about.gitlab.com/handbook/values/#reproducibility) your [decisions](https://fleetdm.com/handbook/company/why-this-way#why-handbook-first-strategy). Remove outdated content so your writing is trustworthy, and [write simply](http://www.paulgraham.com/simply.html) so it is outsider friendly. -- **Everyone can contribute.** Have [short toes](https://about.gitlab.com/handbook/values/#short-toes). Get comfortable letting others contribute to your domain. -- **Be transparent.** Everything we do is [public by default](https://fleetdm.com/handbook/company/why-this-way#why-open-source). Redact [non-public info](https://fleetdm.com/handbook/business-operations#levels-of-confidentiality) carefully. +- **Have short toes.** Everyone can contribute. Get comfortable with [others contributing to your work](https://about.gitlab.com/handbook/values/#short-toes). +- **Public by default.** Everything we do is [public by default](https://fleetdm.com/handbook/company/why-this-way#why-open-source). Redact [non-public info](https://fleetdm.com/handbook/business-operations#levels-of-confidentiality) carefully. - **[Commit](https://www.audible.com/pd/The-15-Commitments-of-Conscious-Leadership-Audiobook/B00SKV11H2) to candor.** Give pointed and respectful feedback, even [when you disagree](https://fleetdm.com/handbook/company/why-this-way#why-this-way). Interrupt and be interrupted. @@ -107,11 +109,12 @@ A few years later, Zach, Mike Arpaia, and [Jason Meller](https://honest.security When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community took over maintenance of the open source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform. ### 2020: Fleet was incorporated -Zach partnered with our CEO, Mike McNeil, to found a new, independent company: Fleet Device Management Inc. In November 2020, we [announced](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) the transition and kicked off the logistics of moving the GitHub repository. +Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/ceo-handbook), to found a new, independent company: Fleet Device Management Inc. In November 2020, we [announced](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) the transition and kicked off the logistics of moving the GitHub repository. ### 2022: Millions of hosts Fleet raised its Series A funding round. The world now has at least 1.65 million computers and virtual hosts enrolled in Fleet, including enterprises, governments, startups, families, and hobbyist racks all over the world. + > 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 @@ -122,12 +125,13 @@ Above and beyond the organizational chart, Fleet organizes cross-functional grou ## Advisors While most improvements at Fleet are driven by informal conversations with customers and open-source contributors, the company also has a few dozen advisors and investors, including -[Sid](https://about.gitlab.com/blog/2022/10/14/one-third-of-what-we-learned-about-ipos-in-taking-gitlab-public/) [Sijbrandij](https://about.gitlab.com/handbook/ceo/#sijbrandij-pronunciation-hint) _(GitLab)_, [Dylan Field](https://en.wikipedia.org/wiki/Dylan_Field) _(Figma)_, [Jack Naglieri](https://councils.forbes.com/profile/Jack-Naglieri-Founder-CEO-Panther/a5f3a285-e983-4f4c-9f9d-2f0d4335f00d) _(Panther Labs)_, [Mike Arpaia](https://www.youtube.com/watch?v=zfCak2UIOD8) _(osquery)_, and [other smart people who are eager to help](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit). If you have a question for one of them, Fleet's CEO is happy to introduce you. (Just ask.) +[Sid](https://about.gitlab.com/blog/2022/10/14/one-third-of-what-we-learned-about-ipos-in-taking-gitlab-public/) [Sijbrandij](https://about.gitlab.com/handbook/ceo/#sijbrandij-pronunciation-hint) _(GitLab)_, [Dylan Field](https://en.wikipedia.org/wiki/Dylan_Field) _(Figma)_, [Mike Arpaia](https://www.youtube.com/watch?v=zfCak2UIOD8) _(osquery)_, and [other smart people who are eager to help](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit). If you have a question for one of them, Fleet's CEO is happy to introduce you. ([Just ask](https://fleetdm.com/handbook/company/ceo-handbook).) ## Strategy You can read about the [company's positioning ("👑 Crown jewels")](https://docs.google.com/document/d/1E0VU4AcB6UTVRd4JKD45Saxh9Gz-mkO3LnGSTBDLEZo/edit#) and [vulnerability management positioning ("👑 Crown jewels pt. 2"](https://docs.google.com/document/d/1VXnZo5EQeircKvUPYai69GPW8QnECamCE5CU1vjLMPU/edit), or review [decks](https://drive.google.com/drive/folders/1cw_lL3_Xu9ZOXKGPghh8F4tc0ND9kQeY) and [recordings](https://us-65885.app.gong.io/conversations?workspace-id=9148397688380544352&callSearch=%7B%22search%22%3A%7B%22type%22%3A%22And%22%2C%22filters%22%3A%5B%7B%22type%22%3A%22CallTitle%22%2C%22phrase%22%3A%22all%20hands%22%7D%5D%7D%7D) from recent company-wide ["All hands" meetings](https://fleetdm.com/handbook/business-operations#all-hands). + ## Slack channels The following Slack channels are maintained by Fleet's founders: @@ -159,6 +163,10 @@ Please see [📖Business Operations#tools-we-use](https://fleetdm.com/handbook/b Please see [📖Company#strategy](#strategy). +##### CEO handbook + +Please see [📖Company#CEO](https://fleetdm.com/handbook/company/ceo-handbook). + diff --git a/handbook/company/account-executive.md b/handbook/company/account-executive.md new file mode 100644 index 0000000000..e6495efea7 --- /dev/null +++ b/handbook/company/account-executive.md @@ -0,0 +1,72 @@ +# 🐋 Account Executive + +## Let's start with why we exist. 📡 + +Ever wondered if your employer is monitoring your work computer? + +Organizations make huge investments every year to keep their laptops and servers online, secure, compliant, and usable from anywhere. This is called "device management". + +At Fleet, we think it's time device management became [transparent](https://fleetdm.com/transparency) and [open source](https://fleetdm.com/handbook/company#open-source). + + +## About the company 🌈 + +You can read more about the company in our [handbook](https://fleetdm.com/handbook/company), which is public and open to the world. + +tldr; Fleet Device Management Inc. is a [recently-funded](https://techcrunch.com/2022/04/28/fleet-nabs-20m-to-enable-enterprises-to-manage-their-devices/) Series A startup founded and backed by the same people who created osquery, the leading open source security agent. Today, osquery is installed on millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community). + + +## Your primary responsibilities 🔭 + +As an Account Executive at Fleet, you will get the chance to… + +- 🎯 Direct and participate in prospecting target companies, identifying key decision makers and influencers, leading when assigned/necessary/appropriate +- 📈 Use available data to identify opportunities and trends with individual prospects +- 📣 Actively promote FleetDM product and services on social media +- 🖥️ Actively present and demonstrate the value of FleetDM products and services and upgrades targeting customer expansion opportunities +- ❔ Appropriately use and follow MEDDPPICC process to qualify and progress opportunities to best help prospects solve problems +- 🤔 Anticipate market trends and identify new opportunities for growth +- 🕴️ Utilize systems and tools such as salesforce to analyze pipeline and opportunity data and keep all information up to date for leadership reporting +- 🚀 Work collaboratively with the product management, customer support, and engineering teams to facilitate feature development based on customer asks +- 🧑‍💻 Collaborate with the marketing team to plan, execute and track impactful marketing campaigns, in order to meet and/or exceed quarterly pipeline and revenue targets +- 🤝 Work with prospects to find win-win commercial agreements + +## Are you our new team member? 🧑‍🚀 + +If most of these qualities sound like you, we would love to chat and see if we're a good fit. + +### You "get it": + +- 🦉 5+ years experience selling to enterprise customers +- 📣 Have excellent communication and interpersonal skills +- 🧑‍💻 Love technology and can explain how things work in detail +- 🧪 Extensive experience with Slack, Salesforce, Zendesk, Google Suite, and GitHub +- ⏩ Thrive in a complex, fast-paced, results driven environment with the ability to pivot to organizational changes easily + +### You can "walk the walk": + +- 🤝 Decisive with the ability to shift gears between thinking and doing +- 📈 Ability to partner with various teams and stakeholders to drive sales +- 👀 Strong understanding of the enterprise procurement process +- ➕ Bonus: Direct experience with Fleet, MDM, osquery or SQL query writing, and working with SRE,CPE, or SecOps teams + +### You can "talk the talk": + +- 💭 You know how to manage complex sales, difficult escalations, and challenging procurement processes with the utmost care and organization +- 💖 You know how to manage your time and priorities between leads, opportunities other day-to-day responsibilities +- ✍ You have the ability to effectively influence key stakeholders, from senior executives to day-to-day engineering contacts, and drive Fleet's value with them +- 🧬 You care about delivering an outstanding customer experience and advocating for the customer's needs within Fleet +- ➕ Bonus: You are comfortable with concepts like security, APIs, and DevOps + +## Why should you join us? 🛸 + +Learn more about the company and [why you should join us here](https://fleetdm.com/handbook/company#is-it-any-good). + + +## Want to join the team? + +Want to join the team? + +Reach out to Alex Mitchell on Linkedin. + + diff --git a/handbook/business-operations/ceo-handbook.md b/handbook/company/ceo-handbook.md similarity index 94% rename from handbook/business-operations/ceo-handbook.md rename to handbook/company/ceo-handbook.md index 2aae507194..626c082e39 100644 --- a/handbook/business-operations/ceo-handbook.md +++ b/handbook/company/ceo-handbook.md @@ -15,13 +15,13 @@ The CEO is the [DRI](https://fleetdm.com/handbook/company/why-this-way#why-direc | Task | Description | Frequency | | ----------------------------------------------------------- | -----------------------------------------------------------------| --------------------- | -| [CEO e-mail management](https://fleetdm.com/handbook/business-operations/ceo-handbook#ceo-email-management)| Triage inbound communications, draft responses, flag actions | Daily, multiple times | +| [CEO e-mail management](#ceo-email-management)| Triage inbound communications, draft responses, flag actions | Daily, multiple times | | General communications [slack channel](https://fleetdm.com/handbook/business-operations#slack-channels) | Triage inbound communications, draft responses, flag actions | Daily, multiple times | -| Schedule internal and external [meetings for the CEO](https://fleetdm.com/handbook/business-operations/ceo-handbook#scheduling-with-the-ceo)| Triage inbound communications, draft responses, flag actions | Daily, multiple times | -| [Preparing agendas and content](https://fleetdm.com/handbook/business-operations/ceo-handbook#document-preparation) for CEO's meetings | Create and edit agenda, provide context, and contact information | PRN | +| Schedule internal and external [meetings for the CEO](#scheduling-with-the-ceo)| Triage inbound communications, draft responses, flag actions | Daily, multiple times | +| [Preparing agendas and content](#document-preparation) for CEO's meetings | Create and edit agenda, provide context, and contact information | PRN | | Expenses for the CEO | Intake expense receipts | PRN | | [Ad-hoc](https://fleetdm.com/handbook/customers#scheduling-a-customer-call) requests from the CEO | Triage requests, prioritize actions, flag actions for further review | PRN | -| Total [travel coordination](https://fleetdm.com/handbook/business-operations/ceo-handbook#travel-preferences) for the CEO | Triage travel request, plan and coordinate flight, stay, and concierge arrangements | PRN | +| Total [travel coordination](#travel-preferences) for the CEO | Triage travel request, plan and coordinate flight, stay, and concierge arrangements | PRN | | Coordinate the [weekly E-Group calendar](https://fleetdm.com/handbook/business-operations#weekly-updates) events for the Executive team | Triage requests, agenda prep, flag actions, follow up | Weekly, PRN | ## CEO preferences @@ -49,7 +49,7 @@ Don't schedule over the Weekly E-group call unless approved by Mike. Add Meeting agendas by copying and pasting the "🗣️Agenda:[link](link)" in the calendar invite description. -Last-minute changes or cancellations must be communicated to Mike via [direct message (DM) only](https://fleetdm.com/handbook/business-operations/ceo-handbook#why-not-mention-the-ceo-in-slack-threads). +Last-minute changes or cancellations must be communicated to Mike via [direct message (DM) only](#why-not-mention-the-ceo-in-slack-threads). - If there is additional context to share, you can cross-post another Slack message as part of your DM. ### CEO email management @@ -57,7 +57,7 @@ Last-minute changes or cancellations must be communicated to Mike via [direct me The Apprentice to the CEO is [responsible](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) for handling all email traffic prior to review. Multiple times daily (minimum 3), The Apprentice will reduce the scope of Mike's inbox to only include necessary and actionable communication. - Marking spam emails as read (same for emails Mike doesn't actually need to read). - Escalate actionable sales communication and update Mike directly. - - Ensure all calendar invites have [necessary documents](https://fleetdm.com/handbook/business-operations/ceo-handbook#document-preparation) included. + - Ensure all calendar invites have [necessary documents](#document-preparation) included. ### Travel preferences Preferences for flights, in descending order of importance are: @@ -145,15 +145,15 @@ Every month the Apprentice will do the prep work for the monthly "✌️ All han The day before the All hands, Mike will prepare slides that reflect the CEO vision and focus. -### After the all hands +### After the All hands -The Apprentice will post a link to the All hands meeting recording and slide deck in Slack. +The Apprentice will post a link to the All hands Gong recording and slide deck in Slack. Template to use: ``` Thanks to everyone who contributed to today's "All hands" call. -:tv: If you weren't able to attend, please [**watch the recording**](Current.link.to.Gong.recording) _(1.5x playback supported)_. +:tv: If you weren't able to attend, please *[watch the recording](Current-link-to-Gong-recording)* _(1.5x playback supported)_. You can also grab a copy of the [original slides](https://fleetdm.com/handbook/business-operations#all-hands) for use in your own confidential presentations. ``` @@ -162,10 +162,10 @@ You can also grab a copy of the [original slides](https://fleetdm.com/handbook/b - To create the recording link: - Open [Gong recording](https://us-65885.app.gong.io/home?workspace-id=9148397688380544352&r=m) and `Share call` - `Share with customers` - - `Copy link` and paste the url `[**Watch the recording**](here.in.your.template.message)`. + - `Copy link` and paste the url `*[Watch the recording](`here-in-your-template-message`)*`. - The PDF can be found in the current months [👋All hands folder](https://drive.google.com/drive/u/0/folders/1cw_lL3_Xu9ZOXKGPghh8F4tc0ND9kQeY) in Google Drive. - - Drag and drop the PDF into your updated Slack message, which will look like this:👇 + - Download the PDF and upload (double click the `+`) into your updated Slack message, which will look like this:👇 ![image](https://github.com/Sampfluger88/fleet/assets/108141731/c2002cfa-a0f6-4349-bb06-71104f6cdce1) diff --git a/handbook/company/customer-support-engineer.md b/handbook/company/customer-support-engineer.md new file mode 100644 index 0000000000..4ccc3ed2b4 --- /dev/null +++ b/handbook/company/customer-support-engineer.md @@ -0,0 +1,67 @@ +# 🐋 Customer Support Engineer + +## Let's start with why we exist. 📡 + +Ever wondered if your employer is monitoring your work computer? + +Organizations make huge investments every year to keep their laptops and servers online, secure, compliant, and usable from anywhere. This is called "device management". + +At Fleet, we think it's time device management became [transparent](https://fleetdm.com/transparency) and [open source](https://fleetdm.com/handbook/company#open-source). + + +## About the company 🌈 + +You can read more about the company in our [handbook](https://fleetdm.com/handbook/company), which is public and open to the world. + +tldr; Fleet Device Management Inc. is a [recently-funded](https://techcrunch.com/2022/04/28/fleet-nabs-20m-to-enable-enterprises-to-manage-their-devices/) Series A startup founded and backed by the same people who created osquery, the leading open source security agent. Today, osquery is installed on millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community). + + +### Your primary responsibilities 🔍 + +at Fleet you will get the chance to… + +In your first 120 days: +- 🏋🏻 Train under our customer support and engineering team to learn the ins and outs of Fleet, frequently asked customer questions, develop and understanding of our troubleshooting guide, and learn how to search through documentation and Fleet repo. +- 🚀 Deploy Fleet on your own to have a better understanding of the customer experience and how the product works. +- ⏫ Work hand-in-hand with the customer success team by participating in calls with customers to discuss any support issues they may have. +- 🥇 Be the first line of defense in customer Slack channels for any reported problems, how-to questions, feature request intake, and bug report filling. + + +### Are you our new team member? 🧑‍🚀 + +If most of these qualities sound like you, we would love to chat and see if we're a good fit. + +### You "get it": + +- 🎯 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. +- 💡 Excellent communication and collaboration skills, with the ability to work closely with customer success, engineering, and product teams +- 👥 A customer-centric mindset, focusing on delivering value and a positive user experience + +### You can "walk the walk": + +- 🤝 Collaboration: You work best in a participatory, team-based environment. +- 🦉 Experience: 2-3 years of work experience supporting Windows, Linux, and MacOS devices in an Enterprise environment. Experience with AWS, SQL, device management, and osquery a bonus. +- 🛠️ Communication: You are outgoing, customer facing, and enjoy problem solving while assisting external stakeholders. +- 🟣 Openness: You are flexible and open to new ideas and ways of working. + +### You can "talk the talk": + +- 💭 Cybersecurity or IT background, experience with device management solutions like Fleet, Intune, Jamf Pro, Workspace One, etc. +- 💖 An excellent understanding of macOS, Windows, Linux and core services like Autopilot, ABM/ASM, + MDM, ADE, APNs, syslog, etc. +- ✍️ Familiarity with SQLite, shell scripting, Python, Powershell, and using Terminal to execute commands or run scripts. + +- 🧑‍🔬 Experience working with Enterprise customers to help resolve complex technical issues. +- ➕ Bonus: Familiarity with GitOps workflows and steps to contribute code in open source projects. + +## Why should you join us? 🛸 + +Learn more about the company and [why you should join us here](https://fleetdm.com/handbook/company#is-it-any-good). + + +## Want to join the team? + +You can [apply for this position here](https://3x3q33auqgj.typeform.com/to/ndA2wMXl). + + + diff --git a/handbook/company/development-groups.md b/handbook/company/development-groups.md index a5102fb4ea..cd3b92541e 100644 --- a/handbook/company/development-groups.md +++ b/handbook/company/development-groups.md @@ -178,7 +178,7 @@ These questions are helpful for the product team when considering what to priori #### Design reviews -Design reviews are [conducted daily by the CEO](https://fleetdm.com/handbook/business-operations/ceo-handbook#calendar-audit). +Design reviews are [conducted daily by the CEO](https://fleetdm.com/handbook/company/ceo-handbook#calendar-audit). The product designer prepares proposed changes in the form of wireframes for this meeting, and presents them quickly. Here are some tips for making this meeting effective: - Bring 1 key engineer who has been helping out with the user story, when possible and helpful. diff --git a/handbook/company/software-engineer.md b/handbook/company/software-engineer.md new file mode 100644 index 0000000000..84bee0ec47 --- /dev/null +++ b/handbook/company/software-engineer.md @@ -0,0 +1,65 @@ +# 🚀 Software Engineer + +## Let's start with why we exist. 📡 + +Ever wondered if your employer is monitoring your work computer? + +Organizations make huge investments every year to keep their laptops and servers online, secure, compliant, and usable from anywhere. This is called "device management". + +At Fleet, we think it's time device management became [transparent](https://fleetdm.com/transparency) and [open source](https://fleetdm.com/handbook/company#open-source). + + +## About the company 🌈 + +You can read more about the company in our [handbook](https://fleetdm.com/handbook/company), which is public and open to the world. + +Fleet Device Management Inc. is a [recently-funded](https://techcrunch.com/2022/04/28/fleet-nabs-20m-to-enable-enterprises-to-manage-their-devices/) Series A startup founded and backed by the same people who created osquery, the leading open source security agent. Today, osquery is installed on millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community). + + +## Your primary responsibilities 🔭 + +At Fleet, you will get the chance to… + +- 🧑‍🔬 Design, develop, test, and maintain a state-of-the-art Golang application that includes robust APIs to support mobile and desktop clients. +- 🛠️ Write code and tests, build prototypes, resolve issues, and profile and analyze bottlenecks. +- 💭 Manage and optimize scalable distributed systems in the cloud. +- 🤝 Collaborate closely with product managers to understand requirements and translate them into actionable specifications. +- 🚀 Actively participate in all engineering scrum meetings, including sprint planning, daily standups, sprint demos, sprint retrospectives, and estimation sessions. +- 🌟 Contribute to the overall success of the [customer cxperience (CX)](https://fleetdm.com/handbook/company/development-groups#customer-experience-group) product group by ensuring users receive valuable new features. + +If most of these qualities sound like you, we would love to chat and see if we're a good fit. + +### You "get it": + +- 🦉 Translate requirements into well-designed and functional software. +- 🤝 Communicate regularly with stakeholders, project managers, quality assurance teams, and other developers regarding progress on long-term technology roadmap. +- 🧪 Collaborate with QA team for testing software features. +- 🏃‍♂️ Familiarity with agile development processes and scrum methodologies. +- 🛠️ Produce quality code, raising the bar for team performance and speed. +- 📖 Mentor junior team members. + +### You can "walk the walk": + +- 🤝 Collaboration: You work best in a participatory, team-based environment. +- 🚀 Prototype-first: You embrace speed and failure as we iterate towards the right solution. You have hands-on experience in creating low and high fidelity prototypes. You’re comfortable accepting suboptimal designs in favor of iteration. +- 🧬 Simplicity: You love complex questions and use your work to simplify that complexity for users. +- 🛠️ Technical: You understand the software development processes. You understand that software quality matters. +- 🟣 Openness: You are flexible and open to new ideas and ways of working. +- ➕ Bonus: Cybersecurity or IT background. + +### You can "talk the talk": + +- 💭 3-5 years' of experience in backend/SaaS development. +- 🦉 Proficient in backend development. You practice OOP design and are comfortable in a lean software development environment. + +## Why should you join us? 🛸 + +Learn more about the company and [why you should join us here](https://fleetdm.com/handbook/company#is-it-any-good). + + +## Want to join the team? + +Want to join the team? + +You can connect with [Luke Heath on LinkedIn](https://www.linkedin.com/in/lukeheath/). + diff --git a/handbook/company/solutions-consultant.md b/handbook/company/solutions-consultant.md index 8e0785409d..7ff7deb71a 100644 --- a/handbook/company/solutions-consultant.md +++ b/handbook/company/solutions-consultant.md @@ -61,7 +61,7 @@ Learn more about the company and [why you should join us here](https://fleetdm.c ## Want to join the team? -You can [apply for this position here](https://3x3q33auqgj.typeform.com/to/upGkhYsN). +You can [apply for this position here](https://3x3q33auqgj.typeform.com/to/ndA2wMXl). diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md index f9a370208b..e6cf6ae4f2 100644 --- a/handbook/company/why-this-way.md +++ b/handbook/company/why-this-way.md @@ -12,7 +12,7 @@ Here are some of Fleet's decisions about the best way to work, and the reasoning Fleet's source code, website, documentation, company handbook, and internal tools are [public](https://github.com/fleetdm/fleet) and accessible to everyone, including engineers, executives, and end users. (Even [paid features](https://fleetdm.com/pricing) are source-available.) -Meanwhile, the [company behind Fleet](https://twitter.com/fleetctl) is built on the [open-core](https://www.heavybit.com/library/video/commercial-open-source-business-strategies) business model. Openness is one of our core [values](https://fleetdm.com/handbook/company#values), and everything we do is public by [default](https://about.gitlab.com/handbook/values/#public-by-default). Even the [company handbook](https://fleetdm.com/handbook) is open to the world. +Meanwhile, the [company behind Fleet](https://twitter.com/fleetctl) is built on the [open-core](https://www.heavybit.com/library/video/commercial-open-source-business-strategies) business model. Openness is one of our core [values](https://fleetdm.com/handbook/company#values), and everything we do is [public by default](https://handbook.gitlab.com/handbook/values/#public-by-default). Even the [company handbook](https://fleetdm.com/handbook) is open to the world. Is open-source collaboration _really_ worth all that? Is it any good? @@ -41,6 +41,20 @@ Making changes to the handbook first [encourages](https://www.youtube.com/watch? To contribute to the handbook, click "Edit this page" and make your [edits in Markdown](https://fleetdm.com/handbook/company). +## Why read documentation? + +There are three reasons for visiting [the docs](https://fleetdm.com/docs): +- **Tire-kicking**: "I think this is cool, now is it something that I could ACTUALLY use? Does it ACTUALLY work? What all's in it? What links can I share with my colleagues to help them see what I'm seeing?" +- **Committed learning**: "I've decided to learn this. I need a curriculum to get me there; with content that makes it as easy as possible, surface-level as possible. I want to learn how Fleet works and how to do all the things." +- **Quick reference**: "Is this thing broken or am I using it right? How do I use this?" Whether they just stumbled in from a search engine, an on-site search, or through the Fleet website navigation, visitors interested in quick reference are interested in getting to the correct answer quickly. Quick referencers search for REST API pages, the config surface of the Fleet server, agent options, how to build YAML for `fleetctl apply`, the built-in MDM profiles, the table schema, the built-in queries, reference architectures and cost calculators for deploying your own Fleet instance. + +Everyone [can contribute](https://fleetdm.com/handbook/company#openness) to Fleet's documentation. Here are a few principles to keep in mind: + +- **🚪 Start simple.** It's easier to learn when you aren't overwhelmed. Good documentation pages and sections start _prescriptive, brief, and clear_; ideally with a short example. You can always hedge and caveat further down the page. This makes the docs more [accessible and outsider-friendly](https://fleetdm.com/handbook/company#purpose). For example, notice how [this page gets more complicated as you scroll down](https://sailsjs.com/documentation/reference/blueprint-api/destroy), or how [both](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?schema) of [these sections](https://sailsjs.com/documentation/concepts/models-and-orm/model-settings#?seldomused-settings) start simple, with caveats pushed down to the end. + + + + ## Why the emphasis on training? Investing in people and providing generous, prioritized training, especially up front, helps contributors understand what is going on at Fleet. By making training a prerequisite at Fleet, we can: - help team members feel confident in the better decisions they make at work. @@ -57,25 +71,15 @@ Here are a few examples of how Fleet prioritizes training: ## Why direct responsibility? Like Apple and GitLab, Fleet uses the concept of [directly responsible individuals (DRIs)](https://about.gitlab.com/handbook/people-group/directly-responsible-individuals/) to know who is responsible for what. -A DRI is a person who is singularly responsble for a given aspect of the open-source project, the product, or the company. A DRI is responsible for making decisions, accomplishing goals, and getting any resources necessary to make a given area of Fleet successful. - -For example, every department maintains its own dedicated [handbook page](https://fleetdm.com/handbook) which is kept up to date with accurate, current information, including the group's [kanban board](https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible), Slack channels, and recurring tasks ("rituals"). Changes are always approved by the DRI [first, before they become real](https://fleetdm.com/handbook/company/why-this-way#why-handbook-first-strategy). - DRIs help us collaborate efficiently by knowing exactly who is responsible and can make decisions about the work they're doing. This saves time by eliminating a requirement for consensus decisions or political presenteeism, enables faster decision-making, and ensures a single individual is aware of what to do next. -### Reporting structure -In addition to Fleet's [organizational chart](https://fleetdm.com/handbook/company#org-chart), the company also organizes [cross-functional product groups](https://fleetdm.com/handbook/company#product-groups) to allow for faster collaboration and fewer roundtrips. +- **What is a DRI?**: A DRI is a person who is singularly responsible for a given aspect of the open-source project, the product, or the company. A DRI is responsible for making decisions, accomplishing goals, and getting any resources necessary to make a given area of Fleet successful. For example, every department maintains its own dedicated [handbook page](https://fleetdm.com/handbook) which is kept up to date with accurate, current information, including the group's [kanban board](https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible), Slack channels, and recurring tasks ("rituals"). +- **Change control**: In keeping with Fleet's handbook-first philosophy and value of writing things down, changes are always approved by the DRI [first, before they become real](https://fleetdm.com/handbook/company/why-this-way#why-handbook-first-strategy). Fleet aims to make picking the right reviewer for your change as easy and automatic as possible. +- **Picking a reviewer**: In most cases, you won't need to select a particular reviewer for your pull request. (It will just happen automatically.) Automatic PR review requests are driven by a combination of [custom repo automation](https://github.com/fleetdm/fleet/pull/12786) and [CODEOWNERS files](https://github.com/search?q=org%3Afleetdm+path%3ACODEOWNERS&type=code). When in doubt, refer to the roles in the company's [cross-functional product groups](https://fleetdm.com/handbook/company#product-groups), and (to a lesser degree) the job titles and reporting structure indicated by the [company's organizational chart](https://fleetdm.com/handbook/company#org-chart). +- **"Maintained by" photo**: For [handbook pages](https://github.com/fleetdm/fleet/tree/main/handbook) and [articles](https://github.com/fleetdm/fleet/tree/main/articles), the "Maintained by" photo displayed on the website corresponds with the `name="maintainedBy"` tags at the very bottom of the raw markdown source for each page. This photo should match the DRI who is auto-requested to approve changes. (It is determined by the person's GitHub profile picture.) +- **Multiple maintainers**: In some cases, multiple subject-matter experts called "maintainers" can merge changes to certain file paths, even though there is already a dedicated DRI configured as the "CODEOWNER". For examples of this, see the auto-approval flows configured as `sails.config.custom.githubRepoMaintainersByPath` and related configuration in [`website/config/custom.js`](https://github.com/fleetdm/fleet/blob/main/website/config/custom.js). -### Reviewers -Fleet aims to make picking the right reviewer for your change as easy and automatic as possible. In many cases, you won't need to select a particular reviewer for your pull request. (It will just happen automatically.) - -To check out the right person to review a given piece of content or source code path, consider: -1. The [CODEOWNERS](https://github.com/fleetdm/fleet/blob/main/CODEOWNERS) files of the fleetdm/fleet and fleetdm/confidential repositories. -2. The `name="maintainedBy"` tags at the very bottom of the raw markdown source for [every handbook page](https://github.com/fleetdm/fleet/tree/main/handbook) and [individual article](https://github.com/fleetdm/fleet/tree/main/articles). -3. The job titles and reporting structure indicated by the [company's organizational chart](https://fleetdm.com/handbook/company#org-chart) and the roles in our [cross-functional product groups](https://fleetdm.com/handbook/company#product-groups). - -> In some cases, multiple subject-matter experts can merge changes to files even though there is a dedicated DRI configured as the "CODEOWNER". For examples of this, see the auto-approval flows configured as `sails.config.custom.githubRepoDRIByPath` and `sails.config.custom.confidentialGithubRepoDRIByPath` in [`website/config/custom.js`](https://github.com/fleetdm/fleet/blob/main/website/config/custom.js). ## Why do we use a wireframe-first approach? @@ -249,5 +253,42 @@ The first step was to add a simpler way to schedule queries, and tuck away the l Packs will always be supported in Fleet. +## Why does Fleet use sentence case? + +Fleet uses sentence case capitalization for all headings, subheadings, button text in the Fleet product, fleetdm.com, the documentation, the handbook, marketing material, direct emails, in Slack, and in every other conceivable situation. + +In sentence case, we write and capitalize words as if they were in sentences: + +> Ask questions about your servers, containers, and laptops running Linux, Windows, and macOS + +As we use sentence case, only the first word is capitalized. But, if a word would normally be capitalized in the sentence (e.g., a proper noun, an acronym, or a stylization) it should remain capitalized. User roles (e.g., "observer" or "maintainer") and features (e.g. "automations") in the Fleet product aren't treated as proper nouns and shouldn't be capitalized. + +The reason for sentence case at Fleet is that everyone capitalizes differently in English, and capitalization conventions have not been taught very consistently in schools. Sentence case simplifies capitalization rules so that contributors can deliver more natural, even-looking content with a voice that feels similar no matter where you're reading it. + +## Why does Fleet use "MDM on/off" instead of "MDM enrolled/unenrolled"? + +Fleet is more than an MDM (mobile device management) solution. + +With Fleet, you can secure and investigate Macs, Windows servers, Chromebooks, and more by installing the fleetd agent (or chrome extension for Chromebooks). When we use the word "enroll" in Fleet, we want this to mean anytime one of these hosts shows up in Fleet and the user can see that sweet telemetry. + +Fleet also has MDM features that allow IT admins to enforce OS settings, OS updates, and more. When we use the phrase "MDM on" in Fleet, it means a host has these features activated. + +Workspace ONE and other MDM solutions use "enroll" to mean both telemetry is being collecting and enforcement features are activated. + +Since Fleet is more than MDM, you can collect telemetry on your Windows servers and you can enforce OS settings on your Macs. Or you can collect telemetry for both without enforcing OS settings. + + + + +#### Stubs +The following stubs are included only so that old links continue to work (for backwards compatibility.) + +##### Reporting structure +Please see [handbook/company/why-this-way#why-direct-responsibility](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility). + +##### Reviewers +Please see [handbook/company/why-this-way#why-direct-responsibility](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility). + + diff --git a/handbook/customers/README.md b/handbook/customers/README.md index 80154d22ca..04f6ff7597 100644 --- a/handbook/customers/README.md +++ b/handbook/customers/README.md @@ -66,9 +66,12 @@ This workflow outlines the process that sales and customer success can follow wh - Schedule the customer onboarding kickoff call - Collect deployment details (if not completed during POC) - Schedule the recurring customer check-in - - Owns running the meeting, note taking, TODO follow up, etc. + - Owns running the meeting, note taking, TODO follow up, etc. +> Due to legislation by the U.S. Department of Commerce, we are unable to initiate business with [certain countries and territories including specific U.S. sanction programs.](https://ofac.treasury.gov/sanctions-programs-and-country-information) + + ## Fleet's W-9 A recent signed copy of Fleet's W-9 form can be found in [this confidential PDF in Google Drive](https://drive.google.com/file/d/1ugXazEBk1oVm_LqGbYNsIFECcv5jXLA9/view?usp=drivesdk). @@ -457,9 +460,8 @@ In order to maintain a consistent contributor experience in Salesforce, we log i 2. Click the accounts tab and check for the following: * The default filter is Customers when you click on the accounts tab. Click on an account to continue. -* Click on a customer and make sure billing address, parent account, LinkedIn company URL, CISO employees (#), employees, and industry appear first at the top of the account. -* "Looking for meeting notes" reminder should appear on the right of the screen. -* Useful links section should include links to Purchase Orders (POs), signed subscription agreements, invoices sent, meeting notes, and signed NDA. Clicking these links should search the appropriate repository for the requested information pertaining to the customer. +* Click on a customer and make sure billing address, parent account, LinkedIn company URL, CISO employees (#), employees, and industry appear first at the top of the account. +* Useful links section should appear in the top right section of the account page. It includes links to purchase orders (POs), signed subscription agreements, invoices sent, meeting notes, and signed NDA. Clicking these links should search the appropriate repository for the requested information pertaining to the customer. All meeting notes should be saved in the [Meeting notes](https://drive.google.com/drive/folders/18e-rVadHG0T5w98OKMngM-yv-K9SXaOq) folder in Google Drive with the account name and date in the title. We do not use the notes feature on "accounts" or "opportunities" in Salesforce. * Additional information section should include fields for account (customer) name first, account rating, LinkedIn sales navigator URL, LinkedIn company URL, and my LinkedIn overlaps. Make sure the LinkedIn links work. * Accounting section should include the following fields: invoice sent (latest), the payment received on (latest), subscription end date (latest), press approval field, license key, total opportunities (#), deals won (#), close date (first deal), cumulative revenue, payment terms, billing address, and shipping address. * Opportunities, meeting notes, and activity feed should appear on the right. @@ -502,8 +504,8 @@ The following table lists the Customer's group's rituals, frequency, and Directl | 🗣️ Product Feature Requests | Weekly | Present and advocate for requests and ideas brought to Fleet's attention by customers that are interesting from a product perspective. | Kathy Satterlee | | Customer meetings | Weekly | Check-in on how product and company are performing, provide updates on new product features or progress on customer requests. These are private meetings with one meeting for each individual commercial customer. | Kathy Satterlee | | Release announcements | Every three weeks | Update customers on new features and resolve issues in an upcoming release. | Kathy Satterlee | -| Sales huddle | Weekly | Agenda: Go through every [open opportunity](https://fleetdm.lightning.force.com/lightning/o/Opportunity/list?filterName=00B4x00000CTHZIEA5) and update the next steps. | Alex Mitchell -[Salesforce contributor experience checkup](#salesforce-contributor-experience-checkups)| Monthly | Make sure all users see a detailed view of contacts, opportunities, accounts, and leads. | Nathan Holliday | +| Opportunity pipeline review | Weekly | Agenda: Go through every [open opportunity](https://fleetdm.lightning.force.com/lightning/o/Opportunity/list?filterName=00B4x00000CTHZIEA5) and update the next steps, amounts, dates, and status (including choosing Closed Lost if no communications for >= 45 days). | Alex Mitchell +[Salesforce contributor experience checkup](#salesforce-contributor-experience-checkups)| Monthly | Make sure all users see a detailed view of contacts, opportunities, accounts, and leads. | Taylor Hughes | | Lead pipeline review | Weekly | Agenda: Review leads by status/stage; make sure SLAs are met. | Alex Mitchell | | Dripify review | Daily | Review responses to Dripify sequencing, respond to standard messages, escalate urgent messages in `#help-CEO`. | Brad Macdowall diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md index 0c47090efc..d2b0476a6b 100644 --- a/handbook/engineering/README.md +++ b/handbook/engineering/README.md @@ -417,14 +417,16 @@ When merging a pull request from a community contributor: ## Changes to tables' schema Whenever a PR is proposed for making changes to our [tables' schema](https://fleetdm.com/tables/screenlock)(e.g. to schema/tables/screenlock.yml), it also has to be reflected in our osquery_fleet_schema.json file. -It should be done by running these commands: + +The website team will [periodically](https://fleetdm.com/handbook/marketing/website-handbook#rituals) update the json file with the latest changes. If the changes should be deployed sooner, you can generate the new json file yourself by running these commands: ``` cd website ./node_modules/sails/bin/sails.js run generate-merged-schema ``` + > When adding a new table, make sure it does not already exist with the same name. If it does, consider changing the new table name or merge the two tables if it makes sense. -> If a table is added to our ChromeOS extension but it does not exist in osquery, add a note that mentions it. As in this [example](https://github.com/fleetdm/fleet/blob/e95e075e77b683167e86d50960e3dc17045e3c44/schema/tables/mdm.yml#L2). +> If a table is added to our ChromeOS extension but it does not exist in osquery or if it is a table added by fleetd, add a note that mentions it. As in this [example](https://github.com/fleetdm/fleet/blob/e95e075e77b683167e86d50960e3dc17045e3c44/schema/tables/mdm.yml#L2). ## Quality @@ -488,7 +490,7 @@ When a new bug is created using the [bug report form](https://github.com/fleetdm At this state, the [bug review DRI](#rituals) (QA) is responsible for going through the inbox and documenting reproduction steps, asking for more reproduction details from the reporter, or asking the product team for more guidance. QA has one week to move the bug to the next step (reproduced). -For community-reported bugs, this may require QA to gather more information from the reporter. QA should reach out to the reporter if more information is needed to reproduce the issue. Reporters have six weeks to provide follow-up information for each report. We'll ping them again as a reminder at three weeks. After six weeks, we'll close the bug to remove it from our visibility, but reporters are welcome to re-open and provide context. +For community-reported bugs, this may require QA to gather more information from the reporter. QA should reach out to the reporter if more information is needed to reproduce the issue. Reporters are encouraged to provide timely follow-up information for each report. At two weeks since last communication QA will ping the reporter for more information on the status of the issue. After four weeks of stale communication QA will close the issue. Reporters are welcome to re-open the closed issue if more investigation is warranted. Once reproduced, QA documents the reproduction steps in the description and moves it to the reproduced state. If QA or the engineering manager feels the bug report may be expected behavior, or if clarity is required on the intended behavior, it is assigned to the group's product manager. [See on GitHub](https://github.com/fleetdm/fleet/issues?q=archived%3Afalse+org%3Afleetdm+is%3Aissue+is%3Aopen+label%3Abug+label%3A%3Areproduce+sort%3Acreated-asc+). @@ -589,10 +591,29 @@ In the above process, any reference to "QA" refers to: Reed Haynes, Product Qual ## Infrastructure +- [Infrastructure links](#infrastructure-links) +- [Best practices](#best-practices) - [24/7 on-call](#24-7-on-call) The [infrastructure product group](https://fleetdm.com/handbook/company/development-groups#infrastructure-group) is responsible for deploying, supporting, and maintaining all Fleet-managed cloud deployments. +### Infrastructure links + +The following are quick links to infrastructure-related README files in both public and private repos that can be used as a quick reference for infrastructure-related code: + +- [Sandbox](https://github.com/fleetdm/fleet/blob/main/infrastructure/sandbox/readme.md) +- [Terraform Module](https://github.com/fleetdm/fleet/blob/main/terraform/README.md) +- [Loadtesting](https://github.com/fleetdm/fleet/blob/main/infrastructure/loadtesting/terraform/readme.md) +- [Cloud](https://github.com/fleetdm/confidential/blob/main/infrastructure/cloud/template/README.md) +- [SSO](https://github.com/fleetdm/confidential/blob/main/infrastructure/sso/README.md) +- [VPN](https://github.com/fleetdm/confidential/blob/main/vpn/README.md) + +### Best practices + +The infrastructure team follows industry best practices when designing and deploying infrastructure. For containerized infrastructure, Google has created a [reference document](https://cloud.google.com/architecture/best-practices-for-operating-containers) as an ideal reference for these practices. + +Many of these practices must be implemented in Fleet directly, and engineering will work to ensure that feature implementation follows these practices. The infrastructure team will make itself available to provide guidance as needed. If a feature is not compatible with these practices, an issue will be created with a request to correct the implementation. + ### 24/7 on-call The 24/7 on-call (aka infrastructure on-call) is responsible for alarms related to fleetdm.com, Fleet sandbox, Fleet managed cloud, as well as delivering 24/7 support for Fleet Ultimate customers. The infrastructure (24/7) on-call responsibility happens in shifts of one week. The people involved in them will be: diff --git a/handbook/marketing/README.md b/handbook/marketing/README.md index 06b179b559..239215d6fa 100644 --- a/handbook/marketing/README.md +++ b/handbook/marketing/README.md @@ -234,7 +234,7 @@ When a new pull request is submitted by a community contributor (someone not a m - Additions or fixes to the Standard Query Library (as long as the SQL works properly and is attributed correctly). - If a review is needed: - - Request a review from the [Product DRI](../people/README.md#directly-responsible-individuals). They should approve extensive changes and new features. Ask in the #g-product channel in Fleet's Slack for more information. + - Request a review from the [Product DRI](../people/README.md#directly-responsible-individuals). They should approve extensive changes and new features. Ask in the [#help-product](https://fleetdm.slack.com/archives/C02A8BRABB5) channel in Fleet's Slack for more information. - Tag the DRI and the contributor in a comment on the PR, letting everyone know why an additional review is needed. Make sure to say thanks! - Find any related open issues and make a note in the comments. @@ -294,7 +294,8 @@ We use Figma for most of our design work. This includes the Fleet product, our w ##### Which file should I use? -**Fleet product** All product design work is done in the [Fleet EE (scratchpad)](https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/%F0%9F%9A%A7-Fleet-EE-(dev-ready%2C-scratchpad)?node-id=9209%3A302838) Figma doc. Check out the [README](https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/%F0%9F%9A%A7-Fleet-EE-(dev-ready%2C-scratchpad)?node-id=2750%3A67203) for how to use this doc. +**Fleet product.** All product design work is done in the [Fleet product](https://www.figma.com/files/project/17318630/%F0%9F%94%9C%F0%9F%93%A6-Fleet-EE%C2%AE-(product)?fuid=1234929285759903870) Figma project. +See [📖Product#Working with Figma](https://fleetdm.com/handbook/product#working-with-figma) for more details. **Fleet website.** All website design work is done in the [fleetdm.com (current, dev-ready)](https://www.figma.com/file/yLP0vJ8Ms4GbCoofLwptwS/%E2%9C%85-fleetdm.com-(current%2C-dev-ready)?node-id=794%3A373) Figma file. diff --git a/handbook/marketing/article-formatting-guide.md b/handbook/marketing/article-formatting-guide.md index 5b304a034f..292a1d2e1f 100644 --- a/handbook/marketing/article-formatting-guide.md +++ b/handbook/marketing/article-formatting-guide.md @@ -80,7 +80,7 @@ Use the following code snippet to include an inline CTA (call to action) in your ``` Struggling with this? It takes some adjustment, and you need repetitions of seeing things written this way and correcting yourself. Many contributors have given the feedback that this opinionated solution is a huge relief once you build the habit of using sentence case capitalization, since it frees up mental capacity in every copywriting situation. You don't have to think as hard, nor choose between flouting and laboriously adhering to the (likely somewhat complex and out of date) styleguide. -> Struggling with this? It takes some adjustment, and you need repetitions of seeing things written this way and correcting yourself. Many contributors have given the feedback that this opinionated solution is a huge relief once you build the habit of using sentence case capitalization, since it frees up mental capacity in every copywriting situation. You don't have to think as hard, nor choose between flouting and laboriously adhering to the (likely somewhat complex and out of date) styleguide. - -> TODO: extrapolate the bulk of this whole sentence-case section to "Why this way", since it applies for every department at Fleet +You can read about why we use sentence case in ["📖Company/Why this way?"](https://fleetdm.com/handbook/company/why-this-way.md#why-does-fleet-use-sentence-case). ### Contractions They’re great! Don’t be afraid to use them. They’ll help your writing sound more approachable. diff --git a/handbook/marketing/website-handbook.md b/handbook/marketing/website-handbook.md index 1523f11908..98fa47a7cf 100644 --- a/handbook/marketing/website-handbook.md +++ b/handbook/marketing/website-handbook.md @@ -66,7 +66,7 @@ Quality assurance (QA) checks must be completed before changes to the website ca ### Manual QA -Before estimating changes to the website, the product manager of the website group is responsible for making sure that manual QA steps have been added to requests. +The product manager of the website group is responsible for making sure that manual QA steps have been added to requests. #### Writing QA steps @@ -229,6 +229,11 @@ TODO: Document. TODO: Document. +## Rituals +| Ritual | Frequency | Description | DRI | +|:-----------------------------|:-------------------------|:----------------------------------------------------|-------------------| +| Generate latest schema | once every 3 weeks | After each sprint, generate the latest tables json file to incorporate any new schema documentation. | Eric Shaw | + diff --git a/handbook/product/README.md b/handbook/product/README.md index 902ff56083..f90e6aa951 100644 --- a/handbook/product/README.md +++ b/handbook/product/README.md @@ -16,24 +16,55 @@ The product team is responsible for product design tasks like drafting [changes At Fleet, like [GitLab](https://about.gitlab.com/handbook/product-development-flow/#but-wait-isnt-this-waterfall) and [other organizations](https://speakerdeck.com/mikermcneil/i-love-apis), every change to the product's UI gets [wireframed first](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach). -* Take the top issue that is assigned to you in the "Prioritized" column of the drafting board. +- Take the top issue that is assigned to you in the "Prioritized" column of the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board). + +- Create a new file inside the [Fleet product](https://www.figma.com/files/project/17318630/%F0%9F%94%9C%F0%9F%93%A6-Fleet-EE%C2%AE-(product)?fuid=1234929285759903870) Figma project. See [Working with Figma](https://fleetdm.com/handbook/product#working-with-figma) below for more details. + +- Use dev notes (component available in our library) to highlight important information to engineers and other teammates. -* Create a page in the [Fleet EE (scratchpad, dev-ready) Figma file](https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/%F0%9F%9A%A7-Fleet-EE-dev-ready%2C-scratchpad?node-id=3923%3A208793) and combine your issue's number and - title to name the Figma page. +- Draft changes to the Fleet product that solve the problem specified in the issue. Constantly place yourself in the shoes of a user while drafting changes. Place these drafts in the appropriate Figma file in Fleet product project. -* Draft changes to the Fleet product that solve the problem specified in the issue. Constantly place - yourself in the shoes of a user while drafting changes. Place these drafts in the appropriate - Figma page in Fleet EE (scratchpad, dev-ready). +- While drafting, reach out to sales, customer success, and marketing for a business perspective. -* While drafting, reach out to sales, customer success, and marketing for a business perspective. +- While drafting, engage engineering to gain insight into technical costs and feasibility. -* While drafting, engage engineering to gain insight into technical costs and feasibility. +### Working with Figma + +#### Create a new file + +When starting a new draft: + +- Create a new file inside the [Fleet product](https://www.figma.com/files/project/17318630/%F0%9F%94%9C%F0%9F%93%A6-Fleet-EE%C2%AE-(product)?fuid=1234929285759903870) project by duplicating "\[TEMPLATE\] Starter file" (pinned to the top of the project). +- Right-click on the duplicated file, select "Share", and ensure **anyone with the link** can view the file. +- Rename each Figma file to include the number and name of the corresponding issue on the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board). (e.g. # 11766 Instructions for Autopilot enrollment). +- The starter file includes 3 predefined pages: Cover, Ready, and Scratchpad. + - **Cover.** This page has a component with issue number, issue name, and status fields. There are 3 statuses: Work in progress, Approved, and Released (the main source of truth is still the drafting board). + - **Ready.** Use this page to communicate designs reviews and development. + - **Scratchpad.** Use this page for work in progress and design that might be useful in the future. + + +#### Keep projects/files clean and up-to-date + +- Once your designs are reviewed and approved, change the status on the cover page of the relevant Figma file and move the issue to the "Designed" column. +- After each release (every 3 weeks) make sure you change the status on the cover page of the relevant Figma files that you worked on during the sprint to "Released". + +#### Questions and missing information + +1. Take a screenshot of the area in Figma +2. Start a thread in the #help-product Slack channel and paste in the screenshot + +Note: Figma does have a commenting system, but it is not easy to search for outstanding concerns and is therefore not preferred. + +For external contributors: please consider opening an issue with reference screenshots if you have a Figma related question you need to resolve. ### Scheduling design reviews - Prepare your draft in the user story issue. - Prepare the agenda for your design review meeting, which should be an empty document other than the proposed changes you will present. -- Review the draft with the CEO at one of the daily design review meetings, or schedule an ad-hoc design review if you need to move faster. (Efficient access to design reviews on-demand [is a priority for Fleet's CEO](https://fleetdm.com/handbook/business-operations/ceo-handbook). Emphasizing design helps us live our [empathy](https://fleetdm.com/handbook/company#empathy) value.) +- Review the draft with the CEO at one of the daily design review meetings, or schedule an ad-hoc design review if you need to move faster. (Efficient access to design reviews on-demand [is a priority for Fleet's CEO](https://fleetdm.com/handbook/company/ceo-handbook). Emphasizing design helps us live our [empathy](https://fleetdm.com/handbook/company#empathy) value.) +- When introducing a story, clarify which review "mode" the CEO should operate in: + + **Final review** mode — you are 70% sure the design is 100% done. + + **Feedback** mode — you know the design is not ready for final review, but would like to get early feedback. - During the review meeting, take detailed notes of any feedback on the draft. - Address the feedback by modifying your draft. - Rinse and repeat at subsequent sessions until there is no more feedback. @@ -43,7 +74,7 @@ At Fleet, like [GitLab](https://about.gitlab.com/handbook/product-development-fl #### Estimating Once the draft has been approved: -* move it to the "Designed" column in the drafting board +* move it to the "Designed" column in the drafting board and assign it to the appropriate engineering manager. * make sure that the issue is updated with the latest information on the work to be done, such as link to the correct page in the Fleet EE (scratchpad) Figma and most recent requirements. Learn https://fleetdm.com/handbook/company/development-groups#making-changes @@ -107,6 +138,28 @@ Sprints are numbered according to the release version. For example, for the spri ### Product design conventions +#### MDM behind-the-frame + +Behind every MDM [wireframe at Fleet](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach), there are 3 foundational design principles: + +- **Use-case first.** Taking advantage of top-level features vs. per-platform options allows us to take advantage of similarities and avoid having two different ways to configure the same thing. +Start off cross-platform for every option, setting, and feature. If we **prove** it's impossible, _then_ work backward making it platform-specific. + +- **Bridge the Mac and Windows gap.** Implement enough help text, links, guides, gifs, etc that a reasonably persistent human being can figure it out just by trying to use the UI. + Even if that means we have fewer features or slightly lower granularity (we can iterate and add more granularity later), Make it easy enough to understand. Whether they're experienced Mac admins people or career Windows folks (even if someone has never used a Windows tool) they should _"get it"_. + +- **Control the noise.** Bring the needs surface level, tuck away things you don't need by default (when possible, given time). For example, hide Windows controls if there are no Windows devices (based on number of Windows hosts). + +##### Wireframes + +- Showing these principles and ideas, to help remember the pros and cons and conceptualize the above visually. + + - Figma: +https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/%F0%9F%9A%A7-Fleet-EE-(dev-ready%2C-scratchpad)?type=design&node-id=17819%3A222919&t=kBHyWO7TXGpkylzS-1 + + + + We have certain design conventions that we include in Fleet. We will document more of these over time. > TODO: Link to style guide here instead, and deduplicate all of this content (or as much as possible). @@ -290,6 +343,9 @@ Every week, a member of the product team (as determined in the [rituals](#ritual If there are changes, the DRI should send a message in the #help-product Slack channel, noting the current versions and whether any of the above has changed. +### New CIS benchmarks +When we create new CIS benchmarks, also submit the new CIS benchmark set to CIS for [certification](https://www.cisecurity.org/cis-securesuite/pricing-and-categories/product-vendor/cis-benchmark-assessment#:~:text=In%20order%20to%20incorporate%20and,recommendations%20in%20the%20associated%20CIS). + ## Rituals Directly Responsible Individuals (DRI) engage in the ritual(s) below at the frequency specified. diff --git a/handbook/product/pricing-features-table.yml b/handbook/product/pricing-features-table.yml index df7684cdce..b60508e9a2 100644 --- a/handbook/product/pricing-features-table.yml +++ b/handbook/product/pricing-features-table.yml @@ -104,7 +104,7 @@ - name: Ship logs to Splunk, Snowflake, and more tier: Free comingSoon: false - - name: Programable audit log + - name: Programmable audit log tier: Premium comingSoon: true - name: Just-in-time (JIT) provisioning @@ -184,4 +184,4 @@ comingSoon: false - name: Managed Cloud tier: Premium - comingSoon: false \ No newline at end of file + comingSoon: false diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index 9f09d843b7..9a491153db 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.33.1" + default = "fleetdm/fleet:v4.34.0" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index cf4f244b9d..8095a8d024 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.33.1" + default = "fleet:v4.34.0" } diff --git a/infrastructure/loadtesting/terraform/loadtesting.tf b/infrastructure/loadtesting/terraform/loadtesting.tf index 5861c2ced7..3d97e39305 100644 --- a/infrastructure/loadtesting/terraform/loadtesting.tf +++ b/infrastructure/loadtesting/terraform/loadtesting.tf @@ -55,6 +55,7 @@ resource "aws_ecs_task_definition" "loadtest" { "-server_url", "http://${aws_lb.internal.dns_name}", "--policy_pass_prob", "0.5", "--start_period", "5m", + "--orbit_prob", "0.0" ] } ]) diff --git a/infrastructure/loadtesting/terraform/terraform.tfvars b/infrastructure/loadtesting/terraform/terraform.tfvars deleted file mode 100644 index a54778f636..0000000000 --- a/infrastructure/loadtesting/terraform/terraform.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -tag = "6f8abe3" -db_instance_type = "db.t4g.medium" -redis_instance_type = "cache.t4g.small" \ No newline at end of file diff --git a/infrastructure/sandbox/JITProvisioner/ingress_destroyer/main.go b/infrastructure/sandbox/JITProvisioner/ingress_destroyer/main.go index 15556a5d74..328a68ae9a 100644 --- a/infrastructure/sandbox/JITProvisioner/ingress_destroyer/main.go +++ b/infrastructure/sandbox/JITProvisioner/ingress_destroyer/main.go @@ -6,18 +6,18 @@ import ( "log" "os" "os/exec" - //"time" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - //autoscaling "k8s.io/client-go/applyconfigurations/autoscaling/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" ) func main() { + log.SetFlags(log.LstdFlags | log.Lshortfile) instanceID := getOrPanic("INSTANCE_ID") ddbTable := getOrPanic("DYNAMODB_LIFECYCLE_TABLE") clusterName := getOrPanic("CLUSTER_NAME") @@ -66,14 +66,25 @@ func deleteIngress(id, name, ddbTable string) { log.Fatal(err) } - /* + // Delete the cronjob so we don't spam the database for stuff that's not running + err = clientset.BatchV1().CronJobs("default").Delete(context.Background(), id, v1.DeleteOptions{}) + if err != nil { + log.Fatal(err) + } + // Scale it down to save money - time.sleep(60) - _, err = clientset.AppsV1().Deployments("default").ApplyScale(context.Background(), id, &autoscaling.ScaleApplyConfiguration{Spec: &autoscaling.ScaleSpecApplyConfiguration{Replicas: new(int32)}}, v1.ApplyOptions{}) + time.Sleep(60) + s, err := clientset.AppsV1().Deployments("default").GetScale(context.Background(), id, v1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + sc := *s + sc.Spec.Replicas = 0 + _, err = clientset.AppsV1().Deployments("default").UpdateScale(context.Background(), id, &sc, v1.UpdateOptions{}) if err != nil { log.Fatal(err) } - */ svc := dynamodb.New(sess) err = updateFleetInstanceState(id, ddbTable, svc) diff --git a/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf b/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf index 097c9a2a85..40bed07ec2 100644 --- a/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf +++ b/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf @@ -206,7 +206,7 @@ resource "random_uuid" "jitprovisioner" { # Use the local to make the trigger work. locals { - fleet_tag = "v4.33.1" + fleet_tag = "v4.34.0" } resource "null_resource" "standard-query-library" { diff --git a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/cronjobs.yaml b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/cronjobs.yaml index fab39e3aee..f7992ac309 100644 --- a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/cronjobs.yaml +++ b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/cronjobs.yaml @@ -35,10 +35,10 @@ spec: resources: limits: cpu: {{ .Values.resources.limits.cpu }} - memory: {{ .Values.resources.limits.memory }} + memory: "2Gi" requests: cpu: {{ .Values.resources.requests.cpu }} - memory: {{ .Values.resources.requests.memory }} + memory: "2Gi" env: ## BEGIN FLEET SECTION - name: FLEET_SERVER_SANDBOX_ENABLED diff --git a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/deployment.yaml b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/deployment.yaml index f9bcd16ae6..137243dd06 100644 --- a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/deployment.yaml +++ b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/fleet/templates/deployment.yaml @@ -62,7 +62,7 @@ spec: value: elasticapm - name: FLEET_LOGGING_TRACING_ENABLED value: "true" - - name: FLEET_VULNERABILITIES_EXTERNAL_SCHEDULED + - name: FLEET_VULNERABILITIES_DISABLE_SCHEDULE value: "true" - name: FLEET_SESSION_DURATION value: "1y" diff --git a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf index ed530ab360..976f4631a0 100644 --- a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf +++ b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf @@ -165,7 +165,7 @@ resource "helm_release" "main" { set { name = "imageTag" - value = "v4.33.1" + value = "v4.34.1" } set { @@ -212,6 +212,16 @@ resource "helm_release" "main" { name = "apm.token" value = var.apm_token } + + set { + name = "resources.limits.memory" + value = "512Mi" + } + + set { + name = "resources.requests.memory" + value = "512Mi" + } } data "aws_iam_policy_document" "main" { diff --git a/mdm_profiles/setup_assistant.json b/mdm_profiles/automatic_enrollment.json similarity index 86% rename from mdm_profiles/setup_assistant.json rename to mdm_profiles/automatic_enrollment.json index 4f96ca2652..b7a6289ee5 100644 --- a/mdm_profiles/setup_assistant.json +++ b/mdm_profiles/automatic_enrollment.json @@ -1,5 +1,5 @@ { - "profile_name": "FleetDM example enrollment profile", + "profile_name": "Fleet's example automatic enrollment profile", "allow_pairing": true, "is_mdm_removable": true, "org_magic": "1", diff --git a/orbit/changes/11980-updates-panic b/orbit/changes/11980-updates-panic new file mode 100644 index 0000000000..11992611e4 --- /dev/null +++ b/orbit/changes/11980-updates-panic @@ -0,0 +1 @@ +* Fixed a crash that happened when updates where disabled and certain conditions (Nudge configuration set or host elegible for MDM migration) were met. diff --git a/orbit/changes/12068-migration-sanity-check b/orbit/changes/12068-migration-sanity-check new file mode 100644 index 0000000000..07cfc6ba38 --- /dev/null +++ b/orbit/changes/12068-migration-sanity-check @@ -0,0 +1 @@ +* Ensure MDM migration modal is not shown, and enrollment commands are not run if the host is already enrolled into Fleet diff --git a/orbit/cmd/desktop/desktop.go b/orbit/cmd/desktop/desktop.go index 232fabc4d7..a6755e11da 100644 --- a/orbit/cmd/desktop/desktop.go +++ b/orbit/cmd/desktop/desktop.go @@ -237,6 +237,7 @@ func main() { ) mdmMigrator = useraction.NewMDMMigrator( swiftDialogPath, + fleetURL, 15*time.Minute, &mdmMigrationHandler{ client: client, diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 2080380716..b82272ec1a 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -618,7 +618,7 @@ func main() { renewEnrollmentProfileCommandFrequency = time.Hour windowsMDMEnrollmentCommandFrequency = time.Hour ) - configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency) + configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL) switch runtime.GOOS { case "darwin": diff --git a/orbit/pkg/augeas/lenses/simplevars.aug b/orbit/pkg/augeas/lenses/simplevars.aug index 1863564956..29d4a2b37f 100644 --- a/orbit/pkg/augeas/lenses/simplevars.aug +++ b/orbit/pkg/augeas/lenses/simplevars.aug @@ -43,6 +43,7 @@ let filter = incl "/etc/kernel-img.conf" . incl "/etc/kerneloops.conf" . incl "/etc/wgetrc" . incl "/etc/zabbix/*.conf" + . incl "/etc/zabbix/**/*.conf" . incl "/etc/audit/auditd.conf" . incl "/etc/mixerctl.conf" . incl "/etc/wsconsctlctl.conf" diff --git a/orbit/pkg/profiles/profiles_darwin.go b/orbit/pkg/profiles/profiles_darwin.go index feba9230d7..8d591bdcec 100644 --- a/orbit/pkg/profiles/profiles_darwin.go +++ b/orbit/pkg/profiles/profiles_darwin.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" "fmt" + "net/url" "os/exec" "github.com/fleetdm/fleet/v4/server/fleet" @@ -52,3 +53,54 @@ var execScript = func(script string) (*bytes.Buffer, error) { } return &outBuf, nil } + +// IsEnrolledIntoMatchingURL runs the `profiles` command to get the current MDM +// enrollment information and reports if the hostname of the MDM server +// supervising the device matches the hostname of the provided URL. +func IsEnrolledIntoMatchingURL(serverURL string) (bool, error) { + out, err := getMDMInfoFromProfilesCmd() + if err != nil { + return false, fmt.Errorf("calling /usr/bin/profiles: %w", err) + } + + // The output of the command is in the form: + // + // ``` + // Enrolled via DEP: No + // MDM enrollment: Yes (User Approved) + // MDM server: https://test.example.com/mdm/apple/mdm + // ``` + // + // If the host is not enrolled into an MDM, the last line is ommitted, + // so we need to check that: + // + // 1. We've got three rows + // 2. The last row matches our server URL + lines := bytes.Split(bytes.TrimSpace(out), []byte("\n")) + if len(lines) < 3 { + return false, nil + } + + parts := bytes.SplitN(lines[2], []byte(":"), 2) + if len(parts) < 2 { + return false, fmt.Errorf("splitting profiles output to get MDM server URL: %w", err) + } + + u, err := url.Parse(string(bytes.TrimSpace(parts[1]))) + if err != nil { + return false, fmt.Errorf("parsing URL from profiles command: %w", err) + } + + fu, err := url.Parse(serverURL) + if err != nil { + return false, fmt.Errorf("parsing provided Fleet URL: %w", err) + } + + return u.Hostname() == fu.Hostname(), nil +} + +// getMDMInfoFromProfilesCmd is declared as a variable so it can be overwritten by tests. +var getMDMInfoFromProfilesCmd = func() ([]byte, error) { + cmd := exec.Command("/usr/bin/profiles", "status", "-type", "enrollment") + return cmd.Output() +} diff --git a/orbit/pkg/profiles/profiles_darwin_test.go b/orbit/pkg/profiles/profiles_darwin_test.go index da53214106..cfdaf315ce 100644 --- a/orbit/pkg/profiles/profiles_darwin_test.go +++ b/orbit/pkg/profiles/profiles_darwin_test.go @@ -69,3 +69,73 @@ func TestGetFleetdConfig(t *testing.T) { } } + +func TestIsEnrolledIntoMatchingURL(t *testing.T) { + fleetURL := "https://valid.com" + cases := []struct { + cmdOut *string + cmdErr error + wantOut bool + wantErr bool + }{ + {nil, errors.New("test error"), false, true}, + {ptr.String(""), nil, false, false}, + {ptr.String(` +Enrolled via DEP: No +MDM enrollment: No + `), nil, false, false}, + { + ptr.String(` +Enrolled via DEP: Yes +MDM enrollment: Yes +MDM server: https://test.example.com + `), + nil, + false, + false, + }, + { + ptr.String(` +Enrolled via DEP: Yes +MDM enrollment: Yes +MDM server / https://test.example.com + `), + nil, + false, + false, + }, + { + ptr.String(` +Enrolled via DEP: Yes +MDM enrollment: Yes +MDM server: https://valid.com/mdm/apple/mdm + `), + nil, + true, + false, + }, + } + + origCmd := getMDMInfoFromProfilesCmd + t.Cleanup(func() { getMDMInfoFromProfilesCmd = origCmd }) + for _, c := range cases { + getMDMInfoFromProfilesCmd = func() ([]byte, error) { + if c.cmdOut == nil { + return nil, c.cmdErr + } + + var buf bytes.Buffer + buf.WriteString(*c.cmdOut) + return []byte(*c.cmdOut), nil + } + + out, err := IsEnrolledIntoMatchingURL(fleetURL) + if c.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, c.wantOut, out) + } + +} diff --git a/orbit/pkg/profiles/profiles_notdarwin.go b/orbit/pkg/profiles/profiles_notdarwin.go index 359a726e0a..39f5758ea0 100644 --- a/orbit/pkg/profiles/profiles_notdarwin.go +++ b/orbit/pkg/profiles/profiles_notdarwin.go @@ -7,3 +7,7 @@ import "github.com/fleetdm/fleet/v4/server/fleet" func GetFleetdConfig() (*fleet.MDMAppleFleetdConfig, error) { return nil, ErrNotImplemented } + +func IsEnrolledIntoMatchingURL(u string) (bool, error) { + return false, ErrNotImplemented +} diff --git a/orbit/pkg/profiles/profiles_notdarwin_test.go b/orbit/pkg/profiles/profiles_notdarwin_test.go index e49ef60549..55c4297751 100644 --- a/orbit/pkg/profiles/profiles_notdarwin_test.go +++ b/orbit/pkg/profiles/profiles_notdarwin_test.go @@ -13,3 +13,9 @@ func TestGetFleetdConfig(t *testing.T) { require.ErrorIs(t, ErrNotImplemented, err) require.Nil(t, config) } + +func TestIsEnrolledIntoMatchingURL(t *testing.T) { + enrolled, err := IsEnrolledIntoMatchingURL("https://test.example.com") + require.ErrorIs(t, ErrNotImplemented, err) + require.False(t, enrolled) +} diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go index bad5526559..b9303e43cc 100644 --- a/orbit/pkg/update/notifications.go +++ b/orbit/pkg/update/notifications.go @@ -5,12 +5,15 @@ import ( "sync" "time" + "github.com/fleetdm/fleet/v4/orbit/pkg/profiles" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/rs/zerolog/log" ) type runCmdFunc func() error +type checkEnrollmentFunc func(url string) (bool, error) + // renewEnrollmentProfileConfigFetcher is a kind of middleware that wraps an // OrbitConfigFetcher and detects if the fleet server sent a notification to // renew the enrollment profile. If so, it runs the command (as root) to @@ -31,13 +34,19 @@ type renewEnrollmentProfileConfigFetcher struct { // runRenewEnrollmentProfile. runCmdFn runCmdFunc + // for tests, to be able to mock the function that checks for Fleet + // enrollment + checkEnrollmentFn checkEnrollmentFunc + // ensures only one command runs at a time, protects access to lastRun cmdMu sync.Mutex lastRun time.Time + + fleetURL string } -func ApplyRenewEnrollmentProfileConfigFetcherMiddleware(fetcher OrbitConfigFetcher, frequency time.Duration) OrbitConfigFetcher { - return &renewEnrollmentProfileConfigFetcher{Fetcher: fetcher, Frequency: frequency} +func ApplyRenewEnrollmentProfileConfigFetcherMiddleware(fetcher OrbitConfigFetcher, frequency time.Duration, fleetURL string) OrbitConfigFetcher { + return &renewEnrollmentProfileConfigFetcher{Fetcher: fetcher, Frequency: frequency, fleetURL: fleetURL} } // GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet @@ -46,17 +55,6 @@ func ApplyRenewEnrollmentProfileConfigFetcherMiddleware(fetcher OrbitConfigFetch func (h *renewEnrollmentProfileConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { cfg, err := h.Fetcher.GetConfig() - // TODO: download and use swiftDialog following the same patterns we - // use for Nudge. - // - // updaterHasTarget := h.UpdateRunner.HasRunnerOptTarget("swiftDialog") - // runnerHasLocalHash := h.UpdateRunner.HasLocalHash("swiftDialog") - // if !updaterHasTarget || !runnerHasLocalHash { - // log.Info().Msg("refreshing the update runner config with swiftDialog targets and hashes") - // log.Debug().Msgf("updater has target: %t, runner has local hash: %t", updaterHasTarget, runnerHasLocalHash) - // return cfg, h.setTargetsAndHashes() - // } - if err == nil && cfg.Notifications.RenewEnrollmentProfile { if h.cmdMu.TryLock() { defer h.cmdMu.Unlock() @@ -67,6 +65,24 @@ func (h *renewEnrollmentProfileConfigFetcher) GetConfig() (*fleet.OrbitConfig, e // updated mdm enrollment). // See https://github.com/fleetdm/fleet/pull/9409#discussion_r1084382455 if time.Since(h.lastRun) > h.Frequency { + // we perform this check locally on the client too to avoid showing the + // dialog if the client has already migrated but the Fleet server + // doesn't know about this state yet. + enrollFn := h.checkEnrollmentFn + if enrollFn == nil { + enrollFn = profiles.IsEnrolledIntoMatchingURL + } + enrolled, err := enrollFn(h.fleetURL) + if err != nil { + log.Error().Err(err).Msg("fetching enrollment status") + return cfg, nil + } + if enrolled { + log.Info().Msg("a request to renew the enrollment profile was processed but not executed because the host is already enrolled into Fleet.") + h.lastRun = time.Now() + return cfg, nil + } + fn := h.runCmdFn if fn == nil { fn = runRenewEnrollmentProfile diff --git a/orbit/pkg/update/notifications_test.go b/orbit/pkg/update/notifications_test.go index 784f5094ea..d0f9cf1b35 100644 --- a/orbit/pkg/update/notifications_test.go +++ b/orbit/pkg/update/notifications_test.go @@ -48,6 +48,9 @@ func TestRenewEnrollmentProfile(t *testing.T) { cmdGotCalled = true return c.cmdErr }, + checkEnrollmentFn: func(url string) (bool, error) { + return false, nil + }, } cfg, err := renewFetcher.GetConfig() @@ -72,15 +75,19 @@ func TestRenewEnrollmentProfilePrevented(t *testing.T) { } var cmdCallCount int + isEnrolled := false chProceed := make(chan struct{}) renewFetcher := &renewEnrollmentProfileConfigFetcher{ Fetcher: fetcher, Frequency: 2 * time.Second, // just to be safe with slow environments (CI) runCmdFn: func() error { - <-chProceed // will be unblocked only when allowed cmdCallCount++ // no need for sync, single-threaded call of this func is guaranteed by the fetcher's mutex return nil }, + checkEnrollmentFn: func(url string) (bool, error) { + <-chProceed // will be unblocked only when allowed + return isEnrolled, nil + }, } assertResult := func(cfg *fleet.OrbitConfig, err error) { @@ -120,6 +127,15 @@ func TestRenewEnrollmentProfilePrevented(t *testing.T) { cfg, err = renewFetcher.GetConfig() assertResult(cfg, err) + // wait for the fetcher's frequency to pass + time.Sleep(renewFetcher.Frequency) + + // this call doesn't execute the command since the host is already + // enrolled + isEnrolled = true + cfg, err = renewFetcher.GetConfig() + assertResult(cfg, err) + require.Equal(t, 2, cmdCallCount) // the initial call and the one after sleep } diff --git a/orbit/pkg/update/nudge.go b/orbit/pkg/update/nudge.go index a45e1fe255..5f5ea4e85f 100644 --- a/orbit/pkg/update/nudge.go +++ b/orbit/pkg/update/nudge.go @@ -71,6 +71,11 @@ func (n *NudgeConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { return nil, nil } + if n.opt.UpdateRunner == nil { + log.Debug().Msg("NudgeConfigFetcher received nil UpdateRunner, this probably indicates that updates are turned off. Skipping any actions related to Nudge") + return cfg, nil + } + if cfg.NudgeConfig == nil { log.Debug().Msg("empty nudge config, removing nudge as target") // TODO(roberto): by early returning and removing the target from the diff --git a/orbit/pkg/update/nudge_test.go b/orbit/pkg/update/nudge_test.go index ece20f8b5c..61ca7bbe6b 100644 --- a/orbit/pkg/update/nudge_test.go +++ b/orbit/pkg/update/nudge_test.go @@ -25,6 +25,29 @@ type nudgeTestSuite struct { withTUF } +func (s *nudgeTestSuite) TestUpdatesDisabled() { + t := s.T() + var err error + cfg := &fleet.OrbitConfig{} + cfg.NudgeConfig, err = fleet.NewNudgeConfig(fleet.MacOSUpdates{MinimumVersion: optjson.SetString("11"), Deadline: optjson.SetString("2022-01-04")}) + require.NoError(t, err) + runNudgeFn := func(execPath, configPath string) error { + return nil + } + var f OrbitConfigFetcher = &dummyConfigFetcher{cfg: cfg} + f = ApplyNudgeConfigFetcherMiddleware(f, NudgeConfigFetcherOptions{ + UpdateRunner: nil, + RootDir: t.TempDir(), + Interval: time.Minute, + runNudgeFn: runNudgeFn, + }) + + // we used to get a panic if updates were disabled (see #11980) + gotCfg, err := f.GetConfig() + require.NoError(t, err) + require.Equal(t, cfg, gotCfg) +} + func (s *nudgeTestSuite) TestNudgeConfigFetcherAddNudge() { t := s.T() tmpDir := t.TempDir() @@ -69,6 +92,8 @@ func (s *nudgeTestSuite) TestNudgeConfigFetcherAddNudge() { // add nuge to the remote s.addRemoteTarget(nudgePath) + // nothing happens if a nil runner is provided + // nudge is added to targets when nudge config is present gotCfg, err = f.GetConfig() require.NoError(t, err) diff --git a/orbit/pkg/update/swift_dialog.go b/orbit/pkg/update/swift_dialog.go index a5f46d5639..ebd0543752 100644 --- a/orbit/pkg/update/swift_dialog.go +++ b/orbit/pkg/update/swift_dialog.go @@ -35,6 +35,11 @@ func (s *SwiftDialogDownloader) GetConfig() (*fleet.OrbitConfig, error) { return nil, nil } + if s.UpdateRunner == nil { + log.Debug().Msg("SwiftDialogDownloader received nil UpdateRunner, this probably indicates that updates are turned off. Skipping any actions related to swiftDialog") + return cfg, nil + } + if !cfg.Notifications.NeedsMDMMigration && !cfg.Notifications.RenewEnrollmentProfile { return cfg, nil } diff --git a/orbit/pkg/update/swift_dialog_test.go b/orbit/pkg/update/swift_dialog_test.go new file mode 100644 index 0000000000..871dc2e93c --- /dev/null +++ b/orbit/pkg/update/swift_dialog_test.go @@ -0,0 +1,21 @@ +package update + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/require" +) + +func TestSwiftDialogUpdatesDisabled(t *testing.T) { + cfg := &fleet.OrbitConfig{} + cfg.Notifications.NeedsMDMMigration = true + cfg.Notifications.RenewEnrollmentProfile = true + var f OrbitConfigFetcher = &dummyConfigFetcher{cfg: cfg} + f = ApplySwiftDialogDownloaderMiddleware(f, nil) + + // we used to get a panic if updates were disabled (see #11980) + gotCfg, err := f.GetConfig() + require.NoError(t, err) + require.Equal(t, cfg, gotCfg) +} diff --git a/orbit/pkg/useraction/mdm_migration_darwin.go b/orbit/pkg/useraction/mdm_migration_darwin.go index 4fedf57553..2a5f36b580 100644 --- a/orbit/pkg/useraction/mdm_migration_darwin.go +++ b/orbit/pkg/useraction/mdm_migration_darwin.go @@ -12,6 +12,7 @@ import ( "text/template" "time" + "github.com/fleetdm/fleet/v4/orbit/pkg/profiles" "github.com/rs/zerolog/log" ) @@ -46,19 +47,31 @@ Please contact your IT admin [here]({{ .ContactURL }}). // swiftDialog. type baseDialog struct { path string + fleetURL string interruptCh chan struct{} } -func newBaseDialog(path string) *baseDialog { - return &baseDialog{path: path, interruptCh: make(chan struct{})} +func newBaseDialog(path, fleetURL string) *baseDialog { + return &baseDialog{path: path, fleetURL: fleetURL, interruptCh: make(chan struct{})} } func (b *baseDialog) CanRun() bool { + // check if swiftDialog has been downloaded if _, err := os.Stat(b.path); err != nil { return false } - return true + // we perform this check locally on the client too to avoid showing the + // dialog if the client has already migrated but the Fleet server + // doesn't know about this state yet. + enrolled, err := profiles.IsEnrolledIntoMatchingURL(b.fleetURL) + if err != nil { + log.Error().Err(err).Msg("fetching enrollment status to show swiftDialog") + return false + } + + // only run the dialog if the host is not enrolled into Fleet + return !enrolled } // Exit sends the interrupt signal to try and stop the current swiftDialog @@ -126,10 +139,10 @@ func (b *baseDialog) render(flags ...string) (chan swiftDialogExitCode, chan err return exitCodeCh, errCh } -func NewMDMMigrator(path string, frequency time.Duration, handler MDMMigratorHandler) MDMMigrator { +func NewMDMMigrator(path, fleetURL string, frequency time.Duration, handler MDMMigratorHandler) MDMMigrator { return &swiftDialogMDMMigrator{ handler: handler, - baseDialog: newBaseDialog(path), + baseDialog: newBaseDialog(path, fleetURL), frequency: frequency, } } @@ -151,8 +164,33 @@ type swiftDialogMDMMigrator struct { intervalMu sync.Mutex } +/** + * Checks in macOS if the user is using dark mode. If we encounter an exit error this is because + * out command returned a non-zero exit code. In this case we can assume the user is NOT using dark + * mode as the "AppleInterfaceStyle" key is only set when dark mode has been set. + * + * More info can be found here: + * https://gist.github.com/jerblack/869a303d1a604171bf8f00bbbefa59c2#file-2-dark-monitor-go-L33-L41 + */ +func isDarkMode() bool { + cmd := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle") + if err := cmd.Run(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + return false + } + } + return true +} + func (m *swiftDialogMDMMigrator) render(message string, flags ...string) (chan swiftDialogExitCode, chan error) { icon := m.props.OrgInfo.OrgLogoURL + + // If the user is using light mode we will set the icon to use the light background logo + if !isDarkMode() { + icon = m.props.OrgInfo.OrgLogoURLLightBackground + } + + // If the user has not set an org logo url, we will use the default fleet logo. if icon == "" { icon = "https://fleetdm.com/images/permanent/fleet-mark-color-40x40@4x.png" } @@ -199,7 +237,6 @@ func (m *swiftDialogMDMMigrator) renderError() (chan swiftDialogExitCode, chan e } func (m *swiftDialogMDMMigrator) renderMigration() error { - var message bytes.Buffer if err := mdmMigrationTemplate.Execute( &message, diff --git a/orbit/pkg/useraction/mdm_migration_notdarwin.go b/orbit/pkg/useraction/mdm_migration_notdarwin.go index 98615a193c..27de1f2c84 100644 --- a/orbit/pkg/useraction/mdm_migration_notdarwin.go +++ b/orbit/pkg/useraction/mdm_migration_notdarwin.go @@ -4,7 +4,7 @@ package useraction import "time" -func NewMDMMigrator(path string, frequency time.Duration, handler MDMMigratorHandler) MDMMigrator { +func NewMDMMigrator(path, fleetURL string, frequency time.Duration, handler MDMMigratorHandler) MDMMigrator { return &NoopMDMMigrator{} } diff --git a/schema/tables/chrome_extensions.yml b/schema/tables/chrome_extensions.yml index 5e6fb7cfbf..b6a93c28ac 100644 --- a/schema/tables/chrome_extensions.yml +++ b/schema/tables/chrome_extensions.yml @@ -105,4 +105,6 @@ columns: platforms: - darwin - windows - - linux \ No newline at end of file + - linux +notes: | + - On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/disk_info.yml b/schema/tables/disk_info.yml index acf35d386b..415048ee2c 100644 --- a/schema/tables/disk_info.yml +++ b/schema/tables/disk_info.yml @@ -46,5 +46,6 @@ columns: platforms: - windows evented: false -notes: >- - - For ChromeOS, this table is not a core osquery table. It is included as part of the Fleetd Chrome extension. Available for Chrome 91+. +notes: | + - On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). + - Available for ChromeOS 91+. diff --git a/schema/tables/geolocation.yml b/schema/tables/geolocation.yml index 708c2f6c6f..e815d535c2 100644 --- a/schema/tables/geolocation.yml +++ b/schema/tables/geolocation.yml @@ -20,3 +20,5 @@ columns: type: text required: false description: Region +notes: | + - This table is not a core osquery table. This table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/mounts.yml b/schema/tables/mounts.yml index 6da6b2e517..c1bb9a3336 100644 --- a/schema/tables/mounts.yml +++ b/schema/tables/mounts.yml @@ -1,10 +1,9 @@ name: mounts examples: >- - If this query returns a 1 in the enabled column, location services are enabled - on this Mac. + Returns the drive free space in gigabytes and as percentage. ``` - SELECT enabled from location_services; + SELECT path, type, ROUND((blocks_available * blocks_size * 10e-10), 2) AS free_gb, ROUND ((blocks_available * 1.0 / blocks * 1.0) * 100, 2) AS free_pc FROM mounts WHERE path = '/'; ``` diff --git a/schema/tables/network_interfaces.yml b/schema/tables/network_interfaces.yml index f6938a6805..b4ed84bcf1 100644 --- a/schema/tables/network_interfaces.yml +++ b/schema/tables/network_interfaces.yml @@ -16,5 +16,6 @@ columns: type: text required: false description: IPv6 address (only available to extensions force-installed by enterprise policy) -notes: >- +notes: | + - This table is not a core osquery table. This table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). - Requires that the fleetd extension is force-installed by enterprise policy diff --git a/schema/tables/npm_packages.yml b/schema/tables/npm_packages.yml index 63380ee6ef..6b483dfca0 100644 --- a/schema/tables/npm_packages.yml +++ b/schema/tables/npm_packages.yml @@ -7,12 +7,9 @@ columns: platforms: - linux examples: >- - List the author, description and more information about packages made by Fleet. Replace the - homepage with any other distributor desired. + List the author, description and more information about the NPM package called `webpack`, if installed: + ```sql + SELECT author, description, directory, version FROM npm_packages WHERE name='webpack'; ``` - - SELECT author, description, directory, version FROM npm_packages WHERE homepage='https://fleetdm.com'; - - ``` - +description: Node.js packages globally installed on a system. diff --git a/schema/tables/os_version.yml b/schema/tables/os_version.yml index 070e5cae86..66797503e4 100644 --- a/schema/tables/os_version.yml +++ b/schema/tables/os_version.yml @@ -22,4 +22,6 @@ columns: - linux - name: mount_namespace_id platforms: - - linux \ No newline at end of file + - linux +notes: | + - On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/osquery_info.yml b/schema/tables/osquery_info.yml index 9a1fa5baed..e84662a1ac 100644 --- a/schema/tables/osquery_info.yml +++ b/schema/tables/osquery_info.yml @@ -54,3 +54,5 @@ examples: >- SELECT version FROM osquery_info; ``` +notes: | + - On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/privacy_preferences.yml b/schema/tables/privacy_preferences.yml index 706c4f7f0f..b8987e019a 100644 --- a/schema/tables/privacy_preferences.yml +++ b/schema/tables/privacy_preferences.yml @@ -1,5 +1,4 @@ name: privacy_preferences -notes: This table is not a core osquery table. It is included as part of the Fleetd Chrome extension. description: Information on Chrome features that can affect a user's privacy, available from the [chrome.privacy APIs](https://developer.chrome.com/docs/extensions/reference/privacy/) platforms: - chrome @@ -85,3 +84,5 @@ columns: description: 1 if enabled else 0 * Available for Chrome 111+ required: false type: integer +notes: | + - This table is not a core osquery table. This table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/screenlock.yml b/schema/tables/screenlock.yml index a2ebb507c2..22da7609db 100644 --- a/schema/tables/screenlock.yml +++ b/schema/tables/screenlock.yml @@ -4,6 +4,7 @@ platforms: - chrome description: >- Returns if the screen locks automatically and the time, in seconds, it takes until the screen is locked automatically while idle. For macOS, this table will return no results if osquery is running as root. -notes: >- +notes: | - For macOS, this only fetches results for osquery's current logged-in user context. The user must also have recently logged in. - - For ChromeOS, this table is not a core osquery table. It is included as part of the Fleetd Chrome extension. Available for Chrome 73+. + - For ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). + - For ChromeOS, this table is only available for Chrome 73+. diff --git a/schema/tables/system_info.yml b/schema/tables/system_info.yml index f0381404cd..e5c16c16b1 100644 --- a/schema/tables/system_info.yml +++ b/schema/tables/system_info.yml @@ -86,3 +86,5 @@ examples: >- SELECT CPU_type, hardware_vendor, hardware_model, hardware_serial FROM system_info; ``` +notes: | + - This table is not a core osquery table. This table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/system_state.yml b/schema/tables/system_state.yml index b89780d7b0..323fd7a023 100644 --- a/schema/tables/system_state.yml +++ b/schema/tables/system_state.yml @@ -17,4 +17,4 @@ columns: required: false evented: false notes: >- - - This table is not a core osquery table. It is included as part of the Fleetd Chrome extension. + - This table is not a core osquery table. This table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/schema/tables/users.yml b/schema/tables/users.yml index e9d7851c3c..574d1d2bf9 100644 --- a/schema/tables/users.yml +++ b/schema/tables/users.yml @@ -60,4 +60,6 @@ columns: platforms: - linux - name: username - description: Username \ No newline at end of file + description: Username +notes: | + - On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos). diff --git a/server/config/config.go b/server/config/config.go index e5b0549ce9..40e4a36c44 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1092,6 +1092,17 @@ func (man Manager) addConfigs() { man.addConfigDuration("mdm.apple_dep_sync_periodicity", 1*time.Minute, "How much time to wait for DEP profile assignment") man.addConfigString("mdm.windows_wstep_identity_cert", "", "Microsoft WSTEP PEM-encoded certificate path") man.addConfigString("mdm.windows_wstep_identity_key", "", "Microsoft WSTEP PEM-encoded private key path") + + // Hide Microsoft/Windows MDM flags as we don't want it to be discoverable for users for now + betaMDMFlags := []string{ + "mdm.windows_wstep_identity_cert", + "mdm.windows_wstep_identity_key", + } + for _, mdmFlag := range betaMDMFlags { + if flag := man.command.PersistentFlags().Lookup(flagNameFromConfigKey(mdmFlag)); flag != nil { + flag.Hidden = true + } + } } // LoadConfig will load the config variables into a fully initialized diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 45c8190b65..a88faa41ed 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1,6 +1,7 @@ package mysql import ( + "bytes" "context" "database/sql" "errors" @@ -80,7 +81,8 @@ SELECT identifier, mobileconfig, created_at, - updated_at + updated_at, + checksum FROM mdm_apple_configuration_profiles WHERE @@ -107,55 +109,6 @@ ORDER BY name` return res, nil } -func (ds *Datastore) MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error) { - // as a special-case, should never be called without at least one hash but if - // so, never matches anything. - if len(hexMD5Hashes) == 0 { - return nil, nil - } - - stmt := ` -SELECT - p1.team_id -FROM - mdm_apple_configuration_profiles p1 -WHERE - p1.identifier NOT IN (?) AND - NOT EXISTS ( - SELECT - 1 - FROM - mdm_apple_configuration_profiles p2 - WHERE - p2.identifier NOT IN (?) AND - p1.team_id = p2.team_id AND - HEX(p2.checksum) NOT IN (?) - ) -GROUP BY - p1.team_id -HAVING - COUNT(*) = ?` - - // when matching a set of profiles to a team, only the custom profiles need - // to match, i.e. we ignore any fleet-specific profiles. - idents := mobileconfig.FleetPayloadIdentifiers() - fleetIdents := make([]string, 0, len(idents)) - for ident := range idents { - fleetIdents = append(fleetIdents, ident) - } - - stmt, args, err := sqlx.In(stmt, fleetIdents, fleetIdents, hexMD5Hashes, len(hexMD5Hashes)) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "prepare query arguments") - } - - var teamIDs []uint - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teamIDs, stmt, args...); err != nil { - return nil, ctxerr.Wrap(ctx, err, "execute query") - } - return teamIDs, nil -} - func (ds *Datastore) GetMDMAppleConfigProfile(ctx context.Context, profileID uint) (*fleet.MDMAppleConfigProfile, error) { stmt := ` SELECT @@ -693,7 +646,7 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic if name := appCfg.MDM.AppleBMDefaultTeam; name != "" { team, err := ds.TeamByName(ctx, name) switch { - case errors.Is(err, sql.ErrNoRows): + case fleet.IsNotFound(err): level.Debug(ds.logger).Log( "msg", "ingesting devices from DEP: unable to find default team assigned in config, the devices won't be assigned to a team", @@ -965,11 +918,11 @@ func (ds *Datastore) UpdateHostTablesOnMDMUnenroll(ctx context.Context, uuid str return ctxerr.Wrap(ctx, err, "getting host id from UUID") } + // NOTE: set installed_from_dep = 0 so DEP host will not be counted as pending after it unrolls _, err = tx.ExecContext(ctx, ` - DELETE FROM host_mdm - WHERE host_id = ?`, hostID) + UPDATE host_mdm SET enrolled = 0, installed_from_dep = 0, server_url = '', mdm_id = NULL WHERE host_id = ?`, hostID) if err != nil { - return ctxerr.Wrap(ctx, err, "removing host_mdm rows for host") + return ctxerr.Wrap(ctx, err, "clearing host_mdm for host") } // Since the host is unenrolled, delete all profiles assigned to the @@ -1158,6 +1111,31 @@ ON DUPLICATE KEY UPDATE }) } +func (ds *Datastore) BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, profs []*fleet.MDMAppleProfilePayload) error { + return ds.withTx(ctx, func(tx sqlx.ExtContext) error { + return bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, profs) + }) +} + +func bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context, tx sqlx.ExtContext, profs []*fleet.MDMAppleProfilePayload) error { + if len(profs) == 0 { + return nil + } + + var args []any + var argStr strings.Builder + for _, p := range profs { + args = append(args, p.ProfileIdentifier, p.HostUUID) + argStr.WriteString("(?, ?),") + } + + stmt := fmt.Sprintf(`DELETE FROM host_mdm_apple_profiles WHERE (profile_identifier, host_uuid) IN (%s)`, strings.Trim(argStr.String(), ",")) + if _, err := tx.ExecContext(ctx, stmt, args...); err != nil { + return ctxerr.Wrap(ctx, err, "error executing query") + } + return nil +} + // 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. @@ -1239,7 +1217,7 @@ WHERE return nil } - const profilesToInstallStmt = ` + const desiredStateStmt = ` SELECT ds.profile_id as profile_id, ds.host_uuid as host_uuid, @@ -1268,35 +1246,28 @@ WHERE -- profiles in A and B but with operation type "remove" ( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) )` - stmt, args, err := sqlx.In(profilesToInstallStmt, - uuids, fleet.MDMAppleOperationTypeRemove, - ) + stmt, args, err := sqlx.In(desiredStateStmt, uuids, fleet.MDMAppleOperationTypeRemove) if err != nil { return ctxerr.Wrap(ctx, err, "building profiles to install statement") } - var profilesToInstall []*fleet.MDMAppleProfilePayload - err = sqlx.SelectContext(ctx, tx, &profilesToInstall, stmt, args...) + var wantedProfiles []*fleet.MDMAppleProfilePayload + err = sqlx.SelectContext(ctx, tx, &wantedProfiles, stmt, args...) if err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute") } - installIdentifiers := []string{} - identifierToHosts := map[string][]string{} - for _, p := range profilesToInstall { - installIdentifiers = append(installIdentifiers, p.ProfileIdentifier) - if _, ok := identifierToHosts[p.ProfileIdentifier]; !ok { - identifierToHosts[p.ProfileIdentifier] = []string{} - } - identifierToHosts[p.ProfileIdentifier] = append(identifierToHosts[p.ProfileIdentifier], p.HostUUID) - } - profilesToRemoveStmt := ` + const currentStateStmt = ` SELECT hmap.profile_id as profile_id, hmap.host_uuid as host_uuid, hmap.profile_identifier as profile_identifier, hmap.profile_name as profile_name, - hmap.checksum as checksum + hmap.checksum as checksum, + hmap.status as status, + hmap.operation_type as operation_type, + hmap.detail as detail, + hmap.command_uuid as command_uuid FROM ( SELECT h.uuid, macp.profile_id @@ -1313,79 +1284,92 @@ WHERE AND ds.profile_id IS NULL AND ds.uuid IS NULL -- except "remove" operations in any state AND ( hmap.operation_type IS NULL OR hmap.operation_type != ? ) - -- profiles that are being installed - ` + ` - inArgs := []any{uuids, uuids, fleet.MDMAppleOperationTypeRemove} - if len(installIdentifiers) > 0 { - profilesToRemoveStmt += `AND hmap.profile_identifier NOT IN (?)` - inArgs = append(inArgs, installIdentifiers) - - } - - stmt, args, err = sqlx.In(profilesToRemoveStmt, inArgs...) + stmt, args, err = sqlx.In(currentStateStmt, uuids, uuids, fleet.MDMAppleOperationTypeRemove) if err != nil { return ctxerr.Wrap(ctx, err, "building profiles to remove statement") } - var profilesToRemove []*fleet.MDMAppleProfilePayload - err = sqlx.SelectContext(ctx, tx, &profilesToRemove, stmt, args...) + var currentProfiles []*fleet.MDMAppleProfilePayload + err = sqlx.SelectContext(ctx, tx, ¤tProfiles, stmt, args...) if err != nil { - return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute") + return ctxerr.Wrap(ctx, err, "fetching profiles to remove") } - if len(profilesToInstall) == 0 && len(profilesToRemove) == 0 { + if len(wantedProfiles) == 0 && len(currentProfiles) == 0 { return nil } - // before doing the inserts, remove profiles with identifiers that will be re-sent - if len(profilesToInstall) > 0 { - var dargs []any - var dsb strings.Builder - for identifier, hostUUIDs := range identifierToHosts { - for _, hostUUID := range hostUUIDs { - dargs = append(dargs, hostUUID, identifier) - dsb.WriteString("(?,?),") - } - } - stmt = fmt.Sprintf(`DELETE FROM host_mdm_apple_profiles WHERE (host_uuid, profile_identifier) IN(%s)`, strings.TrimSuffix(dsb.String(), ",")) - _, err = tx.ExecContext(ctx, stmt, dargs...) - if err != nil { - return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute") - } + // delete all host profiles to start from a clean slate, new entries will be added next + // TODO(roberto): is this really necessary? this was pre-existing + // behavior but I think it can be refactored. For now leaving it as-is. + if err := bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, wantedProfiles); err != nil { + return err } + // profileIntersection tracks profilesToAdd ∩ profilesToRemove, this is used to avoid: + // + // - Sending a RemoveProfile followed by an InstallProfile for a + // profile with an identifier that's already installed, which can cause + // racy behaviors. + // - Sending a InstallProfile command for a profile that's exactly the + // same as the one installed. Customers have reported that sending the + // command causes unwanted behavior. + profileIntersection := apple_mdm.NewProfileBimap() + profileIntersection.IntersectByIdentifierAndHostUUID(wantedProfiles, currentProfiles) + var pargs []any var psb strings.Builder - for _, p := range profilesToInstall { - pargs = append(pargs, p.ProfileID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, fleet.MDMAppleOperationTypeInstall, nil, "") - psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?),") + for _, p := range wantedProfiles { + if pp, ok := profileIntersection.GetMatchingProfileInCurrentState(p); ok { + if pp.Status != &fleet.MDMAppleDeliveryFailed && bytes.Equal(pp.Checksum, p.Checksum) { + pargs = append(pargs, p.ProfileID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + pp.OperationType, pp.Status, pp.CommandUUID, pp.Detail) + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") + continue + } + } + pargs = append(pargs, p.ProfileID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + fleet.MDMAppleOperationTypeInstall, nil, "", "") + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") } - for _, p := range profilesToRemove { - pargs = append(pargs, p.ProfileID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, fleet.MDMAppleOperationTypeRemove, nil, "") - psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?),") + hostProfilesToClean := []*fleet.MDMAppleProfilePayload{} + for _, p := range currentProfiles { + if _, ok := profileIntersection.GetMatchingProfileInDesiredState(p); ok { + hostProfilesToClean = append(hostProfilesToClean, p) + continue + } + pargs = append(pargs, p.ProfileID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum, + fleet.MDMAppleOperationTypeRemove, nil, "", "") + psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),") + } + + if err := bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, hostProfilesToClean); err != nil { + return err } baseStmt := fmt.Sprintf(` -INSERT INTO host_mdm_apple_profiles ( - profile_id, - host_uuid, - profile_identifier, - profile_name, - checksum, - operation_type, - status, - command_uuid -) -VALUES %s -ON DUPLICATE KEY UPDATE - operation_type = VALUES(operation_type), - status = VALUES(status), - command_uuid = VALUES(command_uuid), - checksum = VALUES(checksum), - detail = '' -`, strings.TrimSuffix(psb.String(), ",")) + INSERT INTO host_mdm_apple_profiles ( + profile_id, + host_uuid, + profile_identifier, + profile_name, + checksum, + operation_type, + status, + command_uuid, + detail + ) + VALUES %s + ON DUPLICATE KEY UPDATE + operation_type = VALUES(operation_type), + status = VALUES(status), + command_uuid = VALUES(command_uuid), + checksum = VALUES(checksum), + detail = VALUES(detail) + `, strings.TrimSuffix(psb.String(), ",")) _, err = tx.ExecContext(ctx, baseStmt, pargs...) return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute") @@ -1423,7 +1407,12 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee // state (failed or verified). If the profile's content is edited, all relevant hosts will // be marked as status NULL so that it gets re-installed. query := ` - SELECT ds.profile_id, ds.host_uuid, ds.profile_identifier, ds.profile_name, ds.checksum + SELECT + ds.profile_id, + ds.host_uuid, + ds.profile_identifier, + ds.profile_name, + ds.checksum FROM ( SELECT macp.profile_id, @@ -1473,7 +1462,16 @@ func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet // processed by the ListMDMAppleProfilesToInstall method (since they are in // both, their desired state is necessarily to be installed). query := ` - SELECT hmap.profile_id, hmap.profile_identifier, hmap.profile_name, hmap.host_uuid, hmap.checksum + SELECT + hmap.profile_id, + hmap.profile_identifier, + hmap.profile_name, + hmap.host_uuid, + hmap.checksum, + hmap.operation_type, + hmap.detail, + hmap.status, + hmap.command_uuid FROM ( SELECT h.uuid, macp.profile_id FROM mdm_apple_configuration_profiles macp @@ -1554,6 +1552,9 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload status = VALUES(status), operation_type = VALUES(operation_type), detail = VALUES(detail), + checksum = VALUES(checksum), + profile_identifier = VALUES(profile_identifier), + profile_name = VALUES(profile_name), command_uuid = VALUES(command_uuid)`, strings.TrimSuffix(sb.String(), ","), ) @@ -2207,6 +2208,12 @@ func (ds *Datastore) GetMDMAppleBootstrapPackageBytes(ctx context.Context, token } func (ds *Datastore) GetMDMAppleBootstrapPackageSummary(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackageSummary, error) { + // NOTE: Consider joining on host_dep_assignments instead of host_mdm so DEP hosts that + // manually enroll or re-enroll are included in the results so long as they are not unassigned + // in Apple Business Manager. The problem with using host_dep_assignments is that a host can be + // assigned to Fleet in ABM but still manually enroll. We should probably keep using host_mdm, + // but be better at updating the table with the right values when a host enrolls (perhaps adding + // a query param to the enroll endpoint). stmt := ` SELECT COUNT(IF(ncr.status = 'Acknowledged', 1, NULL)) AS installed, @@ -2238,6 +2245,12 @@ func (ds *Datastore) RecordHostBootstrapPackage(ctx context.Context, commandUUID } func (ds *Datastore) GetHostMDMMacOSSetup(ctx context.Context, hostID uint) (*fleet.HostMDMMacOSSetup, error) { + // NOTE: Consider joining on host_dep_assignments instead of host_mdm so DEP hosts that + // manually enroll or re-enroll are included in the results so long as they are not unassigned + // in Apple Business Manager. The problem with using host_dep_assignments is that a host can be + // assigned to Fleet in ABM but still manually enroll. We should probably keep using host_mdm, + // but be better at updating the table with the right values when a host enrolls (perhaps adding + // a query param to the enroll endpoint). stmt := ` SELECT CASE diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 0e00c8b0d1..20946c31f8 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -62,7 +62,6 @@ func TestMDMApple(t *testing.T) { {"TestMDMAppleDefaultSetupAssistant", testMDMAppleDefaultSetupAssistant}, {"TestSetVerifiedMacOSProfiles", testSetVerifiedMacOSProfiles}, {"TestMDMAppleConfigProfileHash", testMDMAppleConfigProfileHash}, - {"TestMatchMDMAppleConfigProfiles", testMatchMDMAppleConfigProfiles}, {"TestResetMDMAppleEnrollment", testResetMDMAppleEnrollment}, {"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments}, } @@ -149,7 +148,7 @@ func testListMDMAppleConfigProfiles(t *testing.T, ds *Datastore) { cps, err := ds.ListMDMAppleConfigProfiles(ctx, nil) require.NoError(t, err) require.Len(t, cps, 1) - checkConfigProfile(t, *expectedTeam0[0], *cps[0]) + checkConfigProfileWithChecksum(t, *expectedTeam0[0], *cps[0]) // add fleet-managed profiles for the team and globally for idf := range mobileconfig.FleetPayloadIdentifiers() { @@ -167,7 +166,7 @@ func testListMDMAppleConfigProfiles(t *testing.T, ds *Datastore) { cps, err = ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(1)) require.NoError(t, err) require.Len(t, cps, 1) - checkConfigProfile(t, *expectedTeam1[0], *cps[0]) + checkConfigProfileWithChecksum(t, *expectedTeam1[0], *cps[0]) // add another profile with team id 1 cp, err = ds.NewMDMAppleConfigProfile(ctx, *generateCP("another_name1", "another_identifier1", 1)) @@ -180,9 +179,9 @@ func testListMDMAppleConfigProfiles(t *testing.T, ds *Datastore) { for _, cp := range cps { switch cp.Name { case "name1": - checkConfigProfile(t, *expectedTeam1[0], *cp) + checkConfigProfileWithChecksum(t, *expectedTeam1[0], *cp) case "another_name1": - checkConfigProfile(t, *expectedTeam1[1], *cp) + checkConfigProfileWithChecksum(t, *expectedTeam1[1], *cp) default: t.FailNow() } @@ -243,12 +242,17 @@ func storeDummyConfigProfileForTest(t *testing.T, ds *Datastore) *fleet.MDMApple return storedCP } -func checkConfigProfile(t *testing.T, expected fleet.MDMAppleConfigProfile, actual fleet.MDMAppleConfigProfile) { +func checkConfigProfile(t *testing.T, expected, actual fleet.MDMAppleConfigProfile) { require.Equal(t, expected.Name, actual.Name) require.Equal(t, expected.Identifier, actual.Identifier) require.Equal(t, expected.Mobileconfig, actual.Mobileconfig) } +func checkConfigProfileWithChecksum(t *testing.T, expected, actual fleet.MDMAppleConfigProfile) { + checkConfigProfile(t, expected, actual) + require.ElementsMatch(t, md5.Sum(expected.Mobileconfig), actual.Checksum) // nolint:gosec // used only to hash for efficient comparisons +} + func testHostDetailsMDMProfiles(t *testing.T, ds *Datastore) { ctx := context.Background() @@ -1266,9 +1270,33 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { toRemove, err = ds.ListMDMAppleProfilesToRemove(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileID: globalPfs[0].ProfileID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileID: globalPfs[1].ProfileID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"}, - {ProfileID: globalPfs[2].ProfileID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"}, + { + ProfileID: globalPfs[0].ProfileID, + ProfileIdentifier: globalPfs[0].Identifier, + ProfileName: globalPfs[0].Name, + Status: &fleet.MDMAppleDeliveryVerified, + OperationType: fleet.MDMAppleOperationTypeInstall, + HostUUID: "test-uuid-1", + CommandUUID: "command-uuid", + }, + { + ProfileID: globalPfs[1].ProfileID, + ProfileIdentifier: globalPfs[1].Identifier, + ProfileName: globalPfs[1].Name, + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryVerified, + HostUUID: "test-uuid-1", + CommandUUID: "command-uuid", + }, + { + ProfileID: globalPfs[2].ProfileID, + ProfileIdentifier: globalPfs[2].Identifier, + ProfileName: globalPfs[2].Name, + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryVerified, + HostUUID: "test-uuid-1", + CommandUUID: "command-uuid", + }, }, toRemove) } @@ -3448,7 +3476,6 @@ func testListMDMAppleCommands(t *testing.T, ds *Datastore) { res, err = ds.ListMDMAppleCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMAppleCommandListOptions{}) require.NoError(t, err) require.Len(t, res, 2) - } func testMDMAppleEULA(t *testing.T, ds *Datastore) { @@ -4108,13 +4135,14 @@ func TestHostDEPAssignments(t *testing.T) { // simulate MDM unenroll require.NoError(t, ds.UpdateHostTablesOnMDMUnenroll(ctx, depUUID)) - // host MDM row is deleted on unenrollment + // host MDM row is set to defaults on unenrollment getHostResp, err = ds.Host(ctx, testHost.ID) require.NoError(t, err) require.NotNil(t, getHostResp) require.Equal(t, testHost.ID, getHostResp.ID) - require.Nil(t, getHostResp.MDM.EnrollmentStatus) - require.Nil(t, getHostResp.MDM.ServerURL) + require.NotNil(t, getHostResp.MDM.EnrollmentStatus) + require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus) + require.Empty(t, getHostResp.MDM.ServerURL) require.Empty(t, getHostResp.MDM.Name) require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host @@ -4152,13 +4180,14 @@ func TestHostDEPAssignments(t *testing.T) { err = ds.SetOrUpdateMDMData(ctx, testHost.ID, false, false, "", false, "") require.NoError(t, err) - // host MDM row is deleted when osquery reports MDM detail query with empty server URL + // host MDM row is reset to defaults when osquery reports MDM detail query with empty server URL getHostResp, err = ds.Host(ctx, testHost.ID) require.NoError(t, err) require.NotNil(t, getHostResp) require.Equal(t, testHost.ID, getHostResp.ID) - require.Nil(t, getHostResp.MDM.EnrollmentStatus) - require.Nil(t, getHostResp.MDM.ServerURL) + require.NotNil(t, getHostResp.MDM.EnrollmentStatus) + require.Equal(t, "Off", *getHostResp.MDM.EnrollmentStatus) + require.Empty(t, getHostResp.MDM.ServerURL) require.Empty(t, getHostResp.MDM.Name) require.Nil(t, getHostResp.DEPAssignedToFleet) // always nil for get host @@ -4302,145 +4331,6 @@ func testMDMAppleConfigProfileHash(t *testing.T, ds *Datastore) { } } -func testMatchMDMAppleConfigProfiles(t *testing.T, ds *Datastore) { - ctx := context.Background() - - // create some teams with different sets of profiles - tmNoProf, err := ds.NewTeam(ctx, &fleet.Team{Name: "no-prof"}) - require.NoError(t, err) - require.NotNil(t, tmNoProf) - - tmProfA, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-a"}) - require.NoError(t, err) - - tmProfAB, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-ab"}) - require.NoError(t, err) - - tmProfBC, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-bc"}) - require.NoError(t, err) - - tmProfABC, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-abc"}) - require.NoError(t, err) - - tmProfFVB, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-fvb"}) // file-vault and B profiles - require.NoError(t, err) - - tmProfFVFD, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-fvfd"}) // file-vault and fleetd profiles only - require.NoError(t, err) - - tmProfFDB, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-fdb"}) // fleetd and B profiles - require.NoError(t, err) - - tmProfFVFDB, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-fvfdb"}) // file-vault, fleetd and B profiles - require.NoError(t, err) - - // create another team with profile A - tmProfA2, err := ds.NewTeam(ctx, &fleet.Team{Name: "prof-a2"}) - require.NoError(t, err) - - profA := configProfileForTest(t, "A", "A", "A") - profB := configProfileForTest(t, "B", "B", "B") - profC := configProfileForTest(t, "C", "C", "C") - profFV := configProfileForTest(t, "Disk Encryption", mobileconfig.FleetFileVaultPayloadIdentifier, uuid.New().String()) - profFD := configProfileForTest(t, "Fleetd Configuration", mobileconfig.FleetdConfigPayloadIdentifier, uuid.New().String()) - - // tmProfA and tmProfA2 - profA.TeamID = &tmProfA.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profA) - require.NoError(t, err) - profA.TeamID = &tmProfA2.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profA) - require.NoError(t, err) - - // tmProfAB - profA.TeamID = &tmProfAB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profA) - require.NoError(t, err) - profB.TeamID = &tmProfAB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - - // tmProfABC - profA.TeamID = &tmProfABC.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profA) - require.NoError(t, err) - profB.TeamID = &tmProfABC.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - profC.TeamID = &tmProfABC.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profC) - require.NoError(t, err) - - // tmProfBC - profB.TeamID = &tmProfBC.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - profC.TeamID = &tmProfBC.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profC) - require.NoError(t, err) - - // tmProfFVB - profB.TeamID = &tmProfFVB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - profFV.TeamID = &tmProfFVB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFV) - require.NoError(t, err) - - // tmProfFVFD - profFV.TeamID = &tmProfFVFD.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFV) - require.NoError(t, err) - profFD.TeamID = &tmProfFVFD.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFD) - require.NoError(t, err) - - // tmProfFDB - profB.TeamID = &tmProfFDB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - profFD.TeamID = &tmProfFDB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFD) - require.NoError(t, err) - - // tmProfFVFDB - profFV.TeamID = &tmProfFVFDB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFV) - require.NoError(t, err) - profFD.TeamID = &tmProfFVFDB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profFD) - require.NoError(t, err) - profB.TeamID = &tmProfFVFDB.ID - _, err = ds.NewMDMAppleConfigProfile(ctx, *profB) - require.NoError(t, err) - - // get the hashes for each profile, the same way the matching logic would - profAHash := (fleet.MDMApplePreassignProfilePayload{Profile: profA.Mobileconfig}).HexMD5Hash() - profBHash := (fleet.MDMApplePreassignProfilePayload{Profile: profB.Mobileconfig}).HexMD5Hash() - profCHash := (fleet.MDMApplePreassignProfilePayload{Profile: profC.Mobileconfig}).HexMD5Hash() - - cases := []struct { - hashes []string - teamIDs []uint - }{ - {nil, nil}, - {[]string{profAHash}, []uint{tmProfA.ID, tmProfA2.ID}}, - {[]string{profBHash}, []uint{tmProfFVB.ID, tmProfFDB.ID, tmProfFVFDB.ID}}, // matches even though the team has filevault/fleetd in addition to B - {[]string{profCHash}, nil}, - {[]string{profAHash, profBHash}, []uint{tmProfAB.ID}}, - {[]string{profAHash, profBHash, profCHash}, []uint{tmProfABC.ID}}, - {[]string{profBHash, profCHash}, []uint{tmProfBC.ID}}, - {[]string{profAHash, profCHash}, nil}, - } - for _, c := range cases { - t.Run(fmt.Sprintf("%v", c.hashes), func(t *testing.T) { - matches, err := ds.MatchMDMAppleConfigProfiles(ctx, c.hashes) - require.NoError(t, err) - require.ElementsMatch(t, c.teamIDs, matches) - }) - } -} - func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) { ctx := context.Background() host, err := ds.NewHost(ctx, &fleet.Host{ @@ -4495,6 +4385,13 @@ func testResetMDMAppleEnrollment(t *testing.T, ds *Datastore) { require.NoError(t, err) err = ds.RecordHostBootstrapPackage(ctx, "command-uuid", host.UUID) require.NoError(t, err) + // add a record of the host DEP assignment + _, err = ds.writer(ctx).Exec(` + INSERT INTO host_dep_assignments (host_id) + VALUES (?) + ON DUPLICATE KEY UPDATE added_at = CURRENT_TIMESTAMP, deleted_at = NULL + `, host.ID) + require.NoError(t, err) err = ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, "foo.mdm.example.com", true, "") require.NoError(t, err) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 4b3980428d..a00b339a31 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -824,6 +824,7 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{ params = append(params, *opt.MDMNameFilter) } if opt.MDMEnrollmentStatusFilter != "" { + // NOTE: ds.UpdateHostTablesOnMDMUnenroll sets installed_from_dep = 0 so DEP hosts are not counted as pending after unenrollment switch opt.MDMEnrollmentStatusFilter { case fleet.MDMEnrollStatusAutomatic: sql += ` AND hmdm.enrolled = 1 AND hmdm.installed_from_dep = 1` @@ -838,7 +839,7 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{ } } if opt.MDMNameFilter != nil || opt.MDMIDFilter != nil || opt.MDMEnrollmentStatusFilter != "" { - sql += ` AND NOT COALESCE(hmdm.is_server, false) ` + sql += ` AND NOT COALESCE(hmdm.is_server, false) AND h.platform IN('darwin', 'windows')` } return sql, params } @@ -2230,7 +2231,7 @@ func (ds *Datastore) FailingPoliciesCount(ctx context.Context, host *fleet.Host) query := ` SELECT SUM(1 - pm.passes) AS n_failed FROM policy_membership pm - WHERE pm.host_id = ? + WHERE pm.host_id = ? AND pm.passes IS NOT null GROUP BY host_id ` @@ -2755,31 +2756,13 @@ func (ds *Datastore) SetOrUpdateMDMData( installedFromDep bool, name string, ) error { - // MDM queries return an empty server URL when the host is not enrolled to an MDM. - if serverURL == "" { - // We use the reader even if it might miss some not replicated rows, - // because it will eventually catch up on subsequent ingestions and - // the entry will be deleted. - var id uint - switch err := sqlx.GetContext(ctx, ds.reader(ctx), &id, - `SELECT host_id FROM host_mdm WHERE host_id = ?`, hostID, - ); { - case err == nil: - if _, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM host_mdm WHERE host_id = ?`, hostID); err != nil { - return ctxerr.Wrapf(ctx, err, "delete host_mdm row: %d", hostID) - } - return nil - case errors.Is(err, sql.ErrNoRows): - return nil - default: - return ctxerr.Wrapf(ctx, err, "getting host_mdm row: %d", hostID) + var mdmID *uint + if serverURL != "" { + id, err := ds.getOrInsertMDMSolution(ctx, serverURL, name) + if err != nil { + return err } - - } - - mdmID, err := ds.getOrInsertMDMSolution(ctx, serverURL, name) - if err != nil { - return err + mdmID = &id } return ds.updateOrInsert( @@ -2873,7 +2856,7 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) msg := fmt.Sprintf("for host %d", hostID) return nil, ctxerr.Wrap(ctx, notFound("HostDiskEncryptionKey").WithMessage(msg)) } - return nil, ctxerr.Wrapf(ctx, err, "getting data from host_mdm for host_id %d", hostID) + return nil, ctxerr.Wrapf(ctx, err, "getting data from host_disk_encryption_keys for host_id %d", hostID) } return &key, nil } @@ -2933,6 +2916,16 @@ func (ds *Datastore) GetHostMDM(ctx context.Context, hostID uint) (*fleet.HostMD } func (ds *Datastore) GetHostMDMCheckinInfo(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) { + // TODO: consider using host_dep_assignments instead of host_mdm because installed_from_dep can + // be set to false for DEP-assigned host (e.g., ds.UpdateHostTablesOnMDMUnenroll), which may + // lead to unexpected results in certain edge cases where HostMDMCheckinInfo is used to + // determine like bootstrap package installation + // NOTE: Consider joining on host_dep_assignments instead of host_mdm so DEP hosts that + // manually enroll or re-enroll are included in the results so long as they are not unassigned + // in Apple Business Manager. The problem with using host_dep_assignments is that a host can be + // assigned to Fleet in ABM but still manually enroll. We should probably keep using host_mdm, + // but be better at updating the table with the right values when a host enrolls (perhaps adding + // a query param to the enroll endpoint). var hmdm fleet.HostMDMCheckinInfo // use writer as it is used just after creation in some cases @@ -3308,7 +3301,7 @@ func (ds *Datastore) generateAggregatedMDMStatus(ctx context.Context, teamID *ui globalStats = true status fleet.AggregatedMDMStatus ) - + // NOTE: ds.UpdateHostTablesOnMDMUnenroll sets installed_from_dep = 0 so DEP hosts are not counted as pending after unenrollment query := `SELECT COUNT(DISTINCT host_id) as hosts_count, COALESCE(SUM(CASE WHEN NOT enrolled AND NOT installed_from_dep THEN 1 ELSE 0 END), 0) as unenrolled_hosts_count, diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index ef2b7b3838..128bdefd8b 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -1032,13 +1032,17 @@ func testHostsUnenrollFromMDM(t *testing.T, ds *Datastore) { require.Equal(t, 2, solutions[0].HostsCount) // Host `h` unenrolls from MDM, so MDM query returns empty server_url. - err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "", true, "") + err = ds.SetOrUpdateMDMData(ctx, h.ID, false, false, "", false, "") require.NoError(t, err) - // host_mdm entry should not exist anymore. - _, err = ds.GetHostMDM(ctx, h.ID) - require.Error(t, err) - require.True(t, fleet.IsNotFound(err)) + // host_mdm entry should still exist with empty values. + hmdm, err = ds.GetHostMDM(ctx, h.ID) + require.NoError(t, err) + require.Equal(t, h.ID, hmdm.HostID) + require.False(t, hmdm.Enrolled) + require.False(t, hmdm.InstalledFromDep) + require.Nil(t, hmdm.MDMID) + require.Empty(t, hmdm.ServerURL) err = ds.GenerateAggregatedMunkiAndMDM(ctx) require.NoError(t, err) @@ -1049,13 +1053,21 @@ func testHostsUnenrollFromMDM(t *testing.T, ds *Datastore) { require.Equal(t, 1, solutions[0].HostsCount) // Host `h2` unenrolls from MDM, so MDM query returns empty server_url. - err = ds.SetOrUpdateMDMData(ctx, h2.ID, false, true, "", true, "") + err = ds.SetOrUpdateMDMData(ctx, h2.ID, false, false, "", false, "") require.NoError(t, err) // host_mdm entry should not exist anymore. - _, err = ds.GetHostMDM(ctx, h2.ID) - require.Error(t, err) - require.True(t, fleet.IsNotFound(err)) + hmdm, err = ds.GetHostMDM(ctx, h2.ID) + require.NoError(t, err) + + // host_mdm entry should still exist with empty values. + hmdm, err = ds.GetHostMDM(ctx, h2.ID) + require.NoError(t, err) + require.Equal(t, h2.ID, hmdm.HostID) + require.False(t, hmdm.Enrolled) + require.False(t, hmdm.InstalledFromDep) + require.Nil(t, hmdm.MDMID) + require.Empty(t, hmdm.ServerURL) err = ds.GenerateAggregatedMunkiAndMDM(ctx) require.NoError(t, err) @@ -1080,6 +1092,7 @@ func testHostsListMDM(t *testing.T, ds *Datastore) { NodeKey: ptr.String(fmt.Sprintf("%d", i)), UUID: fmt.Sprintf("%d", i), Hostname: fmt.Sprintf("foo.local%d", i), + Platform: "darwin", }) require.NoError(t, err) hostIDs = append(hostIDs, h.ID) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index 660b350b5d..86cedd44e9 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -253,6 +253,7 @@ func testLabelsListHostsInLabel(t *testing.T, db *Datastore) { NodeKey: ptr.String("1"), UUID: "1", Hostname: "foo.local", + Platform: "darwin", }) require.Nil(t, err) @@ -265,6 +266,7 @@ func testLabelsListHostsInLabel(t *testing.T, db *Datastore) { NodeKey: ptr.String("2"), UUID: "2", Hostname: "bar.local", + Platform: "darwin", }) require.Nil(t, err) @@ -277,6 +279,7 @@ func testLabelsListHostsInLabel(t *testing.T, db *Datastore) { NodeKey: ptr.String("3"), UUID: "3", Hostname: "baz.local", + Platform: "darwin", }) require.Nil(t, err) require.NoError(t, db.SetOrUpdateHostDisksSpace(context.Background(), h1.ID, 10, 5)) diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index aea7d99f56..863f9c290f 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -10,7 +10,7 @@ import ( ) // MDMWindowsGetEnrolledDevice receives a Windows MDM device id and returns the device information. -func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) { +func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceHWID string) (*fleet.MDMWindowsEnrolledDevice, error) { stmt := `SELECT mdm_device_id, mdm_hardware_id, @@ -24,12 +24,12 @@ func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceI not_in_oobe, created_at, updated_at - FROM mdm_windows_enrollments WHERE mdm_device_id = ?` + FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?` var winMDMDevice fleet.MDMWindowsEnrolledDevice - if err := sqlx.GetContext(ctx, ds.reader(ctx), &winMDMDevice, stmt, mdmDeviceID); err != nil { + if err := sqlx.GetContext(ctx, ds.reader(ctx), &winMDMDevice, stmt, mdmDeviceHWID); err != nil { if err == sql.ErrNoRows { - return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice").WithMessage(mdmDeviceID)) + return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice").WithMessage(mdmDeviceHWID)) } return nil, ctxerr.Wrap(ctx, err, "get MDMWindowsEnrolledDevice") } @@ -66,7 +66,7 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device device.MDMNotInOOBE) if err != nil { if isDuplicate(err) { - return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsEnrolledDevice", device.MDMDeviceID)) + return ctxerr.Wrap(ctx, alreadyExists("MDMWindowsEnrolledDevice", device.MDMHardwareID)) } return ctxerr.Wrap(ctx, err, "inserting MDMWindowsEnrolledDevice") } @@ -75,10 +75,10 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device } // MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the device id. -func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceID string) error { - stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_device_id = ?" +func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error { + stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?" - res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceID) + res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceHWID) if err != nil { return ctxerr.Wrap(ctx, err, "delete MDMWindowsEnrolledDevice") } diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go index d0020e0a86..00e2a15265 100644 --- a/server/datastore/mysql/microsoft_mdm_test.go +++ b/server/datastore/mysql/microsoft_mdm_test.go @@ -33,7 +33,7 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) { enrolledDevice := &fleet.MDMWindowsEnrolledDevice{ MDMDeviceID: uuid.New().String(), - MDMHardwareID: uuid.New().String(), + MDMHardwareID: uuid.New().String() + uuid.New().String(), MDMDeviceState: uuid.New().String(), MDMDeviceType: "CIMClient_Windows", MDMDeviceName: "DESKTOP-1C3ARC1", @@ -51,19 +51,19 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) { err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) require.ErrorAs(t, err, &ae) - gotEnrolledDevice, err := ds.MDMWindowsGetEnrolledDevice(ctx, enrolledDevice.MDMDeviceID) + gotEnrolledDevice, err := ds.MDMWindowsGetEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) require.NoError(t, err) require.NotZero(t, gotEnrolledDevice.CreatedAt) require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID) require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID) - err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMDeviceID) + err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) require.NoError(t, err) var nfe fleet.NotFoundError - _, err = ds.MDMWindowsGetEnrolledDevice(ctx, enrolledDevice.MDMDeviceID) + _, err = ds.MDMWindowsGetEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) require.ErrorAs(t, err, &nfe) - err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMDeviceID) + err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) require.ErrorAs(t, err, &nfe) } diff --git a/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts.go b/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts.go new file mode 100644 index 0000000000..8a05c36e4a --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts.go @@ -0,0 +1,91 @@ +package tables + +import ( + "database/sql" + "fmt" + + "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx/reflectx" + "github.com/pkg/errors" + "howett.net/plist" +) + +func init() { + MigrationClient.AddMigration(Up_20230711144622, Down_20230711144622) +} + +// setBypassAttemptsToFileVaultProfile is used to add the +// `DeferForceAtUserLoginMaxBypassAttempts` to an existing FileVault profile at +// the right place without doing any other modifications to the profile. +// +// We intentionally use a map[string]interface{} to make sure we're fully +// unmarshalling and marshalling the profile without making additional changes. +func setBypassAttemptsToFileVaultProfile(original []byte) ([]byte, error) { + var configuration map[string]interface{} + if _, err := plist.Unmarshal(original, &configuration); err != nil { + return nil, fmt.Errorf("unmarshalling configuration profile: %w", err) + } + + payloadContent, ok := configuration["PayloadContent"].([]interface{}) + if !ok { + return nil, errors.New("failed to access PayloadContent element") + } + + for _, c := range payloadContent { + payload, ok := c.(map[string]interface{}) + if !ok { + return nil, errors.New("failed to access Payload element") + } + + if payload["PayloadType"] == "com.apple.MCX.FileVault2" { + payload["DeferForceAtUserLoginMaxBypassAttempts"] = 1 + } + } + + out, err := plist.Marshal(configuration, plist.XMLFormat) + if err != nil { + return nil, fmt.Errorf("failed to marshal new payload: %w", err) + } + + return out, nil +} + +func Up_20230711144622(tx *sql.Tx) error { + txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)} + + fvProfiles := []struct { + ID uint `db:"profile_id"` + Mobileconfig []byte `db:"mobileconfig"` + }{} + query := ` + SELECT profile_id, mobileconfig FROM mdm_apple_configuration_profiles macp WHERE identifier = 'com.fleetdm.fleet.mdm.filevault' + ` + if err := txx.Select(&fvProfiles, query); err != nil { + return fmt.Errorf("getting existing FileVault profiles: %w", err) + } + + if len(fvProfiles) == 0 { + return nil + } + + for _, prof := range fvProfiles { + newProf, err := setBypassAttemptsToFileVaultProfile(prof.Mobileconfig) + if err != nil { + return fmt.Errorf("adding new key to profile with ID %d: %w", prof.ID, err) + } + + if _, err = txx.Exec(` + UPDATE mdm_apple_configuration_profiles + SET mobileconfig = ?, checksum = UNHEX(MD5(mobileconfig)) + WHERE profile_id = ? + `, newProf, prof.ID); err != nil { + return fmt.Errorf("updating FileVault profile with ID %d: %w", prof.ID, err) + } + } + + return nil +} + +func Down_20230711144622(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts_test.go b/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts_test.go new file mode 100644 index 0000000000..c885a73447 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230711144622_SetFileVaultMaxBypassAttempts_test.go @@ -0,0 +1,167 @@ +package tables + +import ( + "testing" + + "github.com/stretchr/testify/require" + "howett.net/plist" +) + +func TestUp_20230711144622(t *testing.T) { + db := applyUpToPrev(t) + + stmt := ` +INSERT INTO + mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum) +VALUES (?, ?, ?, ?, UNHEX(MD5(mobileconfig)))` + + mcBytes := []byte(` + + + + + PayloadContent + + + Defer + + Enable + On + PayloadDisplayName + FileVault 2 + PayloadIdentifier + com.apple.MCX.FileVault2.3548D750-6357-4910-8DEA-D80ADCE2C787 + PayloadType + com.apple.MCX.FileVault2 + PayloadUUID + 3548D750-6357-4910-8DEA-D80ADCE2C787 + PayloadVersion + 1 + ShowRecoveryKey + + + + EncryptCertPayloadUUID + A326B71F-EB80-41A5-A8CD-A6F932544281 + Location + Fleet + PayloadDisplayName + FileVault Recovery Key Escrow + PayloadIdentifier + com.apple.security.FDERecoveryKeyEscrow.3690D771-DCB8-4D5D-97D6-209A138DF03E + PayloadType + com.apple.security.FDERecoveryKeyEscrow + PayloadUUID + 3C329F2B-3D47-4141-A2B5-5C52A2FD74F8 + PayloadVersion + 1 + + + PayloadCertificateFileName + Fleet certificate + PayloadContent + dGVzdAo= + PayloadDisplayName + Certificate Root + PayloadIdentifier + com.apple.security.root.A326B71F-EB80-41A5-A8CD-A6F932544281 + PayloadType + com.apple.security.pkcs1 + PayloadUUID + A326B71F-EB80-41A5-A8CD-A6F932544281 + PayloadVersion + 1 + + + dontAllowFDEDisable + + PayloadIdentifier + com.apple.MCX.62024f29-105E-497A-A724-1D5BA4D9E854 + PayloadType + com.apple.MCX + PayloadUUID + 62024f29-105E-497A-A724-1D5BA4D9E854 + PayloadVersion + 1 + + + PayloadDisplayName + Disk encryption + PayloadIdentifier + com.fleetdm.fleet.mdm.filevault + PayloadType + Configuration + PayloadUUID + 74FEAC88-B614-468E-A4B4-B4B0C93B5D52 + PayloadVersion + 1 + + +`) + + // add a global FV profile + r, err := db.Exec(stmt, 0, "com.fleetdm.fleet.mdm.filevault", "Disk encryption", mcBytes) + require.NoError(t, err) + globalProfileID, _ := r.LastInsertId() + + // create a team + r, err = db.Exec(`INSERT INTO teams (name) VALUES (?)`, "Test Team") + require.NoError(t, err) + teamID, _ := r.LastInsertId() + + // add the FV profile to the team + r, err = db.Exec(stmt, teamID, "com.fleetdm.fleet.mdm.filevault", "Disk encryption", mcBytes) + require.NoError(t, err) + teamProfileID, _ := r.LastInsertId() + + var ( + identifier string + mobileconfig []byte + ) + + stmt = "SELECT identifier, mobileconfig FROM mdm_apple_configuration_profiles WHERE name = ? AND team_id = ?" + err = db.QueryRow(stmt, "Disk encryption", 0).Scan(&identifier, &mobileconfig) + require.NoError(t, err) + require.Equal(t, "com.fleetdm.fleet.mdm.filevault", identifier) + require.Equal(t, mcBytes, mobileconfig) + + err = db.QueryRow(stmt, "Disk encryption", teamID).Scan(&identifier, &mobileconfig) + require.NoError(t, err) + require.Equal(t, "com.fleetdm.fleet.mdm.filevault", identifier) + require.Equal(t, mcBytes, mobileconfig) + + applyNext(t, db) + + verifyNewPayload := func(profileID int64) { + var mc []byte + stmt = "SELECT mobileconfig FROM mdm_apple_configuration_profiles WHERE profile_id = ?" + err = db.QueryRow(stmt, profileID).Scan(&mc) + require.NoError(t, err) + + // unmarshal only the fields we want to test + var payload struct { + PayloadContent []map[string]interface{} + } + _, err = plist.Unmarshal(mc, &payload) + require.NoError(t, err) + require.Len(t, payload.PayloadContent, 4) + + // find the right payload + var found map[string]interface{} + for _, p := range payload.PayloadContent { + if p["PayloadType"] == "com.apple.MCX.FileVault2" { + found = p + break + } + } + + require.NotNil(t, found) + require.EqualValues(t, 1, found["DeferForceAtUserLoginMaxBypassAttempts"]) + } + + // verify global profile modifications + verifyNewPayload(globalProfileID) + + // verify tea profile modifications + verifyNewPayload(teamProfileID) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 421e82967e..b1378f2515 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -40,7 +40,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}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": 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}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": 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, \"deferred_save_host\": false, \"live_query_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}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": 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}, \"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}, \"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, \"deferred_save_host\": false, \"live_query_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'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `carve_blocks` ( diff --git a/server/datastore/mysql/teams.go b/server/datastore/mysql/teams.go index f1dd45448d..3fe764fe6b 100644 --- a/server/datastore/mysql/teams.go +++ b/server/datastore/mysql/teams.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "strings" @@ -123,6 +124,9 @@ func (ds *Datastore) TeamByName(ctx context.Context, name string) (*fleet.Team, team := &fleet.Team{} if err := sqlx.GetContext(ctx, ds.reader(ctx), team, stmt, name); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ctxerr.Wrap(ctx, notFound("Team").WithName(name)) + } return nil, ctxerr.Wrap(ctx, err, "select team") } diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 4c6adfbcd4..6dee14e934 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -692,6 +692,7 @@ type ActivityTypeMDMEnrolled struct { HostSerial string `json:"host_serial"` HostDisplayName string `json:"host_display_name"` InstalledFromDEP bool `json:"installed_from_dep"` + MDMPlatform string `json:"mdm_platform"` } func (a ActivityTypeMDMEnrolled) ActivityName() string { @@ -703,10 +704,12 @@ func (a ActivityTypeMDMEnrolled) Documentation() (activity string, details strin `This activity contains the following fields: - "host_serial": Serial number of the host. - "host_display_name": Display name of the host. -- "installed_from_dep": Whether the host was enrolled via DEP.`, `{ +- "installed_from_dep": Whether the host was enrolled via DEP. +- "mdm_platform": Used to distinguish between Apple and Microsoft enrollments. Can be "apple", "microsoft" or not present. If missing, this value is treated as "apple" for backwards compatibility.`, `{ "host_serial": "C08VQ2AXHT96", "host_display_name": "MacBookPro16,1 (C08VQ2AXHT96)", - "installed_from_dep": true + "installed_from_dep": true, + "mdm_platform": "apple" }` } @@ -1016,7 +1019,7 @@ func (a ActivityTypeEnabledWindowsMDM) ActivityName() string { } func (a ActivityTypeEnabledWindowsMDM) Documentation() (activity, details, detailsExample string) { - return `Generated when a user turns on MDM features for all Windows hosts (servers excluded).`, + return `Windows MDM features are not ready for production and are currently in development. These features are disabled by default. Generated when a user turns on MDM features for all Windows hosts (servers excluded).`, `This activity does not contain any detail fields.`, `` } @@ -1027,7 +1030,7 @@ func (a ActivityTypeDisabledWindowsMDM) ActivityName() string { } func (a ActivityTypeDisabledWindowsMDM) Documentation() (activity, details, detailsExample string) { - return `Generated when a user turns off MDM features for all Windows hosts.`, + return `Windows MDM features are not ready for production and are currently in development. These features are disabled by default. Generated when a user turns off MDM features for all Windows hosts.`, `This activity does not contain any detail fields.`, `` } diff --git a/server/fleet/app.go b/server/fleet/app.go index 04512b0de6..6a77190739 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -663,8 +663,9 @@ func (c *AppConfig) UnmarshalJSON(b []byte) error { // OrgInfo contains general info about the organization using Fleet. type OrgInfo struct { - OrgName string `json:"org_name"` - OrgLogoURL string `json:"org_logo_url"` + OrgName string `json:"org_name"` + OrgLogoURL string `json:"org_logo_url"` + OrgLogoURLLightBackground string `json:"org_logo_url_light_background"` // ContactURL is the URL displayed for users to contact support. By default, // https://fleetdm.com/company/contact is used. ContactURL string `json:"contact_url"` diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 2804a53a16..29898a0a9b 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -290,7 +290,7 @@ type MDMAppleConfigProfile struct { // representation of the configuration profile. It must be XML or PKCS7 parseable. Mobileconfig mobileconfig.Mobileconfig `db:"mobileconfig" json:"-"` // Checksum is an MD5 hash of the Mobileconfig bytes - Checksum []byte `db:"checksum" json:"-"` + Checksum []byte `db:"checksum" json:"checksum,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } @@ -377,11 +377,15 @@ func (d HostMDMProfileDetail) Message() string { } type MDMAppleProfilePayload struct { - ProfileID uint `db:"profile_id"` - ProfileIdentifier string `db:"profile_identifier"` - ProfileName string `db:"profile_name"` - HostUUID string `db:"host_uuid"` - Checksum []byte `db:"checksum"` + ProfileID uint `db:"profile_id"` + ProfileIdentifier string `db:"profile_identifier"` + ProfileName string `db:"profile_name"` + HostUUID string `db:"host_uuid"` + Checksum []byte `db:"checksum"` + Status *MDMAppleDeliveryStatus `db:"status" json:"status"` + OperationType MDMAppleOperationType `db:"operation_type"` + Detail string `db:"detail"` + CommandUUID string `db:"command_uuid"` } type MDMAppleBulkUpsertHostProfilePayload struct { @@ -459,6 +463,7 @@ type MDMApplePreassignProfilePayload struct { HostUUID string `json:"host_uuid"` Profile []byte `json:"profile"` Group string `json:"group"` + Exclude bool `json:"exclude"` } // HexMD5Hash returns the hex-encoded MD5 hash of the profile. Note that MD5 is @@ -484,6 +489,7 @@ type MDMApplePreassignProfile struct { Profile []byte Group string HexMD5Hash string + Exclude bool } // MDMAppleSettingsPayload describes the payload accepted by the endpoint to diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 85af07ab74..ccace9adb6 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -784,15 +784,12 @@ type Datastore interface { // For global config profiles, specify nil as the team id. ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint) ([]*MDMAppleConfigProfile, error) - // MatchMDMAppleConfigProfiles returns the list of team ids that have the - // exact set of configuration profiles as those specified by their - // hex-encoded md5 hashes. - MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error) - // DeleteMDMAppleConfigProfile deletes the mdm config profile corresponding // to the specified profile id. DeleteMDMAppleConfigProfile(ctx context.Context, profileID uint) error + BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, payload []*MDMAppleProfilePayload) error + // DeleteMDMAppleConfigProfileByTeamAndIdentifier deletes a configuration // profile using the unique key defined by `team_id` and `identifier` DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx context.Context, teamID *uint, profileIdentifier string) error diff --git a/server/fleet/device.go b/server/fleet/device.go index 7196431ffc..206fcfa789 100644 --- a/server/fleet/device.go +++ b/server/fleet/device.go @@ -36,9 +36,10 @@ type DesktopMDMConfig struct { // DesktopMDMConfig is a subset of fleet.OrgInfo with configuration that's relevant // to Fleet Desktop to operate. type DesktopOrgInfo struct { - OrgName string `json:"org_name"` - OrgLogoURL string `json:"org_logo_url"` - ContactURL string `json:"contact_url"` + OrgName string `json:"org_name"` + OrgLogoURL string `json:"org_logo_url"` + OrgLogoURLLightBackground string `json:"org_logo_url_light_background"` + ContactURL string `json:"contact_url"` } type MigrateMDMDeviceWebhookPayload struct { diff --git a/server/fleet/gen_activity_doc.go b/server/fleet/gen_activity_doc.go index 8abeda8836..65b9ea977f 100644 --- a/server/fleet/gen_activity_doc.go +++ b/server/fleet/gen_activity_doc.go @@ -15,7 +15,7 @@ func main() { var b strings.Builder b.WriteString(` -# Audit Activities +# Audit activities Fleet logs the following information for administrative actions (in JSON): @@ -65,7 +65,9 @@ Example: } b.WriteString(` -`) + + +`) if err := os.WriteFile(os.Args[1], []byte(b.String()), 0600); err != nil { panic(err) diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 928249ec73..cf5c2b0a29 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -7,6 +7,11 @@ import ( "time" ) +const ( + MDMPlatformApple = "apple" + MDMPlatformMicrosoft = "microsoft" +) + type AppleMDM struct { CommonName string `json:"common_name"` SerialNumber string `json:"serial_number"` diff --git a/server/fleet/microsoft_mdm.go b/server/fleet/microsoft_mdm.go index 9ff8fe0a8d..5cbdd841ee 100644 --- a/server/fleet/microsoft_mdm.go +++ b/server/fleet/microsoft_mdm.go @@ -9,6 +9,7 @@ import ( "time" mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" + microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" ) ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -44,13 +45,25 @@ type SoapRequest struct { Body BodyRequest `xml:"Body"` } -// GetBinarySecurityToken returns the header BinarySecurityToken if present -func (req *SoapRequest) GetBinarySecurityToken() (string, error) { +// GetHeaderBinarySecurityToken returns the header BinarySecurityToken if present +func (req *SoapRequest) GetHeaderBinarySecurityToken() (*HeaderBinarySecurityToken, error) { if req.Header.Security == nil { - return "", errors.New("header BinarySecurityToken is not present") + return nil, errors.New("binarySecurityToken is not present") } - return req.Header.Security.Security.Content, nil + if len(req.Header.Security.Security.Content) == 0 { + return nil, errors.New("binarySecurityToken is empty") + } + + if req.Header.Security.Security.Encoding != mdm.EnrollEncode { + return nil, errors.New("binarySecurityToken encoding is invalid") + } + + if req.Header.Security.Security.Value != mdm.BinarySecurityDeviceEnroll && req.Header.Security.Security.Value != mdm.BinarySecurityAzureEnroll { + return nil, errors.New("binarySecurityToken type is invalid") + } + + return &req.Header.Security.Security, nil } // GetMessageID returns the message ID from the header @@ -210,11 +223,6 @@ func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error { return errors.New("invalid requestsecuritytoken message: AdditionalContext.ContextItems missing") } - reqVersion, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemRequestVersion) - if err != nil || (reqVersion != mdm.EnrollmentVersionV5 && reqVersion != mdm.EnrollmentVersionV4) { - return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemRequestVersion, reqVersion, err) - } - reqEnrollType, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemEnrollmentType) if err != nil || reqEnrollType != mdm.ReqSecTokenEnrollType { return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemEnrollmentType, reqEnrollType, err) @@ -328,16 +336,59 @@ type WsSecurity struct { } // Security token container for encoded security sensitive data -type BinSecurityToken struct { +type HeaderBinarySecurityToken struct { Content string `xml:",chardata"` Value string `xml:"ValueType,attr"` Encoding string `xml:"EncodingType,attr"` } +// Get RequestSecurityToken MDM Message from the body +func (token *HeaderBinarySecurityToken) IsValidToken() error { + if token == nil { + return errors.New("binary security token is not present") + } + + if len(token.Content) == 0 { + return errors.New("binary security token is empty") + } + + if token.Value != microsoft_mdm.BinarySecurityDeviceEnroll && token.Value != microsoft_mdm.BinarySecurityAzureEnroll { + return errors.New("binary security token is invalid") + } + + return nil +} + +// Check if input token is a valid Azure JWT token +func (token *HeaderBinarySecurityToken) IsAzureJWTToken() bool { + if token == nil { + return false + } + + if token.Value == microsoft_mdm.BinarySecurityAzureEnroll { + return true + } + + return false +} + +// Check if input token is a valid Device Enroll token +func (token *HeaderBinarySecurityToken) IsDeviceToken() bool { + if token == nil { + return false + } + + if token.Value == microsoft_mdm.BinarySecurityDeviceEnroll { + return true + } + + return false +} + // TokenSecurity is the security token container for BinSecurityToken type TokenSecurity struct { - MustUnderstand string `xml:"mustUnderstand,attr"` - Security BinSecurityToken `xml:"BinarySecurityToken"` + MustUnderstand string `xml:"mustUnderstand,attr"` + Security HeaderBinarySecurityToken `xml:"BinarySecurityToken"` } // To target endpoint header field @@ -486,10 +537,11 @@ type DiscoverResponse struct { } type DiscoverResult struct { - AuthPolicy string `xml:"AuthPolicy"` - EnrollmentVersion string `xml:"EnrollmentVersion"` - EnrollmentPolicyServiceUrl string `xml:"EnrollmentPolicyServiceUrl"` - EnrollmentServiceUrl string `xml:"EnrollmentServiceUrl"` + AuthPolicy string `xml:"AuthPolicy"` + EnrollmentVersion string `xml:"EnrollmentVersion"` + EnrollmentPolicyServiceUrl string `xml:"EnrollmentPolicyServiceUrl"` + EnrollmentServiceUrl string `xml:"EnrollmentServiceUrl"` + AuthServiceUrl *string `xml:"AuthenticationServiceUrl"` } /////////////////////////////////////////////////////////////// @@ -651,7 +703,8 @@ type WindowsMDMAccessTokenPayload struct { // Type is the enrollment type, such as "programmatic". Type WindowsMDMEnrollmentType `json:"type"` Payload struct { - HostUUID string `json:"host_uuid"` + HostUUID string `json:"host_uuid"` + AuthToken string `json:"auth_token"` } `json:"payload"` } @@ -661,18 +714,23 @@ type WindowsMDMEnrollmentType int const ( WindowsMDMProgrammaticEnrollmentType WindowsMDMEnrollmentType = 1 + WindowsMDMAutomaticEnrollmentType WindowsMDMEnrollmentType = 2 ) func (t *WindowsMDMAccessTokenPayload) IsValidToken() error { // Only BSProgrammaticEnrollment are supported for now - if t.Type != WindowsMDMProgrammaticEnrollmentType { + if t.Type != WindowsMDMProgrammaticEnrollmentType && t.Type != WindowsMDMAutomaticEnrollmentType { return errors.New("invalid binary security payload type") } - if len(t.Payload.HostUUID) == 0 { + if t.Type == WindowsMDMProgrammaticEnrollmentType && len(t.Payload.HostUUID) == 0 { return errors.New("invalid binary security payload content") } + if t.Type == WindowsMDMAutomaticEnrollmentType && len(t.Payload.AuthToken) == 0 { + return errors.New("invalid STS auth token payload content") + } + return nil } @@ -760,3 +818,79 @@ type MDMWindowsEnrolledDevice struct { func (e MDMWindowsEnrolledDevice) AuthzType() string { return "mdm_windows" } + +/////////////////////////////////////////////////////////////// +/// Microsoft MS-MDM message + +type SyncMLMessage struct { + XMLinfo string `xml:"xmlns,attr"` + Header SyncMLHeader `xml:"SyncHdr"` + Body SyncMLBody `xml:"SyncBody"` +} + +// SyncML XML Parsing Types - This needs to be improved +type SyncMLHeader struct { + DTD string `xml:"VerDTD"` + Version string `xml:"VerProto"` + SessionID int `xml:"SessionID"` + MsgID int `xml:"MsgID"` + Target string `xml:"Target>LocURI"` + Source string `xml:"Source>LocURI"` + MaxMsgSize int `xml:"Meta>A:MaxMsgSize"` +} + +type SyncMLCommandMeta struct { + XMLinfo string `xml:"xmlns,attr"` + Type string `xml:"Type"` +} + +type SyncMLCommandItem struct { + Meta SyncMLCommandMeta `xml:"Meta"` + Source string `xml:"Source>LocURI"` + Data string `xml:"Data"` +} + +type SyncMLCommand struct { + XMLName xml.Name + CmdID int `xml:",omitempty"` + MsgRef string `xml:",omitempty"` + CmdRef string `xml:",omitempty"` + Cmd string `xml:",omitempty"` + Target string `xml:"Target>LocURI"` + Source string `xml:"Source>LocURI"` + Data string `xml:",omitempty"` + Item []SyncMLCommandItem `xml:",any"` +} + +type SyncMLBody struct { + Item []SyncMLCommand `xml:",any"` +} + +// IsValidSyncMLMsg checks for required fields in the SyncML message +func (req *SyncMLMessage) IsValidSyncMLMsg() error { + if req == nil { + return errors.New("invalid SyncML message: nil") + } + + if len(req.Header.Version) == 0 { + return errors.New("invalid SyncML message: Version") + } + + if len(req.Header.Target) == 0 { + return errors.New("invalid SyncML message: Target") + } + + if req.Header.SessionID == 0 { + return errors.New("invalid SyncML message: SessionID") + } + + if req.Header.MsgID == 0 { + return errors.New("invalid SyncML message: SessionID") + } + + if len(req.Body.Item) == 0 { + return errors.New("invalid SyncML message: Item") + } + + return nil +} diff --git a/server/fleet/service.go b/server/fleet/service.go index ec0f08e8dd..98cb462ba5 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -679,6 +679,9 @@ type Service interface { // MMDAppleEraseDevice erases a host MDMAppleEraseDevice(ctx context.Context, hostID uint) error + // MDMListHostConfigurationProfiles returns configuration profiles for a given host + MDMListHostConfigurationProfiles(ctx context.Context, hostID uint) ([]*MDMAppleConfigProfile, error) + // MDMAppleEnableFileVaultAndEscrow adds a configuration profile for the // given team that enables FileVault with a config that allows Fleet to // escrow the recovery key. @@ -757,13 +760,16 @@ type Service interface { // Windows MDM // GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message - GetMDMMicrosoftDiscoveryResponse(ctx context.Context) (*DiscoverResponse, error) + GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEmail string) (*DiscoverResponse, error) + + // GetMDMMicrosoftSTSAuthResponse returns a valid STS auth page + GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru string, loginHint string) (string, error) // GetMDMWindowsPolicyResponse returns a valid GetPoliciesResponse message - GetMDMWindowsPolicyResponse(ctx context.Context, authToken string) (*GetPoliciesResponse, error) + GetMDMWindowsPolicyResponse(ctx context.Context, authToken *HeaderBinarySecurityToken) (*GetPoliciesResponse, error) // GetMDMWindowsEnrollResponse returns a valid RequestSecurityTokenResponseCollection message - GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg *RequestSecurityToken, authToken string) (*RequestSecurityTokenResponseCollection, error) + GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg *RequestSecurityToken, authToken *HeaderBinarySecurityToken) (*RequestSecurityTokenResponseCollection, error) // GetAuthorizedSoapFault authorize the request so SoapFault message can be returned GetAuthorizedSoapFault(ctx context.Context, eType string, origMsg int, errorMsg error) *SoapFault @@ -771,4 +777,10 @@ type Service interface { // SignMDMMicrosoftClientCSR returns a signed certificate from the client certificate signing request and the // certificate fingerprint. The certificate common name should be passed in the subject parameter. SignMDMMicrosoftClientCSR(ctx context.Context, subject string, csr *x509.CertificateRequest) ([]byte, string, error) + + // GetMDMWindowsManagementResponse returns a valid SyncML response message + GetMDMWindowsManagementResponse(ctx context.Context, reqSyncML *SyncMLMessage) (*string, error) + + // GetMDMWindowsTOSContent returns TOS content + GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) } diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 41d35a43c4..3177aa8bd7 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -3,10 +3,8 @@ package apple_mdm import ( "bytes" "context" - "database/sql" "encoding/json" "encoding/xml" - "errors" "fmt" "strings" "text/template" @@ -300,8 +298,7 @@ func (d *DEPService) RunAssigner(ctx context.Context) error { var appleBMTeam *fleet.Team if appCfg.MDM.AppleBMDefaultTeam != "" { tm, err := d.ds.TeamByName(ctx, appCfg.MDM.AppleBMDefaultTeam) - // NOTE: TeamByName does NOT return a not found error if it does not exist - if err != nil && !errors.Is(err, sql.ErrNoRows) { + if err != nil && !fleet.IsNotFound(err) { return err } appleBMTeam = tm @@ -680,3 +677,53 @@ func GenerateEnrollmentProfileMobileconfig(orgName, fleetURL, scepChallenge, top } return buf.Bytes(), nil } + +// ProfileBimap implements bidirectional mapping for profiles, and utility +// functions to generate those mappings based on frequently used operations. +type ProfileBimap struct { + wantedState map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload + currentState map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload +} + +// NewProfileBimap retuns a new ProfileBimap +func NewProfileBimap() *ProfileBimap { + return &ProfileBimap{ + map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload{}, + map[*fleet.MDMAppleProfilePayload]*fleet.MDMAppleProfilePayload{}, + } +} + +// GetMatchingProfileInDesiredState returns the addition key that matches the given removal +func (pb *ProfileBimap) GetMatchingProfileInDesiredState(removal *fleet.MDMAppleProfilePayload) (*fleet.MDMAppleProfilePayload, bool) { + value, ok := pb.currentState[removal] + return value, ok +} + +// GetMatchingProfileInCurrentState returns the removal key that matches the given addition +func (pb *ProfileBimap) GetMatchingProfileInCurrentState(addition *fleet.MDMAppleProfilePayload) (*fleet.MDMAppleProfilePayload, bool) { + key, ok := pb.wantedState[addition] + return key, ok +} + +// IntersectByIdentifierAndHostUUID populates the bimap matching the profiles by Identifier and HostUUID +func (pb *ProfileBimap) IntersectByIdentifierAndHostUUID(wantedProfiles, currentProfiles []*fleet.MDMAppleProfilePayload) { + key := func(p *fleet.MDMAppleProfilePayload) string { + return fmt.Sprintf("%s-%s", p.ProfileIdentifier, p.HostUUID) + } + + removeProfs := map[string]*fleet.MDMAppleProfilePayload{} + for _, p := range currentProfiles { + removeProfs[key(p)] = p + } + + for _, p := range wantedProfiles { + if pp, ok := removeProfs[key(p)]; ok { + pb.add(p, pp) + } + } +} + +func (pb *ProfileBimap) add(wantedProfile, currentProfile *fleet.MDMAppleProfilePayload) { + pb.wantedState[wantedProfile] = currentProfile + pb.currentState[currentProfile] = wantedProfile +} diff --git a/server/mdm/apple/profile_matcher.go b/server/mdm/apple/profile_matcher.go index 8ce3f946a4..807122a60d 100644 --- a/server/mdm/apple/profile_matcher.go +++ b/server/mdm/apple/profile_matcher.go @@ -61,7 +61,7 @@ func (p *profileMatcher) PreassignProfile(ctx context.Context, payload fleet.MDM // 2 fields set if the top-level Redis hash key was newly created: host uuid // and profile. If a group is provided, then it's 3 fields. - expectOnCreate := 2 + expectOnCreate := 3 args := []any{ // key is the prefix + the external identifier, all of this host's profiles // will be stored under that hash, keyed by the md5-hash. @@ -75,6 +75,8 @@ func (p *profileMatcher) PreassignProfile(ctx context.Context, payload fleet.MDM // the profile itself is stored under its md5-hash field, no-op if it // already existed. md5Hash, payload.Profile, + + md5Hash + "_exclude", payload.Exclude, } if payload.Group != "" { args = append(args, md5Hash+"_group", payload.Group) @@ -124,7 +126,7 @@ func (p *profileMatcher) RetrieveProfiles(ctx context.Context, externalHostIdent delete(profs, "host_uuid") for k, v := range profs { - if strings.HasSuffix(k, "_group") || v == "" { + if strings.HasSuffix(k, "_group") || v == "" || strings.HasSuffix(k, "_exclude") { // only look for profiles' hex hashes, the group information will be // retrieved only when a profile is found. Ignore empty values (e.g. // empty profile). @@ -142,6 +144,7 @@ func (p *profileMatcher) RetrieveProfiles(ctx context.Context, externalHostIdent Profile: []byte(v), Group: profs[k+"_group"], HexMD5Hash: k, + Exclude: profs[k+"_exclude"] == "1", }) } return hostProfs, nil diff --git a/server/mdm/apple/profile_matcher_test.go b/server/mdm/apple/profile_matcher_test.go index bb5dc6b059..3b7fd70311 100644 --- a/server/mdm/apple/profile_matcher_test.go +++ b/server/mdm/apple/profile_matcher_test.go @@ -62,6 +62,7 @@ func TestPreassignProfile(t *testing.T) { ExternalHostIdentifier: "abcd", HostUUID: "1234", Profile: generateProfile("p3", "p3", "Configuration", "p3"), + Exclude: true, } err = matcher.PreassignProfile(ctx, p3) require.NoError(t, err) @@ -108,21 +109,25 @@ func TestPreassignProfile(t *testing.T) { profs, err := redigo.StringMap(conn.Do("HGETALL", keyForExternalHostIdentifier("abcd"))) require.NoError(t, err) require.Equal(t, map[string]string{ - "host_uuid": "1234", - p1.HexMD5Hash(): string(p1.Profile), - p1.HexMD5Hash() + "_group": "g1", - p2.HexMD5Hash(): string(p2.Profile), - p2.HexMD5Hash() + "_group": "g2", - p3.HexMD5Hash(): string(p3.Profile), + "host_uuid": "1234", + p1.HexMD5Hash(): string(p1.Profile), + p1.HexMD5Hash() + "_group": "g1", + p1.HexMD5Hash() + "_exclude": "0", + p2.HexMD5Hash(): string(p2.Profile), + p2.HexMD5Hash() + "_group": "g2", + p2.HexMD5Hash() + "_exclude": "0", + p3.HexMD5Hash(): string(p3.Profile), + p3.HexMD5Hash() + "_exclude": "1", }, profs) // stored 1 profile in new host profs, err = redigo.StringMap(conn.Do("HGETALL", keyForExternalHostIdentifier("efgh"))) require.NoError(t, err) require.Equal(t, map[string]string{ - "host_uuid": "5678", - p4.HexMD5Hash(): string(p4.Profile), - p4.HexMD5Hash() + "_group": "g4", + "host_uuid": "5678", + p4.HexMD5Hash(): string(p4.Profile), + p4.HexMD5Hash() + "_group": "g4", + p4.HexMD5Hash() + "_exclude": "0", }, profs) } diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go index 45ab6b1ae5..2889ff8072 100644 --- a/server/mdm/microsoft/microsoft_mdm.go +++ b/server/mdm/microsoft/microsoft_mdm.go @@ -14,6 +14,12 @@ const ( // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/2681fd76-1997-4557-8963-cf656ab8d887 MDE2DiscoveryPath = MDMPath + "/discovery" + // AuthPath is the HTTP endpoint path that delivers the Security Token Servicefunctionality. + // The MS-MDE2 protocol is agnostic to the token format and value returned by this endpoint. + // See the section 3.2 on the MS-MDE2 specification for more details: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/27ed8c2c-0140-41ce-b2fa-c3d1a793ab4a + MDE2AuthPath = MDMPath + "/auth" + // MDE2PolicyPath is the HTTP endpoint path that delivers the X.509 Certificate Enrollment Policy (MS-XCEP) functionality. // This is the endpoint that process the GetPolicies and GetPoliciesResponse messages // See the section 3.3 on the MS-MDE2 specification for more details on this endpoint requirements: @@ -36,33 +42,38 @@ const ( // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/5b02c625-ced2-4a01-a8e1-da0ae84f5bb7 MDE2ManagementPath = MDMPath + "/management" + // MDE2TOSPath is the HTTP endpoint path that delivers Terms of Service Content + MDE2TOSPath = MDMPath + "/tos" + // These are the entry points for the Microsoft Device Enrollment (MS-MDE) and Microsoft Device Enrollment v2 (MS-MDE2) protocols. // These are required to be implemented by the MDM server to support user-driven enrollments MSEnrollEntryPoint = "/EnrollmentServer/Discovery.svc" MSManageEntryPoint = "/ManagementServer/MDM.svc" ) -// XML Namespaces used by the Microsoft Device Enrollment v2 protocol (MS-MDE2) +// XML Namespaces and type URLs used by the Microsoft Device Enrollment v2 protocol (MS-MDE2) const ( - DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment" - PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy" - EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512" - EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" - EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken" - EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc" - EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary" - EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment" - EnrollNSS = "http://www.w3.org/2003/05/soap-envelope" - EnrollNSA = "http://www.w3.org/2005/08/addressing" - EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance" - EnrollXSD = "http://www.w3.org/2001/XMLSchema" - EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" - ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics" - ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse" - ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse" - ActionNsEnroll = EnrollReq + "/RSTRC/wstep" - EnrollReqTypePKCS10 = EnrollReq + "#PKCS10" - EnrollReqTypePKCS7 = EnrollReq + "#PKCS7" + DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment" + PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy" + EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512" + EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" + EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken" + EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc" + EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary" + EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment" + EnrollNSS = "http://www.w3.org/2003/05/soap-envelope" + EnrollNSA = "http://www.w3.org/2005/08/addressing" + EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance" + EnrollXSD = "http://www.w3.org/2001/XMLSchema" + EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" + ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics" + ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse" + ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse" + ActionNsEnroll = EnrollReq + "/RSTRC/wstep" + EnrollReqTypePKCS10 = EnrollReq + "#PKCS10" + EnrollReqTypePKCS7 = EnrollReq + "#PKCS7" + BinarySecurityDeviceEnroll = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentUserToken" + BinarySecurityAzureEnroll = "urn:ietf:params:oauth:token-type:jwt" ) // Soap Error constants @@ -127,6 +138,12 @@ const ( // HTTP Content Type for SOAP responses SoapContentType = "application/soap+xml; charset=utf-8" + // HTTP Content Type for SyncML MDM responses + SyncMLContentType = "application/vnd.syncml.dm+xml" + + // HTTP Content Type for Webcontainer responses + WebContainerContentType = "text/html; charset=UTF-8" + // Minimal Key Length for SHA1WithRSA encryption PolicyMinKeyLength = "2048" @@ -214,6 +231,22 @@ const ( ReqSecTokenContextItemApplicationVersion = "ApplicationVersion" ReqSecTokenContextItemNotInOobe = "NotInOobe" ReqSecTokenContextItemRequestVersion = "RequestVersion" + + // APPRU query param expected by STS Auth endpoint + STSAuthAppRu = "appru" + + // Login related query param expected by STS Auth endpoint + STSLoginHint = "login_hint" + + // redirect_uri query param expected by TOS endpoint + TOCRedirectURI = "redirect_uri" + + // client-request-id query param expected by TOS endpoint + TOCReqID = "client-request-id" + + // Alert Command IDs + DeviceUnenrollmentID = "1226" + HostInitMessageID = "1201" ) func ResolveWindowsMDMDiscovery(serverURL string) (string, error) { @@ -228,6 +261,10 @@ func ResolveWindowsMDMEnroll(serverURL string) (string, error) { return commonmdm.ResolveURL(serverURL, MDE2EnrollPath, false) } +func ResolveWindowsMDMAuth(serverURL string) (string, error) { + return commonmdm.ResolveURL(serverURL, MDE2AuthPath, false) +} + func ResolveWindowsMDMManagement(serverURL string) (string, error) { return commonmdm.ResolveURL(serverURL, MDE2ManagementPath, false) } diff --git a/server/mdm/microsoft/wstep.go b/server/mdm/microsoft/wstep.go index 9ac857b593..54f8fb009d 100644 --- a/server/mdm/microsoft/wstep.go +++ b/server/mdm/microsoft/wstep.go @@ -1,6 +1,7 @@ package microsoft_mdm import ( + "bytes" "context" "crypto/rand" "crypto/rsa" @@ -17,6 +18,7 @@ import ( "time" "github.com/fleetdm/fleet/v4/server" + "github.com/golang-jwt/jwt/v4" "github.com/micromdm/nanomdm/cryptoutil" "go.mozilla.org/pkcs7" ) @@ -35,6 +37,12 @@ type CertManager interface { // IdentityCert returns the identity certificate of the depot. IdentityCert() x509.Certificate + // NewSTSAuthToken returns an STS auth token for the given UPN claim. + NewSTSAuthToken(upn string) (string, error) + + // GetSTSAuthTokenUPNClaim validates the given token and returns the UPN claim + GetSTSAuthTokenUPNClaim(token string) (string, error) + // TODO: implement other methods as needed: // - verify certificate-device association // - certificate lifecycle management (e.g., renewal, revocation) @@ -48,6 +56,18 @@ type CertStore interface { WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error } +type STSClaims struct { + UPN string `json:"upn"` + jwt.RegisteredClaims +} + +type AzureData struct { + UPN string + TenantID string + UniqueName string + SCP string +} + type manager struct { store CertStore @@ -88,10 +108,18 @@ func newManager(store CertStore, certPEM []byte, privKeyPEM []byte) (*manager, e } func (m *manager) IdentityFingerprint() string { + if m == nil { + return "" + } + return m.identityFingerprint } func (m *manager) IdentityCert() x509.Certificate { + if m == nil { + return x509.Certificate{} + } + return *m.identityCert } @@ -99,6 +127,10 @@ func (m *manager) IdentityCert() x509.Certificate { // subject is the DeviceID of the about to be MDM enrolled device, it will be used as the CommonName of the certificate // clientCSR is the client certificate signing request func (m *manager) SignClientCSR(ctx context.Context, subject string, clientCSR *x509.CertificateRequest) ([]byte, string, error) { + if m == nil { + return nil, "", errors.New("windows mdm identity keypair was not configured") + } + if m.identityCert == nil || m.identityPrivateKey == nil { return nil, "", errors.New("invalid identity certificate or private key") } @@ -132,6 +164,134 @@ func (m *manager) SignClientCSR(ctx context.Context, subject string, clientCSR * return rawSignedDER, CertFingerprintHexStr(signedCert), nil } +// NewSTSAuthToken returns an STS auth token for the given UPN claim. +func (m *manager) NewSTSAuthToken(upn string) (string, error) { + if m == nil { + return "", errors.New("windows mdm identity keypair was not configured") + } + + if m.identityCert == nil || m.identityPrivateKey == nil { + return "", errors.New("invalid identity certificate or private key") + } + + if len(upn) == 0 { + return "", errors.New("invalid upn field") + } + + // Create claims with upn field populated + claims := STSClaims{ + upn, + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Subject: "STSAuthToken", + }, + } + + // Create a new token with the claims and sign it with the private key + token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims) + signedToken, err := token.SignedString(m.identityPrivateKey) + if err != nil { + return "", fmt.Errorf("failed to sign STS token: %w", err) + } + + return signedToken, nil +} + +// GetSTSAuthToken validates the given token and returns the UPN claim +func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) { + if m == nil { + return "", errors.New("windows mdm identity keypair was not configured") + } + + if m.identityCert == nil || m.identityPrivateKey == nil { + return "", errors.New("invalid identity certificate or private key") + } + + if len(tokenStr) == 0 { + return "", errors.New("invalid STS token") + } + + // Since we used the private key to sign the tokens, we use the public counterpart to verify the signature + token, err := jwt.ParseWithClaims(tokenStr, &STSClaims{}, func(token *jwt.Token) (interface{}, error) { + return m.identityCert.PublicKey, nil + }) + if err != nil { + return "", fmt.Errorf("there was an error parsing the STS token claims: %w", err) + } + + if claims, ok := token.Claims.(*STSClaims); ok && token.Valid { + if len(claims.UPN) == 0 { + return "", errors.New("issue with UPN token claim") + } + + return claims.UPN, nil + } + + return "", errors.New("issue with STS token validation") +} + +// GetAzureAuthTokenClaims validates the given Azure AD token and returns +// UPN, TenantID, UniqueName, DeviceID +func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) { + if len(tokenStr) == 0 { + return AzureData{}, errors.New("invalid STS token") + } + + // Decode base64 token + tokenBytes, err := base64.StdEncoding.DecodeString(tokenStr) + if err != nil { + return AzureData{}, errors.New("invalid Azure JWT token") + } + + // Validate token format (header.payload.signature) + parts := bytes.Split(tokenBytes, []byte(".")) + if len(parts) != 3 { + return AzureData{}, errors.New("invalid Azure JWT format") + } + + // Parse JWT token + token, _, err := new(jwt.Parser).ParseUnverified(string(tokenBytes), jwt.MapClaims{}) + if err != nil { + return AzureData{}, errors.New("parse error Azure JWT content") + } + + // Parse JWT token + claims := token.Claims.(jwt.MapClaims) + + // Get UPN claim + upnClaim, ok := claims["upn"].(string) + if !ok || len(upnClaim) == 0 { + return AzureData{}, errors.New("invalid UPN claim") + } + + // Get TenantID claim + tenantIDClaim, ok := claims["tid"].(string) + if !ok || len(tenantIDClaim) == 0 { + return AzureData{}, errors.New("invalid TenantID claim") + } + + // Get UniqueName claim + uniqueNameClaim, ok := claims["unique_name"].(string) + if !ok { + return AzureData{}, errors.New("invalid UniqueName claim") + } + + // Get SCP claim + azureSCPClaim, ok := claims["scp"].(string) + if !ok || azureSCPClaim != "mdm_delegation" { + return AzureData{}, errors.New("invalid SCP claim") + } + + return AzureData{ + UPN: upnClaim, + TenantID: tenantIDClaim, + UniqueName: uniqueNameClaim, + SCP: azureSCPClaim, + }, nil +} + func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificate, csr *x509.CertificateRequest) (*x509.Certificate, error) { certRenewalPeriodInSecsInt, err := strconv.Atoi(PolicyCertRenewalPeriodInSecs) if err != nil { diff --git a/server/mdm/microsoft/wstep_test.go b/server/mdm/microsoft/wstep_test.go index 66c202a5b3..298b8a183b 100644 --- a/server/mdm/microsoft/wstep_test.go +++ b/server/mdm/microsoft/wstep_test.go @@ -75,12 +75,28 @@ func TestNewCertManager(t *testing.T) { require.Equal(t, wantIdentityFingerprint, m.identityFingerprint) } -func TestSignClientCSR(t *testing.T) { - // TODO -} +func TestSTSTokenSigningAndVerification(t *testing.T) { + var store CertStore -func TestGetClientCSR(t *testing.T) { - // TODO + cm, err := NewCertManager(store, testCert, testKey) + require.NoError(t, err) + require.NotNil(t, cm) + + // Get a New STS Auth token + upnEmail := "test@email.com" + stsToken, err := cm.NewSTSAuthToken(upnEmail) + require.NoError(t, err) + require.NotEmpty(t, stsToken) + + // Verify the STS Auth token + upnToken, err := cm.GetSTSAuthTokenUPNClaim(stsToken) + require.NoError(t, err) + require.NotEmpty(t, upnToken) + require.Equal(t, upnEmail, upnToken) + + // New invalid STS Auth token + _, err = cm.NewSTSAuthToken("") + require.ErrorContains(t, err, "invalid upn field") } func TestCertFingerprintHexStr(t *testing.T) { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 5fd0676c89..24cd9f45e2 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -536,10 +536,10 @@ type GetMDMAppleConfigProfileFunc func(ctx context.Context, profileID uint) (*fl type ListMDMAppleConfigProfilesFunc func(ctx context.Context, teamID *uint) ([]*fleet.MDMAppleConfigProfile, error) -type MatchMDMAppleConfigProfilesFunc func(ctx context.Context, hexMD5Hashes []string) ([]uint, error) - type DeleteMDMAppleConfigProfileFunc func(ctx context.Context, profileID uint) error +type BulkDeleteMDMAppleHostsConfigProfilesFunc func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error + type DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc func(ctx context.Context, teamID *uint, profileIdentifier string) error type GetHostMDMProfilesFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) @@ -1442,12 +1442,12 @@ type DataStore struct { ListMDMAppleConfigProfilesFunc ListMDMAppleConfigProfilesFunc ListMDMAppleConfigProfilesFuncInvoked bool - MatchMDMAppleConfigProfilesFunc MatchMDMAppleConfigProfilesFunc - MatchMDMAppleConfigProfilesFuncInvoked bool - DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFuncInvoked bool + BulkDeleteMDMAppleHostsConfigProfilesFunc BulkDeleteMDMAppleHostsConfigProfilesFunc + BulkDeleteMDMAppleHostsConfigProfilesFuncInvoked bool + DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc DeleteMDMAppleConfigProfileByTeamAndIdentifierFunc DeleteMDMAppleConfigProfileByTeamAndIdentifierFuncInvoked bool @@ -3450,13 +3450,6 @@ func (s *DataStore) ListMDMAppleConfigProfiles(ctx context.Context, teamID *uint return s.ListMDMAppleConfigProfilesFunc(ctx, teamID) } -func (s *DataStore) MatchMDMAppleConfigProfiles(ctx context.Context, hexMD5Hashes []string) ([]uint, error) { - s.mu.Lock() - s.MatchMDMAppleConfigProfilesFuncInvoked = true - s.mu.Unlock() - return s.MatchMDMAppleConfigProfilesFunc(ctx, hexMD5Hashes) -} - func (s *DataStore) DeleteMDMAppleConfigProfile(ctx context.Context, profileID uint) error { s.mu.Lock() s.DeleteMDMAppleConfigProfileFuncInvoked = true @@ -3464,6 +3457,13 @@ func (s *DataStore) DeleteMDMAppleConfigProfile(ctx context.Context, profileID u return s.DeleteMDMAppleConfigProfileFunc(ctx, profileID) } +func (s *DataStore) BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error { + s.mu.Lock() + s.BulkDeleteMDMAppleHostsConfigProfilesFuncInvoked = true + s.mu.Unlock() + return s.BulkDeleteMDMAppleHostsConfigProfilesFunc(ctx, payload) +} + func (s *DataStore) DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx context.Context, teamID *uint, profileIdentifier string) error { s.mu.Lock() s.DeleteMDMAppleConfigProfileByTeamAndIdentifierFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index d3c5252dfb..c59d4fa21f 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1412,6 +1412,43 @@ func (svc *Service) MDMAppleEraseDevice(ctx context.Context, hostID uint) error return fleet.ErrMissingLicense } +//////////////////////////////////////////////////////////////////////////////// +// Get profiles assigned to a host +//////////////////////////////////////////////////////////////////////////////// + +type getHostProfilesRequest struct { + ID uint `url:"id"` +} + +type getHostProfilesResponse struct { + HostID uint `json:"host_id"` + Profiles []*fleet.MDMAppleConfigProfile `json:"profiles"` + Err error `json:"error,omitempty"` +} + +func (r getHostProfilesResponse) error() error { return r.Err } + +func getHostProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*getHostProfilesRequest) + sums, err := svc.MDMListHostConfigurationProfiles(ctx, req.ID) + if err != nil { + return getHostProfilesResponse{Err: err}, nil + } + res := getHostProfilesResponse{Profiles: sums, HostID: req.ID} + if res.Profiles == nil { + res.Profiles = []*fleet.MDMAppleConfigProfile{} // return empty json array instead of json null + } + return res, nil +} + +func (svc *Service) MDMListHostConfigurationProfiles(ctx context.Context, hostID uint) ([]*fleet.MDMAppleConfigProfile, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // Batch Replace MDM Apple Profiles //////////////////////////////////////////////////////////////////////////////// @@ -2211,6 +2248,7 @@ func (svc *MDMAppleCheckinAndCommandService) Authenticate(r *mdm.Request, m *mdm HostSerial: info.HardwareSerial, HostDisplayName: info.DisplayName, InstalledFromDEP: info.InstalledFromDEP, + MDMPlatform: fleet.MDMPlatformApple, }) } @@ -2480,6 +2518,22 @@ func ReconcileProfiles( // with the new status, operation_type, etc. hostProfiles := make([]*fleet.MDMAppleBulkUpsertHostProfilePayload, 0, len(toInstall)+len(toRemove)) + // profileIntersection tracks profilesToAdd ∩ profilesToRemove, this is used to avoid: + // + // - Sending a RemoveProfile followed by an InstallProfile for a + // profile with an identifier that's already installed, which can cause + // racy behaviors. + // - Sending a InstallProfile command for a profile that's exactly the + // same as the one installed. Customers have reported that sending the + // command causes unwanted behavior. + profileIntersection := apple_mdm.NewProfileBimap() + profileIntersection.IntersectByIdentifierAndHostUUID(toInstall, toRemove) + + // hostProfilesToCleanup is used to track profiles that should be removed + // from the database directly without having to issue a RemoveProfile + // command. + hostProfilesToCleanup := []*fleet.MDMAppleProfilePayload{} + // install/removeTargets are maps from profileID -> command uuid and host // UUIDs as the underlying MDM services are optimized to send one command to // multiple hosts at the same time. Note that the same command uuid is used @@ -2491,6 +2545,26 @@ func ReconcileProfiles( } installTargets, removeTargets := make(map[uint]*cmdTarget), make(map[uint]*cmdTarget) for _, p := range toInstall { + if pp, ok := profileIntersection.GetMatchingProfileInCurrentState(p); ok { + // if the profile was in any other status than `failed` + // and the checksums match (the profiles are exactly + // the same) we don't send another InstallProfile + // command. + if pp.Status != &fleet.MDMAppleDeliveryFailed && bytes.Equal(pp.Checksum, p.Checksum) { + hostProfiles = append(hostProfiles, &fleet.MDMAppleBulkUpsertHostProfilePayload{ + ProfileID: p.ProfileID, + HostUUID: p.HostUUID, + ProfileIdentifier: p.ProfileIdentifier, + ProfileName: p.ProfileName, + Checksum: p.Checksum, + OperationType: pp.OperationType, + Status: pp.Status, + CommandUUID: pp.CommandUUID, + Detail: pp.Detail, + }) + continue + } + } toGetContents[p.ProfileID] = true target := installTargets[p.ProfileID] @@ -2516,6 +2590,11 @@ func ReconcileProfiles( } for _, p := range toRemove { + if _, ok := profileIntersection.GetMatchingProfileInDesiredState(p); ok { + hostProfilesToCleanup = append(hostProfilesToCleanup, p) + continue + } + target := removeTargets[p.ProfileID] if target == nil { target = &cmdTarget{ @@ -2538,6 +2617,15 @@ func ReconcileProfiles( }) } + // delete all profiles that have a matching identifier to be installed. + // This is to prevent sending both a `RemoveProfile` and an + // `InstallProfile` for the same identifier, which can cause race + // conditions. It's better to "update" the profile by sending a single + // `InstallProfile` command. + if err := ds.BulkDeleteMDMAppleHostsConfigProfiles(ctx, hostProfilesToCleanup); err != nil { + return ctxerr.Wrap(ctx, err, "deleting profiles that didn't change") + } + // First update all the profiles in the database before sending the // commands, this prevents race conditions where we could get a // response from the device before we set its status as 'pending' diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index a2a7eb3e03..c2d4450174 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -972,6 +972,7 @@ func TestMDMAuthenticate(t *testing.T) { require.Equal(t, serial, a.HostSerial) require.Equal(t, a.HostDisplayName, fmt.Sprintf("%s (%s)", model, serial)) require.False(t, a.InstalledFromDEP) + require.Equal(t, fleet.MDMPlatformApple, a.MDMPlatform) return nil } @@ -1944,6 +1945,11 @@ func TestMDMAppleReconcileProfiles(t *testing.T) { }, nil } + ds.BulkDeleteMDMAppleHostsConfigProfilesFunc = func(ctx context.Context, payload []*fleet.MDMAppleProfilePayload) error { + require.Empty(t, payload) + return nil + } + var enqueueFailForOp fleet.MDMAppleOperationType mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) { require.NotNil(t, cmd) diff --git a/server/service/devices.go b/server/service/devices.go index 945b21057b..695b2c4bc9 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -95,11 +95,12 @@ func (r *getDeviceHostRequest) deviceAuthToken() string { } type getDeviceHostResponse struct { - Host *HostDetailResponse `json:"host"` - OrgLogoURL string `json:"org_logo_url"` - Err error `json:"error,omitempty"` - License fleet.LicenseInfo `json:"license"` - GlobalConfig fleet.DeviceGlobalConfig `json:"global_config"` + Host *HostDetailResponse `json:"host"` + OrgLogoURL string `json:"org_logo_url"` + OrgLogoURLLightBackground string `json:"org_logo_url_light_background"` + Err error `json:"error,omitempty"` + License fleet.LicenseInfo `json:"license"` + GlobalConfig fleet.DeviceGlobalConfig `json:"global_config"` } func (r getDeviceHostResponse) error() error { return r.Err } diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go index cfe19c22b9..db63c49953 100644 --- a/server/service/endpoint_utils.go +++ b/server/service/endpoint_utils.go @@ -10,6 +10,7 @@ import ( "io" "net" "net/http" + "net/url" "reflect" "strconv" "strings" @@ -83,10 +84,9 @@ type requestDecoder interface { } // A value that implements bodyDecoder takes control of decoding the request -// body. Other fields such as url and query parameters are decoded prior to -// calling DecodeBody with the request's body as an io.Reader. +// body. type bodyDecoder interface { - DecodeBody(ctx context.Context, r io.Reader) error + DecodeBody(ctx context.Context, r io.Reader, u url.Values) error } // makeDecoder creates a decoder for the type for the struct passed on. If the @@ -304,7 +304,7 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { if isBodyDecoder { bd := v.Interface().(bodyDecoder) - if err := bd.DecodeBody(ctx, body); err != nil { + if err := bd.DecodeBody(ctx, body, r.URL.Query()); err != nil { return nil, err } } diff --git a/server/service/global_policies.go b/server/service/global_policies.go index 967d6e26b6..e2bfbc0a09 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -2,7 +2,6 @@ package service import ( "context" - "database/sql" "errors" "fmt" @@ -442,11 +441,6 @@ func (svc *Service) checkPolicySpecAuthorization(ctx context.Context, policies [ if err != nil { // This is so that the proper HTTP status code is returned svc.authz.SkipAuthorization(ctx) - - if errors.Is(err, sql.ErrNoRows) { - return newNotFoundError() - } - return ctxerr.Wrap(ctx, err, "getting team by name") } if err := svc.authz.Authorize(ctx, &fleet.Policy{ diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go index 4fb9e7778c..a99dc8ba19 100644 --- a/server/service/global_policies_test.go +++ b/server/service/global_policies_test.go @@ -2,7 +2,6 @@ package service import ( "context" - "database/sql" "testing" "github.com/fleetdm/fleet/v4/server/contexts/viewer" @@ -16,7 +15,7 @@ func TestCheckPolicySpecAuthorization(t *testing.T) { t.Run("when team not found", func(t *testing.T) { ds := new(mock.Store) ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } svc, ctx := newTestService(t, ds, nil, nil) @@ -31,7 +30,7 @@ func TestCheckPolicySpecAuthorization(t *testing.T) { ctx = viewer.NewContext(ctx, viewer.Viewer{User: user}) actual := svc.ApplyPolicySpecs(ctx, req) - var expected *notFoundError + var expected fleet.NotFoundError require.ErrorAs(t, actual, &expected) }) diff --git a/server/service/handler.go b/server/service/handler.go index b9beb02cb6..d5d81b0724 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -480,6 +480,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/lock", deviceLockEndpoint, deviceLockRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{}) + mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/profiles", getHostProfilesEndpoint, getHostProfilesRequest{}) mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{}) mdmAppleMW.PATCH("/api/_version_/fleet/mdm/apple/setup", updateMDMAppleSetupEndpoint, updateMDMAppleSetupRequest{}) @@ -596,16 +597,26 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // These endpoint are used by Microsoft devices during MDM device enrollment phase neWindowsMDM := ne.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM()) - // Microsoft MS-MDE Endpoints - // This endpoint is unauthenticated and is used by Microsoft devices to discover the MDM server + // Microsoft MS-MDE2 Endpoints + // This endpoint is unauthenticated and is used by Microsoft devices to discover the MDM server endpoints neWindowsMDM.POST(microsoft_mdm.MDE2DiscoveryPath, mdmMicrosoftDiscoveryEndpoint, SoapRequestContainer{}) + // This endpoint is unauthenticated and is used by Microsoft devices to retrieve the opaque STS auth token + neWindowsMDM.GET(microsoft_mdm.MDE2AuthPath, mdmMicrosoftAuthEndpoint, SoapRequestContainer{}) + // This endpoint is authenticated using the BinarySecurityToken header field neWindowsMDM.POST(microsoft_mdm.MDE2PolicyPath, mdmMicrosoftPolicyEndpoint, SoapRequestContainer{}) // This endpoint is authenticated using the BinarySecurityToken header field neWindowsMDM.POST(microsoft_mdm.MDE2EnrollPath, mdmMicrosoftEnrollEndpoint, SoapRequestContainer{}) + // This endpoint is unauthenticated for now + // It should be authenticated through TLS headers once proper implementation is in place + neWindowsMDM.POST(microsoft_mdm.MDE2ManagementPath, mdmMicrosoftManagementEndpoint, SyncMLReqMsgContainer{}) + + // This endpoint is unauthenticated and is used by to retrieve the MDM enrollment Terms of Use + neWindowsMDM.GET(microsoft_mdm.MDE2TOSPath, mdmMicrosoftTOSEndpoint, MDMWebContainer{}) + ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, EnrollOrbitRequest{}) // For some reason osquery does not provide a node key with the block data. diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index b456efd107..255fbe9a7f 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -522,6 +522,20 @@ func (s *integrationTestSuite) TestUserRolesSpec() { require.NoError(t, err) require.Len(t, user.Teams, 1) assert.Equal(t, fleet.RoleMaintainer, user.Teams[0].Role) + + spec = []byte(fmt.Sprintf(` + roles: + %s: + global_role: null + teams: + - role: maintainer + team: non-existent +`, + email)) + userRoleSpec = applyUserRoleSpecsRequest{} + err = yaml.Unmarshal(spec, &userRoleSpec.Spec) + require.NoError(t, err) + s.Do("POST", "/api/latest/fleet/users/roles/spec", &userRoleSpec, http.StatusBadRequest) } func (s *integrationTestSuite) TestGlobalSchedule() { @@ -937,11 +951,12 @@ func (s *integrationTestSuite) TestBulkDeleteHostByIDs() { require.NoError(t, err) } -func (s *integrationTestSuite) createHosts(t *testing.T) []*fleet.Host { +func (s *integrationTestSuite) createHosts(t *testing.T, platforms ...string) []*fleet.Host { var hosts []*fleet.Host - - platforms := []string{"debian", "rhel", "linux"} - for i := 0; i < 3; i++ { + if len(platforms) == 0 { + platforms = []string{"debian", "rhel", "linux"} + } + for i, platform := range platforms { host, err := s.ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), LabelUpdatedAt: time.Now(), @@ -951,7 +966,7 @@ func (s *integrationTestSuite) createHosts(t *testing.T) []*fleet.Host { NodeKey: ptr.String(fmt.Sprintf("%s%d", t.Name(), i)), UUID: uuid.New().String(), Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i), - Platform: platforms[i], + Platform: platform, }) require.NoError(t, err) hosts = append(hosts, host) @@ -980,7 +995,7 @@ func (s *integrationTestSuite) TestBulkDeleteHostsErrors() { func (s *integrationTestSuite) TestHostsCount() { t := s.T() - hosts := s.createHosts(t) + hosts := s.createHosts(t, "darwin", "darwin", "darwin") // set disk space information for some hosts require.NoError(t, s.ds.SetOrUpdateHostDisksSpace(context.Background(), hosts[0].ID, 10.0, 2.0)) // low disk @@ -1142,7 +1157,7 @@ func (s *integrationTestSuite) TestPacks() { func (s *integrationTestSuite) TestListHosts() { t := s.T() - hosts := s.createHosts(t) + hosts := s.createHosts(t, "darwin", "darwin", "darwin") // set disk space information for some hosts require.NoError(t, s.ds.SetOrUpdateHostDisksSpace(context.Background(), hosts[0].ID, 10.0, 2.0)) // low disk @@ -3057,7 +3072,7 @@ func (s *integrationTestSuite) TestLabels() { lbl2 := createResp.Label.Label // create hosts and add them to that label - hosts := s.createHosts(t) + hosts := s.createHosts(t, "darwin", "darwin", "darwin") for _, h := range hosts { err := s.ds.RecordLabelQueryExecutions(context.Background(), h, map[uint]*bool{lbl2.ID: ptr.Bool(true)}, time.Now(), false) require.NoError(t, err) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 55dee6a7f9..5c62d050f3 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -3589,3 +3589,34 @@ func (s *integrationEnterpriseTestSuite) setTokenForTest(t *testing.T, email, pa s.token = s.getCachedUserToken(email, password) } + +func (s *integrationEnterpriseTestSuite) TestDesktopEndpointWithInvalidPolicy() { + t := s.T() + + token := "abcd123" + host := createHostAndDeviceToken(t, s.ds, token) + + // Create an 'invalid' global policy for host + admin := s.users["admin1@example.com"] + err := s.ds.SaveUser(context.Background(), &admin) + require.NoError(t, err) + + policy, err := s.ds.NewGlobalPolicy(context.Background(), &admin.ID, fleet.PolicyPayload{ + Query: "SELECT 1 FROM table", + Name: "test", + Description: "Some invalid Query", + Resolution: "", + Platform: host.Platform, + Critical: false, + }) + require.NoError(t, err) + require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{policy.ID: nil}, time.Now(), false)) + + // Any 'invalid' policies should be ignored. + desktopRes := fleetDesktopResponse{} + res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&desktopRes)) + require.NoError(t, res.Body.Close()) + require.NoError(t, desktopRes.Err) + require.Equal(t, uint(0), *desktopRes.FailingPolicies) +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 3651293025..a4b836798a 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -373,20 +373,20 @@ func (s *integrationMDMTestSuite) TestProfileManagement() { err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}}) require.NoError(t, err) - var fleetdProfile bytes.Buffer + var globalFleetdProfile bytes.Buffer params := mobileconfig.FleetdProfileOptions{ EnrollSecret: t.Name(), ServerURL: s.server.URL, PayloadType: mobileconfig.FleetdConfigPayloadIdentifier, } - err = mobileconfig.FleetdProfileTemplate.Execute(&fleetdProfile, params) + err = mobileconfig.FleetdProfileTemplate.Execute(&globalFleetdProfile, params) require.NoError(t, err) globalProfiles := [][]byte{ mobileconfigForTest("N1", "I1"), mobileconfigForTest("N2", "I2"), } - wantGlobalProfiles := append(globalProfiles, fleetdProfile.Bytes()) + wantGlobalProfiles := append(globalProfiles, globalFleetdProfile.Bytes()) // add global profiles s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent) @@ -395,10 +395,21 @@ func (s *integrationMDMTestSuite) TestProfileManagement() { tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"}) require.NoError(t, err) + // add an enroll secret so the fleetd profiles differ + var teamResp teamEnrollSecretsResponse + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/secrets", tm.ID), + modifyTeamEnrollSecretsRequest{ + Secrets: []fleet.EnrollSecret{{Secret: "team1_enroll_sec"}}, + }, http.StatusOK, &teamResp) + teamProfiles := [][]byte{ mobileconfigForTest("N3", "I3"), } - wantTeamProfiles := append(teamProfiles, fleetdProfile.Bytes()) + var teamFleetdProfile bytes.Buffer + params.EnrollSecret = "team1_enroll_sec" + err = mobileconfig.FleetdProfileTemplate.Execute(&teamFleetdProfile, params) + require.NoError(t, err) + wantTeamProfiles := append(teamProfiles, teamFleetdProfile.Bytes()) // add profiles to the team s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: teamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID))) @@ -506,7 +517,7 @@ func (s *integrationMDMTestSuite) TestProfileManagement() { // verify that we should install the team profile require.ElementsMatch(t, wantTeamProfiles, installs) // verify that we should delete both profiles - require.ElementsMatch(t, []string{"I1", "I2", mobileconfig.FleetdConfigPayloadIdentifier}, removes) + require.ElementsMatch(t, []string{"I1", "I2"}, removes) // set new team profiles (delete + addition) teamProfiles = [][]byte{ @@ -604,7 +615,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.NotNil(t, h.TeamID) tm1, err := s.ds.Team(ctx, *h.TeamID) require.NoError(t, err) - require.Regexp(t, `^g1 \(\d+-\d+-\d+:\d+:\d+:\d+\.\d\d\d\)$`, tm1.Name) + require.Equal(t, "g1", tm1.Name) // it create activities for the new team, the profiles assigned to it, and // the host moved to it @@ -634,7 +645,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // create a team and set profiles to it tm2, err := s.ds.NewTeam(context.Background(), &fleet.Team{ - Name: "team2_" + t.Name(), + Name: "g1 - g4", }) require.NoError(t, err) prof4 := mobileconfigForTest("n4", "i4") @@ -702,9 +713,9 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // create a new mdm host enrolled in fleet mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) s.runWorker() - // make it part of team 4 + // make it part of team 2 s.Do("POST", "/api/v1/fleet/hosts/transfer", - addHostsToTeamRequest{TeamID: &tm4.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK) + addHostsToTeamRequest{TeamID: &tm2.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK) // simulate having its profiles installed mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { @@ -712,23 +723,22 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { return err }) - // preassign the MDM host to prof1, prof2 and prof4, should match existing - // team tm3 and tm4, and nothing be done since the host is already in tm4 + // preassign the MDM host using "g1" and "g4", should match existing + // team tm2, and nothing be done since the host is already in tm2 s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof1, Group: "g1"}}, http.StatusNoContent) - s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof2, Group: "g2"}}, http.StatusNoContent) s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "mdm2", HostUUID: mdmHost2.UUID, Profile: prof4, Group: "g4"}}, http.StatusNoContent) s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentRequest{ExternalHostIdentifier: "mdm2"}, http.StatusNoContent) - // the host is still part of tm4 + // the host is still part of tm2 h, err = s.ds.Host(ctx, mdmHost2.ID) require.NoError(t, err) require.NotNil(t, h.TeamID) - require.Equal(t, tm4.ID, *h.TeamID) + require.Equal(t, tm2.ID, *h.TeamID) // and its profiles have been left untouched hostProfs, err = s.ds.GetHostMDMProfiles(ctx, mdmHost2.UUID) require.NoError(t, err) - require.Len(t, hostProfs, 3) + require.Len(t, hostProfs, 2) sort.Slice(hostProfs, func(i, j int) bool { l, r := hostProfs[i], hostProfs[j] @@ -737,12 +747,377 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.Equal(t, "n1", hostProfs[0].Name) require.NotNil(t, hostProfs[0].Status) require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[0].Status) - require.Equal(t, "n2", hostProfs[1].Name) + require.Equal(t, "n4", hostProfs[1].Name) require.NotNil(t, hostProfs[1].Status) require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[1].Status) - require.Equal(t, "n4", hostProfs[2].Name) - require.NotNil(t, hostProfs[2].Status) - require.Equal(t, fleet.MDMAppleDeliveryVerifying, *hostProfs[2].Status) +} + +// while s.TestPuppetMatchPreassignProfiles focuses on many edge cases/extra +// checks around profile assignment, this test is mainly focused on +// simulating a few puppet runs in scenarios we want to support, and ensuring that: +// +// - different hosts end up in the right teams +// - teams get edited as expected +// - commands to add/remove profiles are issued adequately +func (s *integrationMDMTestSuite) TestPuppetRun() { + t := s.T() + ctx := context.Background() + + // define a few profiles + prof1, prof2, prof3, prof4 := mobileconfigForTest("n1", "i1"), + mobileconfigForTest("n2", "i2"), + mobileconfigForTest("n3", "i3"), + mobileconfigForTest("n4", "i4") + + // create three hosts + host1, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + host2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + host3, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + s.runWorker() + + // preassignAndMatch simulates the puppet module doing all the + // preassign/match calls for a given set of profiles. + preassignAndMatch := func(profs []fleet.MDMApplePreassignProfilePayload) { + require.NotEmpty(t, profs) + for _, prof := range profs { + s.Do( + "POST", + "/api/latest/fleet/mdm/apple/profiles/preassign", + preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: prof}, + http.StatusNoContent, + ) + } + s.Do( + "POST", + "/api/latest/fleet/mdm/apple/profiles/match", + matchMDMApplePreassignmentRequest{ExternalHostIdentifier: profs[0].ExternalHostIdentifier}, + http.StatusNoContent, + ) + } + + // node default { + // fleetdm::profile { 'n1': + // template => template('n1.mobileconfig.erb'), + // group => 'base', + // } + // + // fleetdm::profile { 'n2': + // template => template('n2.mobileconfig.erb'), + // group => 'workstations', + // } + // + // fleetdm::profile { 'n3': + // template => template('n3.mobileconfig.erb'), + // group => 'workstations', + // } + // + // if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' { + // fleetdm::profile { 'n4': + // template => template('fleetdm/n4.mobileconfig.erb'), + // group => 'kiosks', + // } + // } + puppetRun := func(host *fleet.Host) { + payload := []fleet.MDMApplePreassignProfilePayload{ + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof1, + Group: "base", + }, + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof2, + Group: "workstations", + }, + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof3, + Group: "workstations", + }, + } + + if host.UUID == host2.UUID { + payload = append(payload, fleet.MDMApplePreassignProfilePayload{ + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof4, + Group: "kiosks", + }) + } + + preassignAndMatch(payload) + } + + // host1 checks in + puppetRun(host1) + + // the host now belongs to a team + h1, err := s.ds.Host(ctx, host1.ID) + require.NoError(t, err) + require.NotNil(t, h1.TeamID) + + // the team has the right name + tm1, err := s.ds.Team(ctx, *h1.TeamID) + require.NoError(t, err) + require.Equal(t, "base - workstations", tm1.Name) + // and the right profiles + profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID) + require.NoError(t, err) + require.Len(t, profs, 3) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // host2 checks in + puppetRun(host2) + // a new team is created + h2, err := s.ds.Host(ctx, host2.ID) + require.NoError(t, err) + require.NotNil(t, h2.TeamID) + + // the team has the right name + tm2, err := s.ds.Team(ctx, *h2.TeamID) + require.NoError(t, err) + require.Equal(t, "base - kiosks - workstations", tm2.Name) + // and the right profiles + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID) + require.NoError(t, err) + require.Len(t, profs, 4) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) + require.Equal(t, prof4, []byte(profs[3].Mobileconfig)) + require.True(t, tm2.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // host3 checks in + puppetRun(host3) + // it belongs to the same team as host1 + h3, err := s.ds.Host(ctx, host3.ID) + require.NoError(t, err) + require.Equal(t, h1.TeamID, h3.TeamID) + + // prof2 is edited + oldProf2 := prof2 + prof2 = mobileconfigForTest("n2", "i2-v2") + // host3 checks in again + puppetRun(host3) + // still belongs to the same team + h3, err = s.ds.Host(ctx, host3.ID) + require.NoError(t, err) + require.Equal(t, tm1.ID, *h3.TeamID) + + // but the team has prof2 updated + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID) + require.NoError(t, err) + require.Len(t, profs, 3) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) + require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // host2 checks in, still belongs to the same team + puppetRun(host2) + h2, err = s.ds.Host(ctx, host2.ID) + require.NoError(t, err) + require.Equal(t, tm2.ID, *h2.TeamID) + + // but the team has prof2 updated as well + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID) + require.NoError(t, err) + require.Len(t, profs, 4) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) + require.Equal(t, prof4, []byte(profs[3].Mobileconfig)) + require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // the puppet manifest is changed, and prof3 is removed + // node default { + // fleetdm::profile { 'n1': + // template => template('n1.mobileconfig.erb'), + // group => 'base', + // } + // + // fleetdm::profile { 'n2': + // template => template('n2.mobileconfig.erb'), + // group => 'workstations', + // } + // + // if $facts['system_profiler']['hardware_uuid'] == 'host_2_uuid' { + // fleetdm::profile { 'n4': + // template => template('fleetdm/n4.mobileconfig.erb'), + // group => 'kiosks', + // } + // } + puppetRun = func(host *fleet.Host) { + payload := []fleet.MDMApplePreassignProfilePayload{ + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof1, + Group: "base", + }, + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof2, + Group: "workstations", + }, + } + + if host.UUID == host2.UUID { + payload = append(payload, fleet.MDMApplePreassignProfilePayload{ + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof4, + Group: "kiosks", + }) + } + + preassignAndMatch(payload) + } + + // host1 checks in again + puppetRun(host1) + // still belongs to the same team + h1, err = s.ds.Host(ctx, host1.ID) + require.NoError(t, err) + require.Equal(t, tm1.ID, *h1.TeamID) + + // but the team doesn't have prof3 anymore + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID) + require.NoError(t, err) + require.Len(t, profs, 2) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // same for host2 + puppetRun(host2) + h2, err = s.ds.Host(ctx, host2.ID) + require.NoError(t, err) + require.Equal(t, tm2.ID, *h2.TeamID) + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm2.ID) + require.NoError(t, err) + require.Len(t, profs, 3) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof4, []byte(profs[2].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // The puppet manifest is drastically updated, this time to use exclusions on host3: + // + // node default { + // fleetdm::profile { 'n1': + // template => template('n1.mobileconfig.erb'), + // group => 'base', + // } + // + // fleetdm::profile { 'n2': + // template => template('n2.mobileconfig.erb'), + // group => 'workstations', + // } + // + // if $facts['system_profiler']['hardware_uuid'] == 'host_3_uuid' { + // fleetdm::profile { 'n3': + // template => template('fleetdm/n3.mobileconfig.erb'), + // group => 'no-nudge', + // } + // } else { + // fleetdm::profile { 'n3': + // ensure => absent, + // template => template('fleetdm/n3.mobileconfig.erb'), + // group => 'workstations', + // } + // } + // } + puppetRun = func(host *fleet.Host) { + manifest := []fleet.MDMApplePreassignProfilePayload{ + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof1, + Group: "base", + }, + { + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof2, + Group: "workstations", + }, + } + + if host.UUID == host3.UUID { + manifest = append(manifest, fleet.MDMApplePreassignProfilePayload{ + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof3, + Group: "no-nudge", + Exclude: true, + }) + } else { + manifest = append(manifest, fleet.MDMApplePreassignProfilePayload{ + ExternalHostIdentifier: host.Hostname, + HostUUID: host.UUID, + Profile: prof3, + Group: "workstations", + }) + } + + preassignAndMatch(manifest) + } + + // host1 checks in + puppetRun(host1) + + // the host belongs to the same team + h1, err = s.ds.Host(ctx, host1.ID) + require.NoError(t, err) + require.Equal(t, tm1.ID, *h1.TeamID) + + // the team has the right profiles + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID) + require.NoError(t, err) + require.Len(t, profs, 3) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) + require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + + // host2 checks in + puppetRun(host2) + // it is assigned to tm1 + h2, err = s.ds.Host(ctx, host2.ID) + require.NoError(t, err) + require.Equal(t, tm1.ID, *h2.TeamID) + + // host3 checks in + puppetRun(host3) + + // it is assigned to a new team + h3, err = s.ds.Host(ctx, host3.ID) + require.NoError(t, err) + require.NotNil(t, h3.TeamID) + require.NotEqual(t, tm1.ID, *h3.TeamID) + require.NotEqual(t, tm2.ID, *h3.TeamID) + + // a new team is created + tm3, err := s.ds.Team(ctx, *h3.TeamID) + require.NoError(t, err) + require.Equal(t, "base - no-nudge - workstations", tm3.Name) + // and the right profiles + profs, err = s.ds.ListMDMAppleConfigProfiles(ctx, &tm3.ID) + require.NoError(t, err) + require.Len(t, profs, 2) + require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) + require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) + require.True(t, tm3.Config.MDM.MacOSSettings.EnableDiskEncryption) } func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestMDMClient) { @@ -925,7 +1300,7 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { require.JSONEq( t, fmt.Sprintf( - `{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": true}`, + `{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": true, "mdm_platform": "apple"}`, devices[0].SerialNumber, devices[0].Model, devices[0].SerialNumber, ), string(*activity.Details), @@ -1068,8 +1443,8 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() { } } require.Len(t, details, 2) - require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2])) - require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1])) + require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2])) + require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1])) // set an enroll secret var applyResp applyEnrollSecretSpecResponse @@ -1543,16 +1918,25 @@ func (s *integrationMDMTestSuite) TestMDMAppleListConfigProfiles() { testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"}) require.NoError(t, err) - t.Run("no profiles", func(t *testing.T) { - var resp listMDMAppleConfigProfilesResponse - s.DoJSON("GET", "/api/v1/fleet/mdm/apple/profiles", nil, http.StatusOK, &resp) - require.NotNil(t, resp.ConfigProfiles) // expect empty slice instead of nil - require.Len(t, resp.ConfigProfiles, 0) + mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t) + s.runWorker() - resp = listMDMAppleConfigProfilesResponse{} - s.DoJSON("GET", fmt.Sprintf(`/api/v1/fleet/mdm/apple/profiles?team_id=%d`, testTeam.ID), nil, http.StatusOK, &resp) - require.NotNil(t, resp.ConfigProfiles) // expect empty slice instead of nil - require.Len(t, resp.ConfigProfiles, 0) + t.Run("no profiles", func(t *testing.T) { + var listResp listMDMAppleConfigProfilesResponse + s.DoJSON("GET", "/api/v1/fleet/mdm/apple/profiles", nil, http.StatusOK, &listResp) + require.NotNil(t, listResp.ConfigProfiles) // expect empty slice instead of nil + require.Len(t, listResp.ConfigProfiles, 0) + + listResp = listMDMAppleConfigProfilesResponse{} + s.DoJSON("GET", fmt.Sprintf(`/api/v1/fleet/mdm/apple/profiles?team_id=%d`, testTeam.ID), nil, http.StatusOK, &listResp) + require.NotNil(t, listResp.ConfigProfiles) // expect empty slice instead of nil + require.Len(t, listResp.ConfigProfiles, 0) + + var hostProfilesResp getHostProfilesResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/mdm/hosts/%d/profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp) + require.NotNil(t, hostProfilesResp.Profiles) // expect empty slice instead of nil + require.Len(t, hostProfilesResp.Profiles, 0) + require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID) }) t.Run("with profiles", func(t *testing.T) { @@ -1598,6 +1982,33 @@ func (s *integrationMDMTestSuite) TestMDMAppleListConfigProfiles() { require.Fail(t, "unexpected profile name") } } + + var hostProfilesResp getHostProfilesResponse + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/mdm/hosts/%d/profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp) + require.NotNil(t, hostProfilesResp.Profiles) + require.Len(t, hostProfilesResp.Profiles, 1) + require.Equal(t, p1.Name, hostProfilesResp.Profiles[0].Name) + require.Equal(t, p1.Identifier, hostProfilesResp.Profiles[0].Identifier) + require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID) + + // add the host to a team + err = s.ds.AddHostsToTeam(ctx, &testTeam.ID, []uint{mdmHost.ID}) + require.NoError(t, err) + + hostProfilesResp = getHostProfilesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/mdm/hosts/%d/profiles", mdmHost.ID), nil, http.StatusOK, &hostProfilesResp) + require.NotNil(t, hostProfilesResp.Profiles) + require.Len(t, hostProfilesResp.Profiles, 2) + require.EqualValues(t, mdmHost.ID, hostProfilesResp.HostID) + for _, p := range resp.ConfigProfiles { + if p.Name == p2.Name { + require.Equal(t, p2.Identifier, p.Identifier) + } else if p.Name == p3.Name { + require.Equal(t, p3.Identifier, p.Identifier) + } else { + require.Fail(t, "unexpected profile name") + } + } }) } @@ -2625,7 +3036,7 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() { {Identifier: "G1", OperationType: fleet.MDMAppleOperationTypeRemove, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "G2", OperationType: fleet.MDMAppleOperationTypeRemove, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "T2.1", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, - {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryVerifying}, }, h2: { {Identifier: "G1", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryVerifying}, @@ -2879,14 +3290,14 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() { {Identifier: "T2.3", OperationType: fleet.MDMAppleOperationTypeRemove, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "G2b", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "G4", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, - {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryVerifying}, }, h3: { {Identifier: "T2.2b", OperationType: fleet.MDMAppleOperationTypeRemove, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "T2.3", OperationType: fleet.MDMAppleOperationTypeRemove, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "G2b", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, {Identifier: "G4", OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, - {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryPending}, + {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMAppleOperationTypeInstall, Status: &fleet.MDMAppleDeliveryVerifying}, }, }) @@ -4532,6 +4943,33 @@ func (s *integrationMDMTestSuite) TestGitOpsUserActions() { }, http.StatusForbidden, "team_id", strconv.Itoa(int(t2.ID))) } +func (s *integrationMDMTestSuite) TestOrgLogo() { + t := s.T() + + // change org logo urls + var acResp appConfigResponse + s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "org_info": { + "org_logo_url": "http://test-image.com", + "org_logo_url_light_background": "http://test-image-light.com" + } + }`), http.StatusOK, &acResp) + + // enroll a host + token := "token_test_migration" + host := createOrbitEnrolledHost(t, "darwin", "h", s.ds) + createDeviceTokenForHost(t, s.ds, host.ID, token) + + // check icon urls are correct + getDesktopResp := fleetDesktopResponse{} + res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK) + require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp)) + require.NoError(t, res.Body.Close()) + require.NoError(t, getDesktopResp.Err) + require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) + require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) +} + func (s *integrationMDMTestSuite) setTokenForTest(t *testing.T, email, password string) { oldToken := s.token t.Cleanup(func() { @@ -4895,6 +5333,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile) require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) + require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL) require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName) require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode) @@ -4956,6 +5395,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.True(t, getDesktopResp.Notifications.NeedsMDMMigration) require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile) require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) + require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL) require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName) require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode) @@ -4986,6 +5426,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) require.True(t, getDesktopResp.Notifications.RenewEnrollmentProfile) require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) + require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL) require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName) require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode) @@ -5015,6 +5456,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() { require.False(t, getDesktopResp.Notifications.NeedsMDMMigration) require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile) require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL) + require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground) require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL) require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName) require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode) @@ -5143,320 +5585,6 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) } -func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() { - t := s.T() - - // Preparing the Discovery Request message - requestBytes := []byte(` - - - http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover - urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478 - - http://www.w3.org/2005/08/addressing/anonymous - - https://mdmwindows.com:443/EnrollmentServer/Discovery.svc - - - - - demo@mdmwindows.com - 5.0 - CIMClient_Windows - 6.2.9200.2965 - 48 - - OnPremise - Federated - - - - - `) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid DiscoveryResponse message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("DiscoverResult", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("AuthPolicy", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestInvalidDiscoveryRequest() { - t := s.T() - - // Preparing the Discovery Request message - requestBytes := []byte(` - - - http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover - - http://www.w3.org/2005/08/addressing/anonymous - - https://mdmwindows.com:443/EnrollmentServer/Discovery.svc - - - - - demo@mdmwindows.com - 5.0 - CIMClient_Windows - 6.2.9200.2965 - 48 - - OnPremise - Federated - - - - - `) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid SoapFault message - resSoapMsg := string(resBytes) - - require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) - require.True(t, s.checkIfXMLTagContains("s:text", "invalid SOAP header: Header.MessageID", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestValidGetPoliciesRequest() { - t := s.T() - - // create a new Host to get the UUID on the DB - windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ - ID: 1, - OsqueryHostID: ptr.String("Desktop-ABCQWE"), - NodeKey: ptr.String("Desktop-ABCQWE"), - UUID: uuid.New().String(), - Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), - Platform: "windows", - }) - require.NoError(t, err) - - // Preparing the GetPolicies Request message - encodedBinToken, err := GetEncodedBinarySecurityToken(1, windowsHost.UUID) - require.NoError(t, err) - - requestBytes, err := s.newGetPoliciesMsg(encodedBinToken) - require.NoError(t, err) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid GetPoliciesResponse message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("GetPoliciesResponse", resSoapMsg)) - require.True(t, s.isXMLTagPresent("policyOIDReference", resSoapMsg)) - require.True(t, s.isXMLTagPresent("oIDReferenceID", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("validityPeriodSeconds", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("renewalPeriodSeconds", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("minimalKeyLength", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithInvalidUUID() { - t := s.T() - - // create a new Host to get the UUID on the DB - _, err := s.ds.NewHost(context.Background(), &fleet.Host{ - ID: 1, - OsqueryHostID: ptr.String("Desktop-ABCQWE"), - NodeKey: ptr.String("Desktop-ABCQWE"), - UUID: uuid.New().String(), - Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), - Platform: "windows", - }) - require.NoError(t, err) - - // Preparing the GetPolicies Request message - encodedBinToken, err := GetEncodedBinarySecurityToken(1, "not_exists") - require.NoError(t, err) - - requestBytes, err := s.newGetPoliciesMsg(encodedBinToken) - require.NoError(t, err) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid SoapFault message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) - require.True(t, s.checkIfXMLTagContains("s:text", "binarySecurityTokenValidation: host data cannot be found", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithNotElegibleHost() { - t := s.T() - - // create a new Host to get the UUID on the DB - linuxHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ - ID: 1, - OsqueryHostID: ptr.String("Ubuntu01"), - NodeKey: ptr.String("Ubuntu01"), - UUID: uuid.New().String(), - Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), - Platform: "linux", - }) - require.NoError(t, err) - - // Preparing the GetPolicies Request message - encodedBinToken, err := GetEncodedBinarySecurityToken(1, linuxHost.UUID) - require.NoError(t, err) - - requestBytes, err := s.newGetPoliciesMsg(encodedBinToken) - require.NoError(t, err) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid SoapFault message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) - require.True(t, s.checkIfXMLTagContains("s:text", "host is not elegible for Windows MDM enrollment", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequest() { - t := s.T() - - // create a new Host to get the UUID on the DB - windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ - ID: 1, - OsqueryHostID: ptr.String("Desktop-ABCQWE"), - NodeKey: ptr.String("Desktop-ABCQWE"), - UUID: uuid.New().String(), - Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), - Platform: "windows", - }) - require.NoError(t, err) - - // Delete the host from the list of MDM enrolled devices if present - _ = s.ds.MDMWindowsDeleteEnrolledDevice(context.Background(), windowsHost.UUID) - - // Preparing the RequestSecurityToken Request message - encodedBinToken, err := GetEncodedBinarySecurityToken(1, windowsHost.UUID) - require.NoError(t, err) - - requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true) - require.NoError(t, err) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("RequestSecurityTokenResponseCollection", resSoapMsg)) - require.True(t, s.isXMLTagPresent("DispositionMessage", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("TokenType", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("RequestID", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("BinarySecurityToken", resSoapMsg)) -} - -func (s *integrationMDMTestSuite) TestInvalidRequestSecurityTokenRequestWithMissingAdditionalContext() { - t := s.T() - - // create a new Host to get the UUID on the DB - windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ - ID: 1, - OsqueryHostID: ptr.String("Desktop-ABCQWE"), - NodeKey: ptr.String("Desktop-ABCQWE"), - UUID: uuid.New().String(), - Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), - Platform: "windows", - }) - require.NoError(t, err) - - // Preparing the RequestSecurityToken Request message - encodedBinToken, err := GetEncodedBinarySecurityToken(1, windowsHost.UUID) - require.NoError(t, err) - - requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, false) - require.NoError(t, err) - - resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) - - resBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) - - // Checking if SOAP response can be unmarshalled to an golang type - var xmlType interface{} - err = xml.Unmarshal(resBytes, &xmlType) - require.NoError(t, err) - - // Checking if SOAP response contains a valid SoapFault message - resSoapMsg := string(resBytes) - require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) - require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) - require.True(t, s.checkIfXMLTagContains("s:text", "ContextItem item DeviceType is not present", resSoapMsg)) -} - func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { t := s.T() @@ -5567,6 +5695,538 @@ func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC") } +func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() { + t := s.T() + + // Preparing the Discovery Request message + requestBytes := []byte(` + + + http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover + urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478 + + http://www.w3.org/2005/08/addressing/anonymous + + https://mdmwindows.com:443/EnrollmentServer/Discovery.svc + + + + + demo@mdmwindows.com + 5.0 + CIMClient_Windows + 6.2.9200.2965 + 48 + + OnPremise + Federated + + + + + `) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid DiscoveryResponse message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("DiscoverResult", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("AuthPolicy", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestInvalidDiscoveryRequest() { + t := s.T() + + // Preparing the Discovery Request message + requestBytes := []byte(` + + + http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover + + http://www.w3.org/2005/08/addressing/anonymous + + https://mdmwindows.com:443/EnrollmentServer/Discovery.svc + + + + + demo@mdmwindows.com + 5.0 + CIMClient_Windows + 6.2.9200.2965 + 48 + + OnPremise + Federated + + + + + `) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid SoapFault message + resSoapMsg := string(resBytes) + + require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) + require.True(t, s.checkIfXMLTagContains("s:text", "invalid SOAP header: Header.MessageID", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestNoEmailDiscoveryRequest() { + t := s.T() + + // Preparing the Discovery Request message + requestBytes := []byte(` + + + http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover + urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478 + + http://www.w3.org/2005/08/addressing/anonymous + + https://mdmwindows.com:443/EnrollmentServer/Discovery.svc + + + + + + 5.0 + CIMClient_Windows + 6.2.9200.2965 + 48 + + OnPremise + Federated + + + + + `) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid DiscoveryResponse message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("DiscoverResult", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("AuthPolicy", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg)) + require.True(t, !s.isXMLTagContentPresent("AuthenticationServiceUrl", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithDeviceToken() { + t := s.T() + + // create a new Host to get the UUID on the DB + windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("Desktop-ABCQWE"), + NodeKey: ptr.String("Desktop-ABCQWE"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), + Platform: "windows", + }) + require.NoError(t, err) + + // Preparing the GetPolicies Request message + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, windowsHost.UUID) + require.NoError(t, err) + + requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid GetPoliciesResponse message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("GetPoliciesResponse", resSoapMsg)) + require.True(t, s.isXMLTagPresent("policyOIDReference", resSoapMsg)) + require.True(t, s.isXMLTagPresent("oIDReferenceID", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("validityPeriodSeconds", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("renewalPeriodSeconds", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("minimalKeyLength", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithAzureToken() { + t := s.T() + + // Preparing the GetPolicies Request message with Azure JWT token + azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R" + requestBytes, err := s.newGetPoliciesMsg(false, azureADTok) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid GetPoliciesResponse message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("GetPoliciesResponse", resSoapMsg)) + require.True(t, s.isXMLTagPresent("policyOIDReference", resSoapMsg)) + require.True(t, s.isXMLTagPresent("oIDReferenceID", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("validityPeriodSeconds", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("renewalPeriodSeconds", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("minimalKeyLength", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithInvalidUUID() { + t := s.T() + + // create a new Host to get the UUID on the DB + _, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("Desktop-ABCQWE"), + NodeKey: ptr.String("Desktop-ABCQWE"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), + Platform: "windows", + }) + require.NoError(t, err) + + // Preparing the GetPolicies Request message + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, "not_exists") + require.NoError(t, err) + + requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid SoapFault message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) + require.True(t, s.checkIfXMLTagContains("s:text", "host data cannot be found", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithNotElegibleHost() { + t := s.T() + + // create a new Host to get the UUID on the DB + linuxHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("Ubuntu01"), + NodeKey: ptr.String("Ubuntu01"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), + Platform: "linux", + }) + require.NoError(t, err) + + // Preparing the GetPolicies Request message + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, linuxHost.UUID) + require.NoError(t, err) + + requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid SoapFault message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) + require.True(t, s.checkIfXMLTagContains("s:text", "host is not elegible for Windows MDM enrollment", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithDeviceToken() { + t := s.T() + + // create a new Host to get the UUID on the DB + windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("Desktop-ABCQWE"), + NodeKey: ptr.String("Desktop-ABCQWE"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), + Platform: "windows", + }) + require.NoError(t, err) + + // Delete the host from the list of MDM enrolled devices if present + _ = s.ds.MDMWindowsDeleteEnrolledDevice(context.Background(), windowsHost.UUID) + + // Preparing the RequestSecurityToken Request message + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, windowsHost.UUID) + require.NoError(t, err) + + requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, false) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message + resSoapMsg := string(resBytes) + + require.True(t, s.isXMLTagPresent("RequestSecurityTokenResponseCollection", resSoapMsg)) + require.True(t, s.isXMLTagPresent("DispositionMessage", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("TokenType", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("RequestID", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("BinarySecurityToken", resSoapMsg)) + + // Checking if an activity was created for the enrollment + s.lastActivityOfTypeMatches( + fleet.ActivityTypeMDMEnrolled{}.ActivityName(), + `{ + "mdm_platform": "microsoft", + "host_serial": "", + "installed_from_dep": false, + "host_display_name": "DESKTOP-0C89RC0" + }`, + 0) +} + +func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithAzureToken() { + t := s.T() + + // Preparing the SecurityToken Request message with Azure JWT token + azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R" + requestBytes, err := s.newSecurityTokenMsg(azureADTok, false, false) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("RequestSecurityTokenResponseCollection", resSoapMsg)) + require.True(t, s.isXMLTagPresent("DispositionMessage", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("TokenType", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("RequestID", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("BinarySecurityToken", resSoapMsg)) + + // Checking if an activity was created for the enrollment + s.lastActivityOfTypeMatches( + fleet.ActivityTypeMDMEnrolled{}.ActivityName(), + `{ + "mdm_platform": "microsoft", + "host_serial": "", + "installed_from_dep": false, + "host_display_name": "DESKTOP-0C89RC0" + }`, + 0) +} + +func (s *integrationMDMTestSuite) TestInvalidRequestSecurityTokenRequestWithMissingAdditionalContext() { + t := s.T() + + // create a new Host to get the UUID on the DB + windowsHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + ID: 1, + OsqueryHostID: ptr.String("Desktop-ABCQWE"), + NodeKey: ptr.String("Desktop-ABCQWE"), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()), + Platform: "windows", + }) + require.NoError(t, err) + + // Preparing the RequestSecurityToken Request message + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, windowsHost.UUID) + require.NoError(t, err) + + requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, true) + require.NoError(t, err) + + resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType) + + // Checking if SOAP response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid SoapFault message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg)) + require.True(t, s.checkIfXMLTagContains("s:text", "ContextItem item DeviceType is not present", resSoapMsg)) +} + +func (s *integrationMDMTestSuite) TestValidGetAuthRequest() { + t := s.T() + + // Target Endpoint url with query params + targetEndpointURL := microsoft_mdm.MDE2AuthPath + "?appru=ms-app%3A%2F%2Fwindows.immersivecontrolpanel&login_hint=demo%40mdmwindows.com" + resp := s.DoRaw("GET", targetEndpointURL, nil, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, resp.Header["Content-Type"], "text/html; charset=UTF-8") + require.NotEmpty(t, resBytes) + + // Checking response content + resContent := string(resBytes) + require.Contains(t, resContent, "inputToken.name = 'wresult'") + require.Contains(t, resContent, "form.action = \"ms-app://windows.immersivecontrolpanel\"") + require.Contains(t, resContent, "performPost()") + + // Getting token content + encodedToken := s.getRawTokenValue(resContent) + require.NotEmpty(t, encodedToken) +} + +func (s *integrationMDMTestSuite) TestInvalidGetAuthRequest() { + t := s.T() + + // Target Endpoint url with no login_hit query param + targetEndpointURL := microsoft_mdm.MDE2AuthPath + "?appru=ms-app%3A%2F%2Fwindows.immersivecontrolpanel" + resp := s.DoRaw("GET", targetEndpointURL, nil, http.StatusInternalServerError) + + resBytes, err := io.ReadAll(resp.Body) + resContent := string(resBytes) + require.NoError(t, err) + require.NotEmpty(t, resBytes) + require.Contains(t, resContent, "forbidden") +} + +func (s *integrationMDMTestSuite) TestValidGetTOC() { + t := s.T() + + resp := s.DoRaw("GET", microsoft_mdm.MDE2TOSPath+"?api-version=1.0&redirect_uri=ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin&client-request-id=f2cf3127-1e80-4d73-965d-42a3b84bdb40", nil, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.WebContainerContentType) + + resTOCcontent := string(resBytes) + require.Contains(t, resTOCcontent, "Microsoft.AAD.BrokerPlugin") + require.Contains(t, resTOCcontent, "IsAccepted=true") + require.Contains(t, resTOCcontent, "OpaqueBlob=") +} + +func (s *integrationMDMTestSuite) TestValidSyncMLRequestNoAuth() { + t := s.T() + + // Target Endpoint URL for the management endpoint + targetEndpointURL := microsoft_mdm.MDE2ManagementPath + + // Preparing the SyncML request + requestBytes, err := s.newSyncMLSessionMsg(targetEndpointURL) + require.NoError(t, err) + + resp := s.DoRaw("POST", targetEndpointURL, requestBytes, http.StatusOK) + + resBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SyncMLContentType) + + // Checking if SyncML response can be unmarshalled to an golang type + var xmlType interface{} + err = xml.Unmarshal(resBytes, &xmlType) + require.NoError(t, err) + + // Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message + resSoapMsg := string(resBytes) + require.True(t, s.isXMLTagPresent("SyncHdr", resSoapMsg)) + require.True(t, s.isXMLTagPresent("SyncBody", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("Exec", resSoapMsg)) + require.True(t, s.isXMLTagContentPresent("Add", resSoapMsg)) +} + // /////////////////////////////////////////////////////////////////////////// // Common helpers @@ -5578,6 +6238,24 @@ func (s *integrationMDMTestSuite) runWorker() { require.Empty(s.T(), pending) } +func (s *integrationMDMTestSuite) getRawTokenValue(content string) string { + // Create a regex object with the defined pattern + pattern := `inputToken.value\s*=\s*'([^']*)'` + regex := regexp.MustCompile(pattern) + + // Find the submatch using the regex pattern + submatches := regex.FindStringSubmatch(content) + + if len(submatches) >= 2 { + // Extract the content from the submatch + encodedToken := submatches[1] + + return encodedToken + } + + return "" +} + func (s *integrationMDMTestSuite) isXMLTagPresent(xmlTag string, payload string) bool { regex := fmt.Sprintf("<%s.*>", xmlTag) matched, err := regexp.MatchString(regex, payload) @@ -5609,11 +6287,17 @@ func (s *integrationMDMTestSuite) checkIfXMLTagContains(xmlTag string, xmlConten return true } -func (s *integrationMDMTestSuite) newGetPoliciesMsg(encodedBinToken string) ([]byte, error) { +func (s *integrationMDMTestSuite) newGetPoliciesMsg(deviceToken bool, encodedBinToken string) ([]byte, error) { if len(encodedBinToken) == 0 { return nil, errors.New("encodedBinToken is empty") } + // JWT token by default + tokType := microsoft_mdm.BinarySecurityAzureEnroll + if deviceToken { + tokType = microsoft_mdm.BinarySecurityDeviceEnroll + } + return []byte(` @@ -5624,7 +6308,7 @@ func (s *integrationMDMTestSuite) newGetPoliciesMsg(encodedBinToken string) ([]b https://mdmwindows.com/EnrollmentServer/Policy.svc - ` + encodedBinToken + ` + ` + encodedBinToken + ` @@ -5639,19 +6323,25 @@ func (s *integrationMDMTestSuite) newGetPoliciesMsg(encodedBinToken string) ([]b `), nil } -func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, missingContextItem bool) ([]byte, error) { +func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, deviceToken bool, missingContextItem bool) ([]byte, error) { if len(encodedBinToken) == 0 { return nil, errors.New("encodedBinToken is empty") } var reqSecTokenContextItemDeviceType []byte - if missingContextItem { + if !missingContextItem { reqSecTokenContextItemDeviceType = []byte( ` CIMClient_Windows `) } + // JWT token by default + tokType := microsoft_mdm.BinarySecurityAzureEnroll + if deviceToken { + tokType = microsoft_mdm.BinarySecurityDeviceEnroll + } + // Preparing the RequestSecurityToken Request message requestBytes := []byte( ` @@ -5663,7 +6353,7 @@ func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, mi https://mdmwindows.com/EnrollmentServer/Enrollment.svc - ` + encodedBinToken + ` + ` + encodedBinToken + ` @@ -5723,3 +6413,76 @@ func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, mi return requestBytes, nil } + +// TODO: Add support to add custom DeviceID when DeviceAuth is in place +func (s *integrationMDMTestSuite) newSyncMLSessionMsg(managementUrl string) ([]byte, error) { + if len(managementUrl) == 0 { + return nil, errors.New("managementUrl is empty") + } + + return []byte(` + + + 1.2 + DM/1.2 + 1 + 1 + + ` + managementUrl + ` + + + DB257C3A08778F4FB61E2749066C1F27 + + + + + 2 + 1201 + + + 3 + 1224 + + + com.microsoft/MDM/LoginStatus + + user + + + + 4 + + + ./DevInfo/DevId + + DB257C3A08778F4FB61E2749066C1F27 + + + + ./DevInfo/Man + + VMware, Inc. + + + + ./DevInfo/Mod + + VMware7,1 + + + + ./DevInfo/DmV + + 1.3 + + + + ./DevInfo/Lang + + en-US + + + + + `), nil +} diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 5010484939..e0ac9f2ebd 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -1,6 +1,7 @@ package service import ( + "bytes" "context" "crypto/x509" "encoding/base64" @@ -8,9 +9,13 @@ import ( "encoding/xml" "errors" "fmt" + "html" "io" "net/http" + "net/url" "strconv" + "strings" + "text/template" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -23,22 +28,31 @@ import ( ) type SoapRequestContainer struct { - Data *fleet.SoapRequest - Err error + Data *fleet.SoapRequest + Params url.Values + Err error } // MDM SOAP request decoder -func (req *SoapRequestContainer) DecodeBody(ctx context.Context, r io.Reader) error { +func (req *SoapRequestContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values) error { // Reading the request bytes reqBytes, err := io.ReadAll(r) if err != nil { return ctxerr.Wrap(ctx, err, "reading soap mdm request") } - // Unmarshal the XML data from the request into the SoapRequest struct - err = xml.Unmarshal(reqBytes, &req.Data) - if err != nil { - return ctxerr.Wrap(ctx, err, "unmarshalling soap mdm request") + // Set the request parameters + req.Params = u + + // Handle empty body scenario + req.Data = &fleet.SoapRequest{} + + if len(reqBytes) != 0 { + // Unmarshal the XML data from the request into the SoapRequest struct + err = xml.Unmarshal(reqBytes, &req.Data) + if err != nil { + return ctxerr.Wrap(ctx, err, "unmarshalling soap mdm request") + } } return nil @@ -51,11 +65,11 @@ type SoapResponseContainer struct { func (r SoapResponseContainer) error() error { return r.Err } -// hijackRender writes the response header and the RAW XML output +// hijackRender writes the response header and the RAW HTML output func (r SoapResponseContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { xmlRes, err := xml.MarshalIndent(r.Data, "", "\t") if err != nil { - logging.WithExtras(ctx, "Windows MDM SoapResponseContainer", err) + logging.WithExtras(ctx, "error with SoapResponseContainer", err) w.WriteHeader(http.StatusBadRequest) return } @@ -70,6 +84,110 @@ func (r SoapResponseContainer) hijackRender(ctx context.Context, w http.Response } } +type SyncMLReqMsgContainer struct { + Data *fleet.SyncMLMessage + Params url.Values + Err error +} + +// MDM SOAP request decoder +func (req *SyncMLReqMsgContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values) error { + // Reading the request bytes + reqBytes, err := io.ReadAll(r) + if err != nil { + return ctxerr.Wrap(ctx, err, "reading SyncML message request") + } + + // Set the request parameters + req.Params = u + + // Handle empty body scenario + req.Data = &fleet.SyncMLMessage{} + + if len(reqBytes) != 0 { + // Unmarshal the XML data from the request into the SoapRequest struct + err = xml.Unmarshal(reqBytes, &req.Data) + if err != nil { + return ctxerr.Wrap(ctx, err, "unmarshalling SyncML message request") + } + } + + return nil +} + +type SyncMLResponseMsgContainer struct { + Data *string + Err error +} + +func (r SyncMLResponseMsgContainer) error() error { return r.Err } + +// hijackRender writes the response header and the RAW HTML output +func (r SyncMLResponseMsgContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { + resData := []byte(*r.Data + "\n") + + w.Header().Set("Content-Type", mdm.SyncMLContentType) + w.Header().Set("Content-Length", strconv.Itoa(len(resData))) + w.WriteHeader(http.StatusOK) + if n, err := w.Write(resData); err != nil { + logging.WithExtras(ctx, "err", err, "written", n) + } +} + +type MDMWebContainer struct { + Data *string + Params url.Values + Err error +} + +// MDM SOAP request decoder +func (req *MDMWebContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values) error { + reqBytes, err := io.ReadAll(r) + if err != nil { + return ctxerr.Wrap(ctx, err, "reading Webcontainer HTML message request") + } + + // Set the request parameters + req.Params = u + + // Get req data + content := string(reqBytes) + req.Data = &content + + return nil +} + +func (req MDMWebContainer) error() error { return req.Err } + +// hijackRender writes the response header and the RAW HTML output +func (req MDMWebContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { + resData := []byte(*req.Data + "\n") + + w.Header().Set("Content-Type", mdm.WebContainerContentType) + w.Header().Set("Content-Length", strconv.Itoa(len(resData))) + w.WriteHeader(http.StatusOK) + if n, err := w.Write(resData); err != nil { + logging.WithExtras(ctx, "err", err, "written", n) + } +} + +type MDMAuthContainer struct { + Data *string + Err error +} + +func (r MDMAuthContainer) error() error { return r.Err } + +// hijackRender writes the response header and the RAW XML output +func (r MDMAuthContainer) hijackRender(ctx context.Context, w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + w.Header().Set("Content-Length", strconv.Itoa(len(*r.Data))) + w.WriteHeader(http.StatusOK) + if n, err := w.Write([]byte(*r.Data)); err != nil { + logging.WithExtras(ctx, "err", err, "written", n) + } +} + // getUtcTime returns the current timestamp plus the specified number of minutes, // formatted as "2006-01-02T15:04:05.000Z". func getUtcTime(minutes int) string { @@ -263,6 +381,14 @@ func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_typ } } +// getSTSAuthContent Retuns STS auth content +func getSTSAuthContent(data string) errorer { + return MDMAuthContainer{ + Data: &data, + Err: nil, + } +} + // getSoapResponseFault Returns a SoapResponse with a SoapFault on its body func getSoapResponseFault(relatesTo string, soapFault *mdm_types.SoapFault) errorer { if len(relatesTo) == 0 { @@ -408,11 +534,19 @@ func NewBinarySecurityTokenPayload(encodedToken string) (fleet.WindowsMDMAccessT return tokenPayload, nil } -// GetEncodedBinarySecurityToken returns the base64 form of a BinarySecurityTokenPayload -func GetEncodedBinarySecurityToken(typeID fleet.WindowsMDMEnrollmentType, hostUUID string) (string, error) { +// GetEncodedBinarySecurityToken returns the base64 form of a input payload +func GetEncodedBinarySecurityToken(typeID fleet.WindowsMDMEnrollmentType, payload string) (string, error) { var pld fleet.WindowsMDMAccessTokenPayload pld.Type = typeID - pld.Payload.HostUUID = hostUUID + + if typeID == fleet.WindowsMDMProgrammaticEnrollmentType { + pld.Payload.HostUUID = payload + } else if typeID == fleet.WindowsMDMAutomaticEnrollmentType { + pld.Payload.AuthToken = payload + } else { + return "", fmt.Errorf("invalid enrollment type: %v", typeID) + } + rawBytes, err := json.Marshal(pld) if err != nil { return "", err @@ -591,7 +725,7 @@ func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc } // Getting the DiscoveryResponse message - discoveryResponseMsg, err := svc.GetMDMMicrosoftDiscoveryResponse(ctx) + discoveryResponseMsg, err := svc.GetMDMMicrosoftDiscoveryResponse(ctx, req.Body.Discover.Request.EmailAddress) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil @@ -610,6 +744,31 @@ func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc }, nil } +// mdmMicrosoftAuthEndpoint handles the Security Token Service (STS) implementation +func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + params := request.(*SoapRequestContainer).Params + + // Sanity check on the expected query params + if !params.Has(mdm.STSAuthAppRu) || !params.Has(mdm.STSLoginHint) { + return getSTSAuthContent(""), errors.New("expected STS params are not present") + } + + appru := params.Get(mdm.STSAuthAppRu) + loginHint := params.Get(mdm.STSLoginHint) + + if (len(appru) == 0) || (len(loginHint) == 0) { + return getSTSAuthContent(""), errors.New("expected STS params are empty") + } + + // Getting the STS endpoint HTML content + stsAuthContent, err := svc.GetMDMMicrosoftSTSAuthResponse(ctx, appru, loginHint) + if err != nil { + return getSTSAuthContent(""), errors.New("error generating STS content") + } + + return getSTSAuthContent(stsAuthContent), nil +} + // mdmMicrosoftPolicyEndpoint handles the GetPolicies message and returns a valid GetPoliciesResponse message // GetPoliciesResponse message contains the certificate policies required for the next enrollment step. For more information about these messages, see [MS-XCEP] sections 3.1.4.1.1.1 and 3.1.4.1.1.2. func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { @@ -622,14 +781,14 @@ func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fl } // Binary security token should be extracted to ensure this is a valid call - binSecTokenData, err := req.GetBinarySecurityToken() + hdrSecToken, err := req.GetHeaderBinarySecurityToken() if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the GetPoliciesResponse message - policyResponseMsg, err := svc.GetMDMWindowsPolicyResponse(ctx, binSecTokenData) + policyResponseMsg, err := svc.GetMDMWindowsPolicyResponse(ctx, hdrSecToken) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil @@ -667,14 +826,14 @@ func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fl } // Binary security token should be extracted to ensure this is a valid call - binSecTokenData, err := req.GetBinarySecurityToken() + hdrBinarySecToken, err := req.GetHeaderBinarySecurityToken() if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil } // Getting the RequestSecurityTokenResponseCollection message - enrollResponseMsg, err := svc.GetMDMWindowsEnrollResponse(ctx, reqSecurityTokenMsg, binSecTokenData) + enrollResponseMsg, err := svc.GetMDMWindowsEnrollResponse(ctx, reqSecurityTokenMsg, hdrBinarySecToken) if err != nil { soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) return getSoapResponseFault(req.GetMessageID(), soapFault), nil @@ -693,45 +852,132 @@ func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fl }, nil } -// validateBinarySecurityToken checks if the provided token is valid -func (svc *Service) validateBinarySecurityToken(ctx context.Context, encodedBinarySecToken string) error { - if len(encodedBinarySecToken) == 0 { - return errors.New("binarySecurityTokenValidation: encoded token is invalid") +// mdmMicrosoftManagementEndpoint handles the OMA DM management sessions +// It receives a SyncML message with protocol commands, it process the commands and responds with a +// SyncML message with protocol commands results and more protocol commands for the calling host +// Note: This logic needs to be improved with better SyncML message parsing, better message tracking +// and better security authentication (done through TLS and in-message hash) +func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + reqSyncML := request.(*SyncMLReqMsgContainer).Data + + // Checking first if incoming SyncML message is valid and returning error if this is not the case + if err := reqSyncML.IsValidSyncMLMsg(); err != nil { + soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEFault, err) + return getSoapResponseFault(strconv.Itoa(reqSyncML.Header.MsgID), soapFault), nil } - // Getting the Binary Security Token Payload - binSecToken, err := NewBinarySecurityTokenPayload(encodedBinarySecToken) + // Getting the RequestSecurityTokenResponseCollection message + resSyncML, err := svc.GetMDMWindowsManagementResponse(ctx, reqSyncML) if err != nil { - return fmt.Errorf("binarySecurityTokenValidation: token creation error %v", err) + soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) + return getSoapResponseFault(strconv.Itoa(reqSyncML.Header.MsgID), soapFault), nil } - // Validating the Binary Security Token Payload - err = binSecToken.IsValidToken() + return SyncMLResponseMsgContainer{ + Data: resSyncML, + Err: nil, + }, nil +} + +// mdmMicrosoftTOSEndpoint handles the TOS content for the incoming MDM enrollment request +func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + params := request.(*MDMWebContainer).Params + + // Sanity check on the expected query params + if !params.Has(mdm.TOCRedirectURI) || !params.Has(mdm.TOCReqID) { + soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, errors.New("invalid params")) + return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil + } + + redirectURI := params.Get(mdm.TOCRedirectURI) + reqID := params.Get(mdm.TOCReqID) + + // Getting the TOS content message + resTOCData, err := svc.GetMDMWindowsTOSContent(ctx, redirectURI, reqID) if err != nil { - return fmt.Errorf("binarySecurityTokenValidation: invalid token data %v", err) + soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err) + return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil } - // Validating the Binary Security Token Type used on Programmatic Enrollments - if binSecToken.Type == mdm_types.WindowsMDMProgrammaticEnrollmentType { - host, err := svc.ds.HostByIdentifier(ctx, binSecToken.Payload.HostUUID) + return MDMWebContainer{ + Data: &resTOCData, + Err: nil, + }, nil +} + +// authBinarySecurityToken checks if the provided token is valid +func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (string, error) { + if authToken == nil { + return "", errors.New("authToken is empty") + } + + err := authToken.IsValidToken() + if err != nil { + return "", errors.New("authToken is not valid") + } + + // Tokens that were generated by enrollment client + if authToken.IsDeviceToken() { + + // Getting the Binary Security Token Payload + binSecToken, err := NewBinarySecurityTokenPayload(authToken.Content) if err != nil { - return fmt.Errorf("binarySecurityTokenValidation: host data cannot be found %v", err) + return "", fmt.Errorf("token creation error %v", err) } - // This ensures that only hosts that are eligible for Windows enrollment can be enrolled - if !host.IsEligibleForWindowsMDMEnrollment() { - return errors.New("binarySecurityTokenValidation: host is not elegible for Windows MDM enrollment") + // Validating the Binary Security Token Payload + err = binSecToken.IsValidToken() + if err != nil { + return "", fmt.Errorf("invalid token data %v", err) + } + + // Validating the Binary Security Token Type used on Programmatic Enrollments + if binSecToken.Type == mdm_types.WindowsMDMProgrammaticEnrollmentType { + host, err := svc.ds.HostByIdentifier(ctx, binSecToken.Payload.HostUUID) + if err != nil { + return "", fmt.Errorf("host data cannot be found %v", err) + } + + // This ensures that only hosts that are eligible for Windows enrollment can be enrolled + if !host.IsEligibleForWindowsMDMEnrollment() { + return "", errors.New("host is not elegible for Windows MDM enrollment") + } + + // No errors, token is authorized + return binSecToken.Payload.HostUUID, nil + } + + // Validating the Binary Security Token Type used on Automatic Enrollments (returned by STS Auth Endpoint) + if binSecToken.Type == mdm_types.WindowsMDMAutomaticEnrollmentType { + + upnToken, err := svc.wstepCertManager.GetSTSAuthTokenUPNClaim(binSecToken.Payload.AuthToken) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "issue retrieving UPN from Auth token") + } + + // No errors, token is authorized + return upnToken, nil + } + } + + // Validating the Binary Security Token Type used on Automatic Enrollments + if authToken.IsAzureJWTToken() { + + // Validate the JWT Auth token by retreving its claims + tokenData, err := mdm.GetAzureAuthTokenClaims(authToken.Content) + if err != nil { + return "", fmt.Errorf("binary security token claim failed: %v", err) } // No errors, token is authorized - return nil + return tokenData.UPN, nil } - return errors.New("binarySecurityTokenValidation: token is not authorized") + return "", errors.New("token is not authorized") } // GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message -func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context) (*fleet.DiscoverResponse, error) { +func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEmail string) (*fleet.DiscoverResponse, error) { // skipauth: This endpoint does not use authentication svc.authz.SkipAuthorization(ctx) @@ -760,14 +1006,68 @@ func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context) (*flee return &discoveryMsg, nil } +// GetMDMMicrosoftSTSAuthResponse returns a valid Security Token Service (STS) page content +func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru string, loginHint string) (string, error) { + // skipauth: This endpoint does not use authentication + svc.authz.SkipAuthorization(ctx) + + // Dummy data will be returned as part of the token as user-driven enrollment is not supported yet + // In the future, the following calls would have to be made to support user-driven enrollment + // encodedBST will carry the token to return + // authToken, err := svc.wstepCertManager.NewSTSAuthToken(loginHint) + // encodedBST, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMAutomaticEnrollmentType, authToken) + encodedBST := "user_driven_enrollment_not_implemented" + + // STS Auth Endpoint returns HTML content that gets render in a webview container + // The webview container expect a POST request to the appru URL with the wresult parameter set to the auth token + // The security token in wresult is later passed back in + // This string is opaque to the enrollment client; the client does not interpret the string. + // The returned HTML content contains a JS script that will perform a POST request to the appru URL automatically + // This will set the wresult parameter to the value of auth token + tmpl, err := template.New("").Parse(` + + `) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "STS content template") + } + + var htmlBuf bytes.Buffer + err = tmpl.Execute(&htmlBuf, map[string]string{"ActionURL": appru, "Token": encodedBST}) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "creation of STS content") + } + + return htmlBuf.String(), nil +} + // GetMDMWindowsPolicyResponse returns a valid GetPoliciesResponse message -func (svc *Service) GetMDMWindowsPolicyResponse(ctx context.Context, authToken string) (*fleet.GetPoliciesResponse, error) { - if len(authToken) == 0 { - return nil, fleet.NewInvalidArgumentError("policy response", "authToken is empty") +func (svc *Service) GetMDMWindowsPolicyResponse(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (*fleet.GetPoliciesResponse, error) { + if authToken == nil { + return nil, fleet.NewInvalidArgumentError("policy response", "authToken is invalid") } // Validate the binary security token - err := svc.validateBinarySecurityToken(ctx, authToken) + _, err := svc.authBinarySecurityToken(ctx, authToken) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validate binary security token") } @@ -787,13 +1087,13 @@ func (svc *Service) GetMDMWindowsPolicyResponse(ctx context.Context, authToken s // GetMDMWindowsEnrollResponse returns a valid RequestSecurityTokenResponseCollection message // secTokenMsg is the RequestSecurityToken message // authToken is the base64 encoded binary security token -func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken, authToken string) (*fleet.RequestSecurityTokenResponseCollection, error) { - if len(authToken) == 0 { - return nil, fleet.NewInvalidArgumentError("enroll response", "authToken is empty") +func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken, authToken *fleet.HeaderBinarySecurityToken) (*fleet.RequestSecurityTokenResponseCollection, error) { + if authToken == nil { + return nil, fleet.NewInvalidArgumentError("enroll response", "authToken is not present") } - // Validate the binary security token - err := svc.validateBinarySecurityToken(ctx, authToken) + // Auth the binary security token + userID, err := svc.authBinarySecurityToken(ctx, authToken) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validate binary security token") } @@ -804,7 +1104,7 @@ func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg return nil, ctxerr.Wrap(ctx, err, "device enroll check") } - // Getting the the device provisioning information in the form of a WapProvisioningDoc + // Getting the device provisioning information in the form of a WapProvisioningDoc deviceProvisioning, err := svc.getDeviceProvisioningInformation(ctx, secTokenMsg) if err != nil { return nil, ctxerr.Wrap(ctx, err, "device provisioning information") @@ -819,11 +1119,16 @@ func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg return nil, ctxerr.Wrap(ctx, err, "creation of RequestSecurityTokenResponseCollection message") } - // RequestSecurityTokenResponseCollection message is ready - // The identity and provisioning information will be sent to the Windows MDM Enrollment Client + // RequestSecurityTokenResponseCollection message is ready. The identity + // and provisioning information will be sent to the Windows MDM + // Enrollment Client - // But before doing that, let's save the device information to the list of MDM enrolled MDM devices - err = svc.storeWindowsMDMEnrolledDevice(ctx, secTokenMsg) + // But before doing that, let's save the device information to the list + // of MDM enrolled MDM devices + // + // This method also creates the relevant enrollment activity as it has + // access to the device information. + err = svc.storeWindowsMDMEnrolledDevice(ctx, userID, secTokenMsg) if err != nil { return nil, ctxerr.Wrap(ctx, err, "enrolled device information cannot be stored") } @@ -831,6 +1136,198 @@ func (svc *Service) GetMDMWindowsEnrollResponse(ctx context.Context, secTokenMsg return &secTokenResponseCollectionMsg, nil } +// GetMDMWindowsManagementResponse returns a valid SyncML response message +func (svc *Service) GetMDMWindowsManagementResponse(ctx context.Context, reqSyncML *fleet.SyncMLMessage) (*string, error) { + if reqSyncML == nil { + return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present") + } + + // TODO: The following logic should happen here + // - TLS based auth + // - Device auth based on Source/LocURI DeviceID information + // (this should be present on Enrollment DB) + // - Processing of incoming protocol commands (Alerts mostly + // - MS-MDM session management + // - Inclusion of queued protocol commands should be performed here + // - Tracking of message acknowledgements through Message queue + + // Getting the management response message + resSyncMLmsg, err := svc.getManagementResponse(ctx, reqSyncML) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "device provisioning information") + } + + // Token is authorized + svc.authz.SkipAuthorization(ctx) + + return resSyncMLmsg, nil +} + +// GetMDMWindowsTOSContent returns valid TOC content +func (svc *Service) GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) { + tmpl, err := template.New("").Parse(` + + + + + +
    + +
    +

    Terms and conditions

    +
    Terms and Conditions PDF content should go here
    +
    + + + + `) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "issue generating TOS content") + } + + var htmlBuf bytes.Buffer + err = tmpl.Execute(&htmlBuf, map[string]string{"RedirectURL": redirectUri, "ClientData": reqID}) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "executing TOS template content") + } + + // skipauth: This endpoint does not use authentication + svc.authz.SkipAuthorization(ctx) + + return htmlBuf.String(), nil +} + +func (svc *Service) getManagementResponse(ctx context.Context, reqSyncML *fleet.SyncMLMessage) (*string, error) { + if reqSyncML == nil { + return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present") + } + + // cmdID tracks the command sequence + cmdID := 0 + + // Retrieve the MessageID from the syncml req body + deviceID := reqSyncML.Header.Source + + // Retrieve the sessionID from the syncml req body + sessionID := reqSyncML.Header.SessionID + + // Retrieve the msgID from the syncml req body + msgID := reqSyncML.Header.MsgID + + // Getting the management URL message content + appCfg, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + + urlManagementEndpoint, err := mdm.ResolveWindowsMDMManagement(appCfg.ServerSettings.ServerURL) + if err != nil { + return nil, err + } + + // Checking the SyncML message types + var response string + if isSessionInitializationMessage(reqSyncML.Body) { + // Create response payload - MDM SyncML configuration profiles commands will be enforced here + response = ` + + + + 1.2 + DM/1.2 + ` + strconv.Itoa(sessionID) + ` + ` + strconv.Itoa(msgID) + ` + + ` + deviceID + ` + + + ` + urlManagementEndpoint + ` + + + + + ` + getNextCmdID(&cmdID) + ` + ` + strconv.Itoa(msgID) + ` + 0 + SyncHdr + 200 + + + ` + getNextCmdID(&cmdID) + ` + ` + strconv.Itoa(msgID) + ` + 2 + Alert + 200 + + + ` + getNextCmdID(&cmdID) + ` + ` + strconv.Itoa(msgID) + ` + 3 + Alert + 200 + + + ` + getNextCmdID(&cmdID) + ` + ` + strconv.Itoa(msgID) + ` + 4 + Replace + 200 + + ` + svc.getConfigProfilesToEnforce(ctx, &cmdID) + ` + + + ` + } else { + // Acknowledge SyncML messages sent by host + response = ` + + + + 1.2 + DM/1.2 + ` + strconv.Itoa(sessionID) + ` + ` + strconv.Itoa(msgID) + ` + + ` + deviceID + ` + + + ` + urlManagementEndpoint + ` + + + + + ` + getNextCmdID(&cmdID) + ` + ` + strconv.Itoa(msgID) + ` + 0 + SyncHdr + 200 + + + + ` + } + + // Create a replacer to replace both "\n" and "\t" + replacer := strings.NewReplacer("\n", "", "\t", "") + + // Use the replacer on the string representation of xmlContent + responseRaw := replacer.Replace(response) + + return &responseRaw, nil +} + // removeWindowsDeviceIfAlreadyMDMEnrolled removes the device if already MDM enrolled // HW DeviceID is used to check the list of enrolled devices func (svc *Service) removeWindowsDeviceIfAlreadyMDMEnrolled(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) error { @@ -939,7 +1436,7 @@ func (svc *Service) getDeviceProvisioningInformation(ctx context.Context, secTok } // storeWindowsMDMEnrolledDevice stores the device information to the list of MDM enrolled devices -func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) error { +func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID string, secTokenMsg *fleet.RequestSecurityToken) error { const ( error_tag = "windows MDM enrolled storage: " ) @@ -971,7 +1468,7 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, secTokenM // Getting the Enroll RequestVersion context information from the RequestSecurityToken msg reqEnrollVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemRequestVersion) if err != nil { - return fmt.Errorf("%s %v", error_tag, err) + reqEnrollVersion = "request_version_not_present" } // Getting the RequestVersion context information from the RequestSecurityToken msg @@ -994,7 +1491,7 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, secTokenM MDMDeviceType: reqDeviceType, MDMDeviceName: reqDeviceName, MDMEnrollType: reqEnrollType, - MDMEnrollUserID: "", // No user information is available at this point + MDMEnrollUserID: userID, // This could be Host UUID or UPN email MDMEnrollProtoVersion: reqEnrollVersion, MDMEnrollClientVersion: reqAppVersion, MDMNotInOOBE: false, @@ -1004,6 +1501,19 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, secTokenM return err } + err = svc.ds.NewActivity(ctx, nil, &fleet.ActivityTypeMDMEnrolled{ + HostDisplayName: reqDeviceName, + MDMPlatform: fleet.MDMPlatformMicrosoft, + }) + if err != nil { + // only logging, the device is enrolled at this point, and we + // wouldn't want to fail the request because there was a problem + // creating an activity feed item. + logging.WithExtras(logging.WithNoUser(ctx), + "msg", "failed to generate windows MDM enrolled activity", + ) + } + return nil } @@ -1027,6 +1537,10 @@ func (svc *Service) GetAuthorizedSoapFault(ctx context.Context, eType string, or } func (svc *Service) SignMDMMicrosoftClientCSR(ctx context.Context, subject string, csr *x509.CertificateRequest) ([]byte, string, error) { + if svc.wstepCertManager == nil { + return nil, "", errors.New("windows mdm identity keypair was not configured") + } + cert, fpHex, err := svc.wstepCertManager.SignClientCSR(ctx, subject, csr) if err != nil { return nil, "signing wstep client csr", ctxerr.Wrap(ctx, err) @@ -1038,3 +1552,100 @@ func (svc *Service) SignMDMMicrosoftClientCSR(ctx context.Context, subject strin return cert, fpHex, nil } + +func (svc *Service) getConfigProfilesToEnforce(ctx context.Context, commandID *int) string { + // Getting the management URL + appCfg, _ := svc.ds.AppConfig(ctx) + fleetEnrollUrl := appCfg.ServerSettings.ServerURL + + // Getting the global enrollment secret + var globalEnrollSecret string + secrets, err := svc.ds.GetEnrollSecrets(ctx, nil) + if err != nil { + return "" + } + + for _, secret := range secrets { + if secret.TeamID == nil { + globalEnrollSecret = secret.Secret + break + } + } + + // keeping the same GUID will prevent the MSI to be installed multiple times - it will be + // installed only the first time the message is issued. + // FleetURL and FleetSecret properties are passed to the Fleet MSI + // See here for more information: https://learn.microsoft.com/en-us/windows/win32/msi/command-line-options + installCommandPayload := ` + + + + https://download.fleetdm.com/fleetd-base.msi + + + + 7D127BA8F8CC5937DB3052E2632D672120217D910E271A58565BBA780ED8F05C + + + /quiet FleetURL="` + fleetEnrollUrl + `" FleetSecret="` + globalEnrollSecret + `" + 10 + 1 + 5 + + + ` + + newCmds := ` + ` + getNextCmdID(commandID) + ` + + + ./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7Bf5645004-3214-46ea-92c2-48835689da06%7D/DownloadInstall + + + + + ` + getNextCmdID(commandID) + ` + + + ./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7Bf5645004-3214-46ea-92c2-48835689da06%7D/DownloadInstall + + ` + html.EscapeString(installCommandPayload) + ` + + text/plain + xml + + + ` + + return newCmds +} + +// getNextCmdID returns the next command ID +func getNextCmdID(i *int) string { + *i++ + return strconv.Itoa(*i) +} + +// Checks if body contains a DM device unrollment SyncML message +func isDeviceUnenrollmentMessage(body fleet.SyncMLBody) bool { + for _, element := range body.Item { + if element.Data == mdm.DeviceUnenrollmentID { + return true + } + } + + return false +} + +// Checks if body contains a DM session initialization SyncML message sent by device +func isSessionInitializationMessage(body fleet.SyncMLBody) bool { + isUnenrollMessage := isDeviceUnenrollmentMessage(body) + + for _, element := range body.Item { + if element.Data == mdm.HostInitMessageID && !isUnenrollMessage { + return true + } + } + + return false +} diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 52847ad97f..cc71149337 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -1281,6 +1281,8 @@ func directIngestUsers(ctx context.Context, logger log.Logger, host *fleet.Host, func directIngestMDMMac(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error { if len(rows) == 0 { + logger.Log("component", "service", "method", "ingestMDM", "warn", + fmt.Sprintf("mdm expected single result got %d", len(rows))) // assume the extension is not there return nil } diff --git a/server/service/teams.go b/server/service/teams.go index ee9dd0da04..0356616295 100644 --- a/server/service/teams.go +++ b/server/service/teams.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -180,7 +181,7 @@ type applyTeamSpecsRequest struct { Specs []*fleet.TeamSpec `json:"specs"` } -func (req *applyTeamSpecsRequest) DecodeBody(ctx context.Context, r io.Reader) error { +func (req *applyTeamSpecsRequest) DecodeBody(ctx context.Context, r io.Reader, u url.Values) error { if err := fleet.JSONStrictDecode(r, req); err != nil { err = fleet.NewUserMessageError(err, http.StatusBadRequest) if !req.Force || !fleet.IsJSONUnknownFieldError(err) { diff --git a/server/service/teams_test.go b/server/service/teams_test.go index 73864f912b..1579f78fc4 100644 --- a/server/service/teams_test.go +++ b/server/service/teams_test.go @@ -2,7 +2,6 @@ package service import ( "context" - "database/sql" "encoding/json" "errors" "testing" @@ -262,7 +261,7 @@ func TestApplyTeamSpecs(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - return nil, sql.ErrNoRows + return nil, ¬FoundError{} } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index c41642a0ad..31d143c88d 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -604,6 +604,7 @@ func mdmAppleConfigurationRequiredEndpoints() []struct { {"GET", "/api/latest/fleet/mdm/apple/profiles/summary", false, false}, {"PATCH", "/api/latest/fleet/mdm/hosts/1/unenroll", false, false}, {"GET", "/api/latest/fleet/mdm/hosts/1/encryption_key", false, false}, + {"GET", "/api/latest/fleet/mdm/hosts/1/profiles", false, true}, {"POST", "/api/latest/fleet/mdm/hosts/1/lock", false, false}, {"POST", "/api/latest/fleet/mdm/hosts/1/wipe", false, false}, {"PATCH", "/api/latest/fleet/mdm/apple/settings", false, false}, diff --git a/server/service/user_roles.go b/server/service/user_roles.go index 2fe67dbef7..cce51264bb 100644 --- a/server/service/user_roles.go +++ b/server/service/user_roles.go @@ -47,6 +47,12 @@ func (svc *Service) ApplyUserRolesSpecs(ctx context.Context, specs fleet.UsersRo for _, team := range spec.Teams { t, err := svc.ds.TeamByName(ctx, team.Name) if err != nil { + if fleet.IsNotFound(err) { + return &fleet.BadRequestError{ + Message: err.Error(), + InternalErr: err, + } + } return err } teams = append(teams, fleet.UserTeam{ diff --git a/server/utils.go b/server/utils.go index af207709ca..0c13dab006 100644 --- a/server/utils.go +++ b/server/utils.go @@ -13,6 +13,8 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" + "strings" "time" "github.com/fleetdm/fleet/v4/pkg/fleethttp" @@ -49,18 +51,49 @@ func PostJSONWithTimeout(ctx context.Context, url string, v interface{}) error { resp, err := client.Do(req) if err != nil { - return fmt.Errorf("failed to POST to %s: %s, request-size=%d", url, err, len(jsonBytes)) + return fmt.Errorf("failed to POST to %s: %s, request-size=%d", maskSecretURLParams(url), err, len(jsonBytes)) } defer resp.Body.Close() if !httpSuccessStatus(resp.StatusCode) { body, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("error posting to %s: %d. %s", url, resp.StatusCode, string(body)) + return fmt.Errorf("error posting to %s: %d. %s", maskSecretURLParams(url), resp.StatusCode, string(body)) } return nil } +// maskSecretURLParams masks URL query values if the query param name includes "secret", "token", +// "key", "password". It accepts a raw string and returns a redacted string if the raw string is +// URL-parseable. If it is not URL-parseable, the raw string is returned unchanged. +func maskSecretURLParams(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return rawURL + } + + keywords := []string{"secret", "token", "key", "password"} + containsKeyword := func(s string) bool { + s = strings.ToLower(s) + for _, kw := range keywords { + if strings.Contains(s, kw) { + return true + } + } + return false + } + + q := u.Query() + for k := range q { + if containsKeyword(k) { + q[k] = []string{"MASKED"} + } + } + u.RawQuery = q.Encode() + + return u.Redacted() +} + // TODO: Consider moving other crypto functions from server/mdm/apple/util to here // DecodePrivateKeyPEM decodes PEM-encoded private key data. diff --git a/server/utils_test.go b/server/utils_test.go new file mode 100644 index 0000000000..849351e434 --- /dev/null +++ b/server/utils_test.go @@ -0,0 +1,75 @@ +package server + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMaskSecretURLParams(t *testing.T) { + secretKeywords := []string{"secret", "token", "key", "password"} + mask := "MASKED" + + type testCase struct { + name string + rawURL string + expected string + } + + cases := []testCase{ + { + name: "no params", + rawURL: "https://example.com", + expected: "https://example.com", + }, + { + name: "user info redacted", + rawURL: "https://user:P@$$w0rD@example.com/foo/bar?baz=qux&secret_key=baz", + expected: "https://user:xxxxx@example.com/foo/bar?baz=qux&secret_key=" + mask, + }, + } + for i, kw := range secretKeywords { + cases = append(cases, testCase{ + name: "single " + kw, + rawURL: "https://example.com?" + kw + "=foo", + expected: "https://example.com?" + kw + "=" + mask, + }) + cases = append(cases, testCase{ + name: "multiple " + kw, + rawURL: "https://example.com?" + kw + "=foo" + "&bar_" + kw + "=bar", + expected: "https://example.com?" + kw + "=" + mask + "&bar_" + kw + "=" + mask, + }) + cases = append(cases, testCase{ + name: "multiple " + kw + " with other params", + rawURL: "https://example.com?foo=bar&" + kw + "=foo" + "&bar_" + kw + "=bar", + expected: "https://example.com?foo=bar&" + kw + "=" + mask + "&bar_" + kw + "=" + mask, + }) + cases = append(cases, testCase{ + name: "multiple " + kw + " with other params and fragment", + rawURL: "https://example.com?foo=bar&" + kw + "=foo" + "&bar_" + kw + "=bar#fragment", + expected: "https://example.com?foo=bar&" + kw + "=" + mask + "&bar_" + kw + "=" + mask + "#fragment", + }) + kw2 := secretKeywords[(i+1)%len(secretKeywords)] + cases = append(cases, testCase{ + name: "combined " + kw + " and " + kw2, + rawURL: "https://example.com?foo=bar&" + kw + "=foo" + "&bar_" + kw2 + "=bar", + expected: "https://example.com?foo=bar&" + kw + "=" + mask + "&bar_" + kw2 + "=" + mask, + }) + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + masked := maskSecretURLParams(c.rawURL) + got, err := url.Parse(masked) + require.NoError(t, err) + want, err := url.Parse(c.expected) + require.NoError(t, err) + require.EqualValues(t, got.Query(), want.Query()) + require.Equal(t, got.Fragment, want.Fragment) + require.Equal(t, got.Host, want.Host) + require.Equal(t, got.Path, want.Path) + require.Equal(t, got.Scheme, want.Scheme) + }) + } +} diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index c0741fb4f5..b8db8e2905 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -44,11 +44,11 @@ func TestAppleMDM(t *testing.T) { mdmStorage, err := ds.NewMDMAppleMDMStorage([]byte("test"), []byte("test")) require.NoError(t, err) - //nopLog := kitlog.NewNopLogger() + // nopLog := kitlog.NewNopLogger() // use this to debug/verify details of calls nopLog := kitlog.NewJSONLogger(os.Stdout) - createEnrolledHost := func(t *testing.T, i int, teamID *uint) *fleet.Host { + createEnrolledHost := func(t *testing.T, i int, teamID *uint, depAssignedToFleet bool) *fleet.Host { // create the host h, err := ds.NewHost(ctx, &fleet.Host{ Hostname: fmt.Sprintf("test-host%d-name", i), @@ -71,7 +71,15 @@ func TestAppleMDM(t *testing.T) { VALUES (?, ?, ?, ?, ?, ?)`, h.UUID, h.UUID, "device", "topic", "push_magic", "token_hex") return err }) - err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "http://example.com", true, fleet.WellKnownMDMFleet) + if depAssignedToFleet { + mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, ` + INSERT INTO host_dep_assignments (host_id) VALUES (?) ON DUPLICATE KEY UPDATE host_id = host_id, deleted_at = NULL + `, h.ID) + return err + }) + } + err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "http://example.com", depAssignedToFleet, fleet.WellKnownMDMFleet) require.NoError(t, err) return h } @@ -95,7 +103,7 @@ func TestAppleMDM(t *testing.T) { w.Register(mdmWorker) // create a host and enqueue the job - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "") require.NoError(t, err) @@ -124,7 +132,7 @@ func TestAppleMDM(t *testing.T) { w.Register(mdmWorker) // create a host and enqueue the job - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, nil, "") require.NoError(t, err) @@ -146,7 +154,7 @@ func TestAppleMDM(t *testing.T) { t.Run("installs default manifest", func(t *testing.T) { defer mysql.TruncateTables(t, ds) - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) mdmWorker := &AppleMDM{ Datastore: ds, @@ -176,7 +184,7 @@ func TestAppleMDM(t *testing.T) { t.Run("installs custom bootstrap manifest", func(t *testing.T) { defer mysql.TruncateTables(t, ds) - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{ Name: "custom-bootstrap", TeamID: 0, // no-team @@ -221,7 +229,7 @@ func TestAppleMDM(t *testing.T) { tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"}) require.NoError(t, err) - h := createEnrolledHost(t, 1, &tm.ID) + h := createEnrolledHost(t, 1, &tm.ID, true) err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{ Name: "custom-team-bootstrap", TeamID: tm.ID, @@ -263,7 +271,7 @@ func TestAppleMDM(t *testing.T) { t.Run("unknown enroll reference", func(t *testing.T) { defer mysql.TruncateTables(t, ds) - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) mdmWorker := &AppleMDM{ Datastore: ds, @@ -301,7 +309,7 @@ func TestAppleMDM(t *testing.T) { Fullname: "test", }) require.NoError(t, err) - h := createEnrolledHost(t, 1, nil) + h := createEnrolledHost(t, 1, nil, true) mdmWorker := &AppleMDM{ Datastore: ds, @@ -347,7 +355,7 @@ func TestAppleMDM(t *testing.T) { _, err = ds.SaveTeam(ctx, tm) require.NoError(t, err) - h := createEnrolledHost(t, 1, &tm.ID) + h := createEnrolledHost(t, 1, &tm.ID, true) mdmWorker := &AppleMDM{ Datastore: ds, diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index c5bd5ed0c4..84ac8cb473 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -70,16 +70,25 @@ variable "fleet_config" { }), { name = "fleet" }) - database = object({ + 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 = object({ + redis = optional(object({ address = string use_tls = optional(bool, true) + }), { + address = null + use_tls = true }) awslogs = optional(object({ name = optional(string, null) @@ -93,12 +102,17 @@ variable "fleet_config" { prefix = "fleet" retention = 5 }) - loadbalancer = object({ + loadbalancer = optional(object({ arn = string + }), { + arn = null }) - networking = object({ + 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) diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index 1ec5f3627d..04e086cc77 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -160,16 +160,25 @@ variable "fleet_config" { }), { name = "fleet" }) - database = object({ + 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 = object({ + redis = optional(object({ address = string use_tls = optional(bool, true) + }), { + address = null + use_tls = true }) awslogs = optional(object({ name = optional(string, null) @@ -183,12 +192,17 @@ variable "fleet_config" { prefix = "fleet" retention = 5 }) - loadbalancer = object({ + loadbalancer = optional(object({ arn = string + }), { + arn = null }) - networking = object({ + 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) diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index b5f0642426..1af1cccd5e 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.33.1", + "version": "v4.34.0", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" diff --git a/tools/kubequery/README.md b/tools/kubequery/README.md index ee1d98e19f..724c21d81a 100644 --- a/tools/kubequery/README.md +++ b/tools/kubequery/README.md @@ -1,6 +1,6 @@ # Kubequery and Fleet -Use the provided configuration file ([kubequery-fleet.yml](kubequery-fleet.yml)) to get a [kubequery](https://github.com/Uptycs/kubequery) instance connected to Fleet. +Use the provided configuration file ([kubequery-fleet.yml](kubequery-fleet.yml)) to get a [kubequery](https://github.com/fleetdm/kubequery) instance connected to Fleet. Before deploying, first retrieve the enroll secret from Fleet by opening a web browser to the Fleet URL, going to the Hosts page, and clicking on the "Manage enroll secret" button. Alternatively, you can get the enroll secret using `fleetctl` using `fleetctl get enroll-secret`. diff --git a/tools/loadtest/osquery/macos/README.md b/tools/loadtest/osquery/macos/README.md index ba8bdbf24e..8324dcdb40 100644 --- a/tools/loadtest/osquery/macos/README.md +++ b/tools/loadtest/osquery/macos/README.md @@ -3,9 +3,9 @@ Following are the steps to load test osquery on macOS. The purpose is to know the impact of Fleet provided queries on real devices. -> At the time of writing the changes to add watchog logging needed for this script -> are under review: https://github.com/osquery/osquery/pull/8070. -> You will have to build osqueryd from source code. +> At the time of writing, the changes that add watchdog logging needed for this script are +> merged but not released yet (https://github.com/osquery/osquery/pull/8070). +> You will have to download and extract the osqueryd executable from the PR: https://github.com/osquery/osquery/suites/14033523376/artifacts/783724086 ## Requirements @@ -29,6 +29,10 @@ echo "/usr/local/osquery_extensions/fleetd_tables.ext" > /tmp/extensions.load > The following assumes a Fleet server instance running and listening at `localhost:8080`. +```sh +mkdir -p /Users/luk/osqueryd/osquery_log +``` + ```sh sudo ENROLL_SECRET=<...> ./osquery/osqueryd \ --verbose=true \ @@ -37,6 +41,7 @@ sudo ENROLL_SECRET=<...> ./osquery/osqueryd \ --database_path=/Users/luk/osqueryd/osquery.db \ --logger_path=/Users/luk/osqueryd/osquery_log \ --host_identifier=instance \ + # /Users/luk/fleetdm/git/fleet is the location of the Fleet mono repository. --tls_server_certs=/Users/luk/fleetdm/git/fleet/tools/osquery/fleet.crt \ --enroll_secret_env=ENROLL_SECRET \ --tls_hostname=localhost:8080 \ @@ -56,15 +61,25 @@ sudo ENROLL_SECRET=<...> ./osquery/osqueryd \ --carver_start_endpoint=/api/v1/osquery/carve/begin \ --carver_continue_endpoint=/api/v1/osquery/carve/block \ --carver_block_size=2000000 \ - --extensions_autoload=/tmp/extensions.load + --extensions_autoload=/tmp/extensions.load \ --allow_unsafe \ --enable_watchdog_debug \ --distributed_denylist_duration 0 \ --enable_extensions_watchdog 2>&1 | tee /tmp/osqueryd.log ``` +## Check that the watchdog didn't trigger a worker kill + +The following commands should return no output: +```sh +rg "utilization limit" /tmp/osqueryd.log +rg "Memory limit" /tmp/osqueryd.log +``` + ## Render CPU and memory usage +(Nice to have.) + ```sh ./tools/loadtest/osquery/macos/gnuplot_osqueryd_cpu_memory.sh ``` diff --git a/website/.eslintignore b/website/.eslintignore index f190c2ae4f..eda7786697 100644 --- a/website/.eslintignore +++ b/website/.eslintignore @@ -1,3 +1,3 @@ assets/dependencies/**/*.js views/**/*.ejs - +assets/storybook/* diff --git a/website/api/controllers/webhooks/receive-from-github.js b/website/api/controllers/webhooks/receive-from-github.js index 58cbbf40ba..650561f535 100644 --- a/website/api/controllers/webhooks/receive-from-github.js +++ b/website/api/controllers/webhooks/receive-from-github.js @@ -249,7 +249,7 @@ module.exports = { ); } else if ( - (ghNoun === 'pull_request' && ['opened','reopened','edited'].includes(action)) + (ghNoun === 'pull_request' && ['opened','reopened','edited', 'synchronize'].includes(action)) ) { // ██████╗ ██╗ ██╗██╗ ██╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗ // ██╔══██╗██║ ██║██║ ██║ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝ @@ -301,6 +301,92 @@ module.exports = { require('assert')(sender.login !== undefined); + let DRI_BY_PATH = {}; + if (repo === 'fleet') { + DRI_BY_PATH = sails.config.custom.githubRepoDRIByPath; + } else { + // Other repos don't have this configured. + } + + // Request review from DRI + // History: https://github.com/fleetdm/fleet/pull/12786) (only relevant for paths NOT in the CODEOWNERS file) + // (Draft PRs are skipped) + if (!issueOrPr.draft) { + + let reviewers = [];//« GitHub usernames of people to request review from. + + // Look up already-requested reviewers + // (for use in minimizing extra notifications for editing PRs to contain new changes + // while also still doing appropriate review requests) + // [?] https://developer.github.com/v3/activity/events/types + // [?] The "requested_reviewers" key in the pull request object: https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request + let alreadyRequestedReviewers = _.pluck(issueOrPr.requested_reviewers, 'login'); + + // Look up paths + // [?] https://docs.github.com/en/rest/reference/pulls#list-pull-requests-files + let changedPaths = _.pluck(await sails.helpers.http.get(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`, { + per_page: 100,//eslint-disable-line camelcase + }, baseHeaders).retry(), 'filename');// (don't worry, it's the whole path, not the filename) + + // For each changed file, decide what reviewer to request, if any… + for (let changedPath of changedPaths) { + changedPath = changedPath.replace(/\/+$/,'');// « trim trailing slashes, just in case (b/c otherwise could loop forever) + sails.log.debug(`…checking DRI of changed path "${changedPath}"`); + + let reviewer = undefined;//« whether to request review for this change + let exactMatchDri = DRI_BY_PATH[changedPath]; + if (exactMatchDri) { + let isAuthorDRI = exactMatchDri === issueOrPr.user.login.toLowerCase();//« See `user.login` in https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request + let isSenderDRI = exactMatchDri === sender.login.toLowerCase(); + if (isAuthorDRI || isSenderDRI) { + // If the original PR author OR you, the sender (current PR author/editor) are the DRI, + // then do nothing. No need to request review from yourself, and you CAN'T request + // review from the author (or the GitHub API will respond with an error.) + } else { + // Otherwise, we've found our match. We'll request review from this person. + // (And we'll stop looking.) + reviewer = exactMatchDri; + } + } else {// If there's no DRI for this *exact* file path, then check ancestral paths for the nearest DRI + + let numRemainingPathsToCheck = changedPath.split('/').length - 1; + while (numRemainingPathsToCheck > 0) { + let ancestralPath = changedPath.split('/').slice(0, numRemainingPathsToCheck).join('/'); + sails.log.debug(`…checking DRI of ancestral path "${ancestralPath}" for changed path "${changedPath}"`); + + let nearestAncestralDri = DRI_BY_PATH[ancestralPath];// this is like the "catch-all" DRI, for a higher-level path + + let isAuthorAncestralDRI = nearestAncestralDri === issueOrPr.user.login.toLowerCase();//« See `user.login` in https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request + let isSenderAncestralDRI = nearestAncestralDri === sender.login.toLowerCase(); + if (isAuthorAncestralDRI || isSenderAncestralDRI) { + // For the same reasons as above, if the original PR author or you (current author/editor) + // are the editor, then we do nothing. + } else if (nearestAncestralDri) {// Otherwise, if we have our DRI, we can stop here. + reviewer = nearestAncestralDri; + break; + } + numRemainingPathsToCheck--; + }//∞ + } + + // If review should be requested, do so, but only if review hasn't already + // been requested from this person. + if (reviewer && !alreadyRequestedReviewers.includes(reviewer)) { + reviewers.push(reviewer); + reviewers = _.uniq(reviewers);// « avoid attempting to request review from the same person twice + }//fi + + }//∞ + + if (reviewers.length >= 1) {// « avoid attempting to request review from no one + // [?] https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#request-reviewers-for-a-pull-request + await sails.helpers.http.post(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, { + reviewers: reviewers, + }, baseHeaders); + } + + }//fi + // Check whether auto-approval is warranted. let isAutoApproved = await sails.helpers.githubAutomations.getIsPrPreapproved.with({ repo: repo, @@ -308,6 +394,7 @@ module.exports = { githubUserToCheck: sender.login, isGithubUserMaintainerOrDoesntMatter: GITHUB_USERNAMES_OF_BOTS_AND_MAINTAINERS.includes(sender.login.toLowerCase()) }); + let isHandbookPR = false; if(repo === 'fleet'){ isHandbookPR = await sails.helpers.githubAutomations.getIsPrOnlyHandbookChanges.with({prNumber: prNumber}); diff --git a/website/api/helpers/github-automations/get-is-pr-preapproved.js b/website/api/helpers/github-automations/get-is-pr-preapproved.js index c7bfb8b0c3..8cb8cd281a 100644 --- a/website/api/helpers/github-automations/get-is-pr-preapproved.js +++ b/website/api/helpers/github-automations/get-is-pr-preapproved.js @@ -19,7 +19,7 @@ module.exports = { success: { outputFriendlyName: 'Is PR preapproved?', - outputDescription: 'Whether the provided GitHub user is the DRI for all changed paths.', + outputDescription: 'Whether the provided GitHub user is a maintainer for all changed paths.', outputType: 'boolean', }, @@ -28,19 +28,19 @@ module.exports = { fn: async function ({repo, prNumber, githubUserToCheck, isGithubUserMaintainerOrDoesntMatter}) { - require('assert')(sails.config.custom.githubRepoDRIByPath); - require('assert')(sails.config.custom.confidentialGithubRepoDRIByPath); - require('assert')(sails.config.custom.fleetMdmGitopsGithubRepoDRIByPath); + require('assert')(sails.config.custom.githubRepoMaintainersByPath); + require('assert')(sails.config.custom.confidentialGithubRepoMaintainersByPath); + require('assert')(sails.config.custom.fleetMdmGitopsGithubRepoMaintainersByPath); require('assert')(sails.config.custom.githubAccessToken); - let DRI_BY_PATH = sails.config.custom.githubRepoDRIByPath; + let MAINTAINERS_BY_PATH = sails.config.custom.githubRepoMaintainersByPath; if (repo === 'confidential') { - DRI_BY_PATH = sails.config.custom.confidentialGithubRepoDRIByPath; + MAINTAINERS_BY_PATH = sails.config.custom.confidentialGithubRepoMaintainersByPath; } if (repo === 'fleet-mdm-gitops') { - DRI_BY_PATH = sails.config.custom.fleetMdmGitopsGithubRepoDRIByPath; + MAINTAINERS_BY_PATH = sails.config.custom.fleetMdmGitopsGithubRepoMaintainersByPath; } let owner = 'fleetdm'; @@ -49,22 +49,22 @@ module.exports = { 'Authorization': `token ${sails.config.custom.githubAccessToken}` }; - // Check the PR's author versus the intersection of DRIs for all changed files. + // Check the PR's author versus the intersection of maintainers for all changed files. return await sails.helpers.flow.build(async()=>{ - let isDRIForAllChangedPathsStill = false; + let isMaintainerForAllChangedPathsStill = false; // [?] https://docs.github.com/en/rest/reference/pulls#list-pull-requests-files let changedPaths = _.pluck(await sails.helpers.http.get(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`, { per_page: 100,//eslint-disable-line camelcase }, baseHeaders).retry(), 'filename');// (don't worry, it's the whole path, not the filename) - isDRIForAllChangedPathsStill = _.all(changedPaths, (changedPath)=>{ + isMaintainerForAllChangedPathsStill = _.all(changedPaths, (changedPath)=>{ changedPath = changedPath.replace(/\/+$/,'');// « trim trailing slashes, just in case (b/c otherwise could loop forever) - // sails.log.verbose(`…checking DRI of changed path "${changedPath}"`); + // sails.log.verbose(`…checking maintainership of changed path "${changedPath}"`); - let selfMergers = DRI_BY_PATH[changedPath] ? [].concat(DRI_BY_PATH[changedPath]) : [];// « ensure array + let selfMergers = MAINTAINERS_BY_PATH[changedPath] ? [].concat(MAINTAINERS_BY_PATH[changedPath]) : [];// « ensure array if (!githubUserToCheck && selfMergers.length >= 1) {// « not checking a user, so just make sure all these paths are preapproved for SOMEONE return true; } @@ -74,8 +74,8 @@ module.exports = { let numRemainingPathsToCheck = changedPath.split('/').length; while (numRemainingPathsToCheck > 0) { let ancestralPath = changedPath.split('/').slice(0, -1 * numRemainingPathsToCheck).join('/'); - // sails.log.verbose(`…checking DRI of ancestral path "${ancestralPath}" for changed path`); - let selfMergers = DRI_BY_PATH[ancestralPath] ? [].concat(DRI_BY_PATH[ancestralPath]) : [];// « ensure array + // sails.log.verbose(`…checking maintainers of ancestral path "${ancestralPath}" for changed path`); + let selfMergers = MAINTAINERS_BY_PATH[ancestralPath] ? [].concat(MAINTAINERS_BY_PATH[ancestralPath]) : [];// « ensure array if (!githubUserToCheck && selfMergers.length >= 1) {// « not checking a user, so just make sure all these paths are preapproved for SOMEONE return true; } @@ -86,7 +86,7 @@ module.exports = { }//∞ });//∞ - if (isDRIForAllChangedPathsStill && changedPaths.length < 100) { + if (isMaintainerForAllChangedPathsStill && changedPaths.length < 100) { return true; } else { return false; diff --git a/website/assets/dependencies/docsearch.min.js b/website/assets/dependencies/docsearch.min.js index efb45b8f6f..336fc3e303 100644 --- a/website/assets/dependencies/docsearch.min.js +++ b/website/assets/dependencies/docsearch.min.js @@ -1,2 +1,4 @@ -/*! docsearch 2.6.3 | © Algolia | github.com/algolia/docsearch */ -(function webpackUniversalModuleDefinition(root,factory){if(typeof exports==="object"&&typeof module==="object")module.exports=factory();else if(typeof define==="function"&&define.amd)define([],factory);else if(typeof exports==="object")exports["docsearch"]=factory();else root["docsearch"]=factory()})(typeof self!=="undefined"?self:this,function(){return function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{configurable:false,enumerable:true,get:getter})}};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module["default"]}:function getModuleExports(){return module};__webpack_require__.d(getter,"a",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p="";return __webpack_require__(__webpack_require__.s=22)}([function(module,exports,__webpack_require__){"use strict";var DOM=__webpack_require__(1);function escapeRegExp(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}module.exports={isArray:null,isFunction:null,isObject:null,bind:null,each:null,map:null,mixin:null,isMsie:function(agentString){if(agentString===undefined){agentString=navigator.userAgent}if(/(msie|trident)/i.test(agentString)){var match=agentString.match(/(msie |rv:)(\d+(.\d+)?)/i);if(match){return match[2]}}return false},escapeRegExChars:function(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isNumber:function(obj){return typeof obj==="number"},toStr:function toStr(s){return s===undefined||s===null?"":s+""},cloneDeep:function cloneDeep(obj){var clone=this.mixin({},obj);var self=this;this.each(clone,function(value,key){if(value){if(self.isArray(value)){clone[key]=[].concat(value)}else if(self.isObject(value)){clone[key]=self.cloneDeep(value)}}});return clone},error:function(msg){throw new Error(msg)},every:function(obj,test){var result=true;if(!obj){return result}this.each(obj,function(val,key){if(result){result=test.call(null,val,key,obj)&&result}});return!!result},any:function(obj,test){var found=false;if(!obj){return found}this.each(obj,function(val,key){if(test.call(null,val,key,obj)){found=true;return false}});return found},getUniqueId:function(){var counter=0;return function(){return counter++}}(),templatify:function templatify(obj){if(this.isFunction(obj)){return obj}var $template=DOM.element(obj);if($template.prop("tagName")==="SCRIPT"){return function template(){return $template.text()}}return function template(){return String(obj)}},defer:function(fn){setTimeout(fn,0)},noop:function(){},formatPrefix:function(prefix,noPrefix){return noPrefix?"":prefix+"-"},className:function(prefix,clazz,skipDot){return(skipDot?"":".")+prefix+clazz},escapeHighlightedString:function(str,highlightPreTag,highlightPostTag){highlightPreTag=highlightPreTag||"";var pre=document.createElement("div");pre.appendChild(document.createTextNode(highlightPreTag));highlightPostTag=highlightPostTag||"";var post=document.createElement("div");post.appendChild(document.createTextNode(highlightPostTag));var div=document.createElement("div");div.appendChild(document.createTextNode(str));return div.innerHTML.replace(RegExp(escapeRegExp(pre.innerHTML),"g"),highlightPreTag).replace(RegExp(escapeRegExp(post.innerHTML),"g"),highlightPostTag)}}},function(module,exports,__webpack_require__){"use strict";module.exports={element:null}},function(module,exports){var hasOwn=Object.prototype.hasOwnProperty;var toString=Object.prototype.toString;module.exports=function forEach(obj,fn,ctx){if(toString.call(fn)!=="[object Function]"){throw new TypeError("iterator must be a function")}var l=obj.length;if(l===+l){for(var i=0;i was loaded but did not call our provided callback"),JSONPScriptError:createCustomError("JSONPScriptError","