diff --git a/.github/ISSUE_TEMPLATE/release-qa.md b/.github/ISSUE_TEMPLATE/release-qa.md index 66d6bd14e3..84f8154d50 100644 --- a/.github/ISSUE_TEMPLATE/release-qa.md +++ b/.github/ISSUE_TEMPLATE/release-qa.md @@ -99,6 +99,14 @@ Smoke tests are limited to core functionality and serve as a pre-release final r 3. Verify able to run MDM commands on both macOS and Windows hosts from the CLI. pass/fail +MDM migration flowVerify MDM migration for ADE and non-ADE hosts + +1. Turn off MDM on an ADE-eligible macOS host and verify that the native, "Device Enrollment" macOS notification appears. +2. On the My device page, follow the "Turn on MDM" instructions and verify that MDM is turned on. +3. Turn off MDM on a non ADE-eligible macOS host. +4. On the My device page, follow the "Turn on MDM" instructions and verify that MDM is turned on. +pass/fail + ScriptsVerify script library and execution 1. Verify able to run a script on all host types from CLI. diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md index 2e592a9f21..54fffcd59b 100644 --- a/.github/ISSUE_TEMPLATE/story.md +++ b/.github/ISSUE_TEMPLATE/story.md @@ -35,6 +35,7 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan - [ ] UI changes: TODO - [ ] CLI usage changes: TODO - [ ] REST API changes: TODO +- [ ] Fleet's agent (fleetd) changes: TODO - [ ] Permissions changes: TODO - [ ] Outdated documentation changes: TODO - [ ] Changes to paid features or tiers: TODO diff --git a/.github/workflows/dogfood-deploy.yml b/.github/workflows/dogfood-deploy.yml index d13d2f4761..39f6983824 100644 --- a/.github/workflows/dogfood-deploy.yml +++ b/.github/workflows/dogfood-deploy.yml @@ -31,6 +31,7 @@ env: TF_VAR_elastic_url: ${{ secrets.ELASTIC_APM_SERVER_URL }} TF_VAR_elastic_token: ${{ secrets.ELASTIC_APM_SECRET_TOKEN }} TF_VAR_geolite2_license: ${{ secrets.MAXMIND_LICENSE }} + TF_VAR_dogfood_sidecar_enroll_secret: ${{ secrets.DOGFOOD_SERVERS_CANARY_ENROLL_SECRET }} permissions: id-token: write diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index 990d43c316..9dc86ba0e4 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -24,7 +24,7 @@ defaults: shell: bash env: - FLEET_DESKTOP_VERSION: 1.25.0 + FLEET_DESKTOP_VERSION: 1.26.0 permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cdef747e..3b5cda00d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Fleet 4.51.1 (Jun 12, 2024) + +### Bug fixes + +* Added S3 config variables with a `carves` and `software_installers` prefix, which were used to configure buckets for those features. The existing non-prefixed variables were kept for backwards compatibility. +* Fixed a bug that prevented unused script contents to be periodically cleaned up from the database. + ## Fleet 4.51.0 (Jun 10, 2024) ### Endpoint Operations diff --git a/articles/deploy-fleet-on-ubuntu-with-elastic.md b/articles/deploy-fleet-on-ubuntu-with-elastic.md new file mode 100644 index 0000000000..cf7c9cea60 --- /dev/null +++ b/articles/deploy-fleet-on-ubuntu-with-elastic.md @@ -0,0 +1,346 @@ +# Deploy Fleet on Ubuntu with Elastic + +![Deploy Fleet on Ubuntu with Elastic](../website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-1600x900@2x.png) + +[](https://internews.org/)_Today we wanted to feature [Josh](https://defensivedepth.com/), a member of our community. His work was sponsored by [Internews](https://internews.org/). If you are interested in contributing to the Fleet blog, feel free to [contact us](https://fleetdm.com/company/contact) or reach out to [@jdstrong](https://osquery.slack.com/team/U04MTPBAHQS) on the osquery slack._ + +This guide provides a detailed walkthrough for setting up a small production environment of Fleet alongside Elastic components (Elasticsearch, Kibana, Filebeat). The setup integrates Filebeat to collect scheduled query results from Fleet and feed them into Elasticsearch, while Kibana will be utilized for data visualization and the creation of detections. Additionally, Nginx will serve as a reverse proxy for the Kibana and Fleet web interfaces and will segregate the web administration and agent data+control planes of Fleet for more fine-grained access control. + +The installation and configuration will begin with the Elastic stack components, followed by Fleet and its dependencies. For this guide, they will all be installed on a single server; however, for larger deployments or requirements of higher availability and scalability, a more distributed approach across multiple servers and geographical regions is recommended. + +### Network, server & DNS setup + +This guide is based on Ubuntu 22.04 LTS, although the installation procedures for the components remain consistent across newer versions of the operating system. + +For this guide, subdomain `fleet.localhost.invalid` is pointed to the server's public IP. Replace this subdomain with a valid one configured as such. + +Ports needed, inbound to server: +- `TCP/80` (Only used for the initial Let's Encrypt setup) +- `TCP/443` (Used initially for the Let's Encrypt setup, and then longterm for Fleet distributed agents to checking for data and control) +- `TCP/8443` (Used for Kibana web interface) +- `TCP/9443` (Used for Fleet web interface) + +Set up access control where it makes sense - perimeter firewall or on the server itself. Set the ports for the Kibana (`TCP/8443`) and Fleet (`TCP/9443`) web interfaces to only be accessible from a known-trusted IP space. Also set rules for `TCP/443`, which is used for the deployed osquery agents to check in with Fleet. A common configuration is for the web interface ports to be accessible to a single IP or small set of IPs, and for the osquery check in port to be accessible anywhere. + +Be aware that if you are using a proxy like Cloudflare, you will need to confirm that the ports in this guide will work as expected. + +### Update OS + +Let's start by updating the system's packages and creating a workspace directory: + +```sh +sudo apt-get update && sudo apt-get dist-upgrade -y +mkdir workspace && cd workspace +``` + +### Install & configure Certbot + +Next up is to install Certbot to create and manage our free Let's Encrypt SSL certificate. This certificate will be used by for all components. + +```sh +sudo apt-get install certbot -y +sudo certbot certonly --standalone +``` + +Select option 1 to spin up a temporary web server. Enter the domain that you have pointed to your public IP. You will need TCP/80 & TCP/443 open to the server. + +By default, the certificate and key are saved at: + +- Certificate: `/etc/letsencrypt/live/fleet.localhost.invalid/fullchain.pem` +- Key: `/etc/letsencrypt/live/fleet.localhost.invalid/privkey.pem` + +### Install & configure Nginx + +Let's install Nginx and configure it as a reverse proxy for Fleet and Kibana. + +```sh +sudo apt-get install nginx +nano /etc/nginx/sites-available/fleet # use the below config, remember to update the path to the certificate files +sudo ln -s /etc/nginx/sites-available/fleet /etc/nginx/sites-enabled/ # symlink the config file to enable it +nginx -t # Test the config to make sure there are no syntax errors +sudo systemctl reload nginx # Reload nginx to make the config active +sudo systemctl status nginx # Check the reload to confirm that there are no errors +``` +Nginx Config file: +```sh +# Define SSL configuration +ssl_certificate /etc/letsencrypt/live/fleet.localhost.invalid/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/fleet.localhost.invalid/privkey.pem; + +# Common proxy settings +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + +# Server block for Kibana on port 8443 +server { + listen 8443 ssl default_server; + + location / { + proxy_pass http://localhost:5601; + } +} + +# Server block for Fleet on port 9443 with WebSocket support +server { + listen 9443 ssl; + add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data: blob: wss:; frame-ancestors 'self'"; + + location / { + proxy_pass https://localhost:4443/; + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } +} + +# Server block for specific Orbit osquery paths on port 443 +server { + listen 443 ssl; + + location ~* ^/api/(osquery|fleet/orbit/(config|ping)|v1/osquery) { + proxy_pass https://localhost:4443; + } +} +``` + + +### Install & configure Elasticsearch + + +In case the below does not work, consult Debian package installation instructions at https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html + +Let's download and install Elasticsearch via an Ubuntu package. + +One-time prep needed to add the Elastic APT repository: +```sh +wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - +echo "deb https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-8.x.list +sudo apt-get update +``` + +Install the Elasticsearch package (this will install the latest stable version): + +```sh +sudo apt-get install elasticsearch +``` +The post-install message will contain a password generated for the Elasticsearch built-in superuser (`elastic`). Make note of it as we will need it later. + +Enable and start the Elasticsearch service: + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now elasticsearch.service +``` + +## Install & configure Kibana + +Onto Kibana. Let's download, install and do the initial configuration. + +```sh +sudo apt-get install kibana +``` +Before we start Kibana, we need to edit the configuration file: + +```sh +nano /etc/kibana/kibana.yml +``` + +Set the server host and public base URL by uncommenting and editing the below lines: + +```yaml +server.host: "0.0.0.0" # Sets Kibana to listen on all interfaces +server.publicBaseUrl: "https://fleetmd.localhost.invalid:8443" # This should be set to your custom subdomain/port +``` + +Enable and start the Kibana service: + +```sh +sudo /bin/systemctl daemon-reload +sudo /bin/systemctl enable --now kibana.service +``` + +### Initial configuration + +Access Kibana at `https://fleet.localhost.invalid:8443`. If you get stuck at this step, you may not have opened ports 8443 and 9443, as needed in this walkthrough. Generate and enter the initial setup token and the verification code: + +```sh +/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana +/usr/share/kibana/bin/kibana-verification-code +``` + +From there, log in with the username `elastic` and the password that was generated previously, and choose `Explore on my own`. Navigate to `Management` -> `Stack Monitoring` and set up self-monitoring with `set up with self monitoring` and `Turn on monitoring`. This will give you a nice overview of Elasticsearch, Kibana and eventually Filebeat. + +## Install & configure Filebeat + +The final Elastic component to install is Filebeat. Let's download and configure it to pick up our osquery logs. + +```sh +sudo apt-get install filebeat +``` + +Edit the Filebeat configuration to set up where to send its logs (Elasticsearch). We disable ssl.verification because the connection from Filebeat to Elasticsearch is local (from Filebeat on the server to Elasticsearch on the same system). +Filebeat has built-in support for osquery logs. Let's configure and then enable that filebeat module and then start the Filebeat service: + + +```sh +sudo nano /etc/filebeat/modules.d/osquery.yml.disabled # Use the following config +``` + +```yaml +# Module: osquery + +- module: osquery + result: + enabled: true + + # Set custom paths for the log files. If left empty, + # Filebeat will choose the paths depending on your OS. + var.paths: ["/tmp/osquery_result"] + + # If true, all fields created by this module are prefixed with + # `osquery.result`. Set to false to copy the fields in the root + # of the document. The default is true. + #var.use_namespace: true +``` + + +```sh +sudo filebeat modules enable osquery # Enable the Filebeat osquery module +sudo /bin/systemctl daemon-reload +sudo /bin/systemctl enable --now filebeat.service +``` + +## Install & configure MySQL + +With the Elastic components installed, we can move on to Fleet. First up is installing MySQL and creating the Fleet user and database. + +```sh +sudo apt-get install mysql-server -y +mysql -uroot +create database fleet; # This is the database that will be used by Fleet +create user fleet@'localhost' identified by 'FleetDMPW!'; # Create the mysql user for the Fleet database and set a strong password. +grant all privileges on fleet.* to fleet@'localhost'; # Grant the new user the necessary privileges to the Fleet database. +exit +``` + +## Install & configure Redis + +Redis is used for the Live Query functionality. Let's get it installed. + +```sh +sudo apt-get install redis-server -y +``` + +## Install & configure Fleet + +Finally, the linchpin - Fleet. Let's download the latest version. You can find the latest version here: https://github.com/fleetdm/fleet/releases/latest - make sure you download the main Fleet package and not `fleetctl` at this time. + +```sh +wget https://github.com/fleetdm/fleet/releases/download/fleet-$VERSION/fleet_$VERSION_linux.tar.gz +tar -xf fleet_v*_linux.tar.gz # Extract the Fleet binary +sudo cp fleet_v*_linux/fleet /usr/bin/ # Copy the the Fleet binary to /usr/bin +fleet version # Sanity check to make sure it runs as expected +``` + +Next we will create the directory that will contain the config and installers, and create the config itself. + +```sh +mkdir /etc/fleet +nano /etc/fleet/fleet.config +``` + +Use the following as a baseline for your Fleet config: + +```yaml +mysql: + address: 127.0.0.1:3306 + database: fleet + username: fleet + password: FleetPW! +redis: + address: 127.0.0.1:6379 +server: + address: 0.0.0.0:4443 + cert: /etc/letsencrypt/live/fleet.localhost.invalid/fullchain.pem + key: /etc/letsencrypt/live/fleet.localhost.invalid/privkey.pem + websockets_allow_unsafe_origin: true # This is needed for Live Query functionality to work with the nginx reverse proxy we are using +``` + +Next, let's run the `prepare db` command to complete the necessary database prep. + +```sh +fleet prepare db --config /etc/fleet/fleet.config +``` + +### Setup systemd unit file + +Now that we are ready to run Fleet, let's create a `systemd` unit file to manage Fleet as a service, and then go ahead and start the service: + +```sh +sudo nano /etc/systemd/system/fleet.service # Use the example unit file below +sudo systemctl enable --now fleet.service +sudo systemctl status fleet.service +``` + +```sh +[Unit] +Description=fleet +After=network.target + +[Service] +ExecStart=/usr/bin/fleet serve -c /etc/fleet/fleet.config + +[Install] +WantedBy=multi-user.target +``` + + +Finally, complete the Fleet setup via the web interface at https://fleet.localhost.invalid:9443 + +## fleetctl + +fleetctl is a utility from Fleet that is used to manage Fleet from the command line. Let's download it and get it logged into our instance of Fleet. You can find the latest version here: https://github.com/fleetdm/fleet/releases/latest + +```sh +wget https://github.com/fleetdm/fleet/releases/download/fleet-$VERSION/fleetctl_$VERSION_linux.tar.gz +tar -xf fleetctl_*_linux.tar.gz# Extract the fleetct binary +sudo cp fleetctl_v*_linux/fleetctl /usr/bin/ # Copy the the fleetctl binary to /usr/bin +/usr/bin/fleetctl --version # Sanity check to make sure it runs as expected +``` + +Next, we need to configure it to work with our local instance of Fleet and login to it. + +```sh +fleetctl config set --address https://fleet.localhost.invalid::4443 +fleetctl login +``` + +## Generate agents + +Fleet ships with support for Orbit, a wrapper around osquery. Orbit makes configuration of osquery much simpler, offers auto-update functionality of osquery as well as additional tables developed by Fleet. In order to install an Orbit/osquery agent, you will need to generate an installer. + +You can start the process of generating Orbit agent packages from the Fleet interface - click on the `Add Hosts` button. You can generate the packages anywhere that you have `fleetctl`, including on the server itself. Be sure to install the Docker engine if you need to generate installers for Windows. + +## Load Fleet standard query library + +Fleet has a library of queries that are useful in many different situations - https://fleetdm.com/docs/using-fleet/standard-query-library + +Let's go ahead and load them - once this is complete, you can find them in the web interface under Queries. + +```sh +git clone https://github.com/fleetdm/fleet.git +cd fleet +fleetctl apply -f docs/01-Using-Fleet/standard-query-library/standard-query-library.yml +``` + + + + + + + + + diff --git a/articles/sysadmin-diaries-restoring-fleetd.md b/articles/sysadmin-diaries-restoring-fleetd.md new file mode 100644 index 0000000000..b8f28efe43 --- /dev/null +++ b/articles/sysadmin-diaries-restoring-fleetd.md @@ -0,0 +1,132 @@ +# Sysadmin diaries: restoring `fleetd` + +![Sysadmin diaries: restoring fleetd](../website/assets/images/articles/sysadmin-diaries-1600x900@2x.png) + +As a sysadmin, unexpected challenges are part of the job. In our last diary installment, we discussed the methods of [device enrollment](https://fleetdm.com/guides/sysadmin-diaries-device-enrollment). Today, we tackle a new challenge: a surly employee has deleted the `fleetd` files from their device. What happens next? Can we restore the `fleetd` agent using Mobile Device Management (MDM) commands? In this post, we’ll explore various methods to tackle this situation and ensure your fleet of devices remains secure and compliant. + + +### What is `fleetd` and why it matters + +`Fleetd` is a suite of agents Fleet provides to collect and manage information about your devices. It includes osquery, Orbit, Fleet Desktop, and the `fleetd` Chrome extension. These tools help you maintain visibility and control over your device fleet. + + +### Scenario: the surly employee deletion + +Imagine a disgruntled employee deleting the `fleetd` files from their device. This disruptive act can hinder your ability to manage the device and potentially compromise security. Fortunately, you can reinstall the `fleetd` agent and restore order with the right MDM commands. It's important to note that ADE (Automated Device Enrollment) enrollment ensures we can maintain control of the laptop and still send MDM commands to the host, such as remote lock or wipe. + + +### Solutions and commands + +There are several approaches to reinstall the `fleetd` agent using MDM commands: + + +#### 1. Resending the `fleetd` configuration profile + +One potential solution is to resend the `fleetd` configuration profile. The new feature for [resending profiles](https://fleetdm.com/docs/rest-api/rest-api#resend-hosts-configuration-profile) makes this easy to accomplish through the MDM interface. + + +#### 2. Wipe the device + +A more extreme method is wiping the device, which performs an Erase All Contents and Settings (EACS). This wipes and resets the laptop by erasing the user-data volume, returning the device to an "out-of-box" experience. This process avoids reinstalling macOS, making it a quick and efficient solution but probably an aggressive action. + + +#### 3. Sending the install command + +By default, the install profile is not sent after the first enrollment. However, you can manually send a command to reinstall `fleetd`. Here is the XML command for macOS: + +```xml + + + + + Command + + ManifestURL + https://download.fleetdm.com/fleetd-base-manifest.plist + RequestType + InstallEnterpriseApplication + + CommandUUID + adc1bc23-abec-4499-b57f-c8755c7ffe3c + + +``` + +To run this command, use the following `fleetctl` command: + +```sh +fleetctl mdm run-command --hosts=HOST_IDENTIFIER --payload=path/to/file.xml +``` + +For Windows, the process involves two steps. First, [add the profile](https://fleetdm.com/docs/using-fleet/mdm-custom-os-settings) using gitops or the UI: + +```xml + + addCommandUUID + + + ./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7BA427C0AA-E2D5-40DF-ACE8-0D726A6BE096%7D/DownloadInstall + + + +``` + +Then, execute the command using `fleetctl`: + +```xml + + execCommandUUID + + + ./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7BA427C0AA-E2D5-40DF-ACE8-0D726A6BE096%7D/DownloadInstall + + + + + + + https://download.fleetdm.com/fleetd-base.msi + + + + 9F89C57D1B34800480B38BD96186106EB6418A82B137A0D56694BF6FFA4DDF1A + + + /quiet FLEET_URL="REPLACE_WITH_FLEET_URL_HERE" FLEET_SECRET="REPLACE_WITH_FLEET_SECRET_HERE" + 10 + 1 + 5 + + + + + + text/plain + xml + + + + +``` + + +### Success story and experiment results + +Recently, we conducted an experiment to test these methods. After executing the commands, we observed the device coming back online, confirming the effectiveness of these solutions. This successful experiment highlights the practicality of using MDM commands to restore the `fleetd` agent. + + +### Conclusion + +Dealing with the deletion of `fleetd` files by a surly employee can be a challenge. However, using MDM commands to resend configuration profiles, utilize the EACS, or manually send the install command can efficiently restore functionality and ensure device security. Documenting these processes further strengthens your device management capabilities and prepares you for any future disruptions. + + + + + + + + + + + + diff --git a/changes/148940-app-os-vuln-matching b/changes/148940-app-os-vuln-matching new file mode 100644 index 0000000000..4145a8655f --- /dev/null +++ b/changes/148940-app-os-vuln-matching @@ -0,0 +1 @@ +- Fleet now matches vulnerabilies for applications that include an OS scope [example](https://nvd.nist.gov/vuln/detail/CVE-2023-0400) \ No newline at end of file diff --git a/changes/16961-return-api-token-for-api-only-users b/changes/16961-return-api-token-for-api-only-users new file mode 100644 index 0000000000..97c5ce6a01 --- /dev/null +++ b/changes/16961-return-api-token-for-api-only-users @@ -0,0 +1,2 @@ +- Endpoint `/api/latest/fleet/users/admin` to return API token when creating API-only (non-SSO) users. +- Added API-token of the created API-only (non-SSO) user to the output of `fleetctl user create --api-only`. diff --git a/changes/17316-parse-config-profile-error b/changes/17316-parse-config-profile-error new file mode 100644 index 0000000000..246bbff280 --- /dev/null +++ b/changes/17316-parse-config-profile-error @@ -0,0 +1 @@ +- Fixed issue where Windows-specific error message was displayed when failing to parse macOS configuration profiles. \ No newline at end of file diff --git a/changes/17387-soft-delete-host-script-and-software-install-results b/changes/17387-soft-delete-host-script-and-software-install-results new file mode 100644 index 0000000000..b27533a095 --- /dev/null +++ b/changes/17387-soft-delete-host-script-and-software-install-results @@ -0,0 +1 @@ +* Use a "soft-delete" approach when deleting a host so that its script execution details are still available for the activities feed. diff --git a/changes/17728-send-408-instead-of-500-for-apple-mdm-timeout b/changes/17728-send-408-instead-of-500-for-apple-mdm-timeout new file mode 100644 index 0000000000..369f33441b --- /dev/null +++ b/changes/17728-send-408-instead-of-500-for-apple-mdm-timeout @@ -0,0 +1 @@ +* Fixed the `/mdm/apple/mdm` endpoint so that it returns status code 408 (request timeout) instead of 500 (internal server error) when encountering a timeout reading the request body. diff --git a/changes/18427-cert-names b/changes/18427-cert-names new file mode 100644 index 0000000000..f5f9bea1d8 --- /dev/null +++ b/changes/18427-cert-names @@ -0,0 +1 @@ +* Use Fleet instead of FleetDM in MDM certificates diff --git a/changes/18733-vscode-false-pos b/changes/18733-vscode-false-pos new file mode 100644 index 0000000000..a4189b4c62 --- /dev/null +++ b/changes/18733-vscode-false-pos @@ -0,0 +1 @@ +removed vscode false positive vulnerabilities \ No newline at end of file diff --git a/changes/19000-zoominfo-icon b/changes/19000-zoominfo-icon new file mode 100644 index 0000000000..08bd20c745 --- /dev/null +++ b/changes/19000-zoominfo-icon @@ -0,0 +1 @@ +- Fixed UI bug where Zoom icon was displayed for ZoomInfo. diff --git a/changes/19090-flashing-count b/changes/19090-flashing-count new file mode 100644 index 0000000000..55c4abe22e --- /dev/null +++ b/changes/19090-flashing-count @@ -0,0 +1 @@ +- Cleanup count rendering fixing clientside flashing counts diff --git a/changes/19103-my-device-os-settings b/changes/19103-my-device-os-settings new file mode 100644 index 0000000000..123426477f --- /dev/null +++ b/changes/19103-my-device-os-settings @@ -0,0 +1,2 @@ +- Fixed UI bug where error detail was overflowing the table in "OS settings" modal in "My device" + page UI. diff --git a/changes/19181-software-empty-states b/changes/19181-software-empty-states new file mode 100644 index 0000000000..17019414fd --- /dev/null +++ b/changes/19181-software-empty-states @@ -0,0 +1 @@ +- Clean up software empty states in the UI diff --git a/changes/19197-fix-windows-remove-fleetd-script b/changes/19197-fix-windows-remove-fleetd-script new file mode 100644 index 0000000000..b1af6701c3 --- /dev/null +++ b/changes/19197-fix-windows-remove-fleetd-script @@ -0,0 +1 @@ +* Fixed an issue with the Windows-specific `windows-remove-fleetd.ps1` script provided in the Fleet repository where running the script did remove `fleetd` but made it impossible to reinstall the agent. diff --git a/orbit/changes/19284-modal-background-scrollbars b/changes/19284-modal-background-scrollbars similarity index 100% rename from orbit/changes/19284-modal-background-scrollbars rename to changes/19284-modal-background-scrollbars diff --git a/changes/19290-fix-make-slice-with-capacity b/changes/19290-fix-make-slice-with-capacity new file mode 100644 index 0000000000..27770b1bfe --- /dev/null +++ b/changes/19290-fix-make-slice-with-capacity @@ -0,0 +1 @@ +* Fixed a code linter issue where a slice was created non-empty and appended-to, instead of empty with the required capacity. diff --git a/changes/19324-fix-panic-in-download-software b/changes/19324-fix-panic-in-download-software new file mode 100644 index 0000000000..e5cbf57365 --- /dev/null +++ b/changes/19324-fix-panic-in-download-software @@ -0,0 +1 @@ +* Fixed a panic (API returning code 500) when the software installer exists in the database but the installer does not exist in the storage. diff --git a/changes/19332-clear-secrets-with-gitops b/changes/19332-clear-secrets-with-gitops new file mode 100644 index 0000000000..f152fa84c5 --- /dev/null +++ b/changes/19332-clear-secrets-with-gitops @@ -0,0 +1,2 @@ +* Enabled `fleetctl gitops` to create teams with no enroll secrets, or clear enroll secrets for an existing team. This is done by setting `team_settings.secrets` to nothing or to null or to an empty array ( `[]` ) in YAML. +* Enabled `fleetctl apply` to create teams with no enroll secrets, or clear enroll secrets for an existing team. This is done by setting `team.secrets` to an empty array in YAML. diff --git a/changes/19348-software-host-details-page b/changes/19348-software-host-details-page new file mode 100644 index 0000000000..01f65248c4 --- /dev/null +++ b/changes/19348-software-host-details-page @@ -0,0 +1,3 @@ +Fixed host details page and device details page not showing the latest software. + +Added `exclude_software` query parameter to the `/api/latest/fleet/hosts/:id` endpoint to exclude software from the response. diff --git a/changes/19453-improve-software-installer-upload-endpoint b/changes/19453-improve-software-installer-upload-endpoint new file mode 100644 index 0000000000..c4402ca71f --- /dev/null +++ b/changes/19453-improve-software-installer-upload-endpoint @@ -0,0 +1 @@ +* Extended the timeout for the endpoint to upload a software installer (`POST /fleet/software/package`), and improved handling of the maximum size. diff --git a/changes/19500-scripts-cleanup b/changes/19500-scripts-cleanup new file mode 100644 index 0000000000..f0a365adf7 --- /dev/null +++ b/changes/19500-scripts-cleanup @@ -0,0 +1 @@ +* Fixed a bug that prevented unused script contents to be periodically cleaned up from the database. diff --git a/changes/19512-mdm-migration-sonoma b/changes/19512-mdm-migration-sonoma new file mode 100644 index 0000000000..d82dff2208 --- /dev/null +++ b/changes/19512-mdm-migration-sonoma @@ -0,0 +1 @@ +- Fixed bug where MDM migration failed when attempting to renew enrollment profiles on macOS Sonoma devices. diff --git a/changes/19545-unlock-pin b/changes/19545-unlock-pin new file mode 100644 index 0000000000..ee0f715202 --- /dev/null +++ b/changes/19545-unlock-pin @@ -0,0 +1,2 @@ +* /api/latest/fleet/hosts/:id/lock returns `unlock_pin` for Apple hosts +* UI no longer uses unlock pending state for Apple hosts diff --git a/changes/19580-fix-linux-unlock-script-for-user-without-password b/changes/19580-fix-linux-unlock-script-for-user-without-password new file mode 100644 index 0000000000..5769de0793 --- /dev/null +++ b/changes/19580-fix-linux-unlock-script-for-user-without-password @@ -0,0 +1 @@ +* Fixed the Linux unlock script to support passwordless users. diff --git a/changes/19600-add-config-to-set-query-report-cap b/changes/19600-add-config-to-set-query-report-cap new file mode 100644 index 0000000000..a016c325ff --- /dev/null +++ b/changes/19600-add-config-to-set-query-report-cap @@ -0,0 +1 @@ +* Added a server setting to configure the query repory cap size, `server_settings.query_report_cap` (default is 1000). diff --git a/changes/19612-idp-ingest b/changes/19612-idp-ingest new file mode 100644 index 0000000000..497ea956b6 --- /dev/null +++ b/changes/19612-idp-ingest @@ -0,0 +1 @@ +- Fixes issue where the MDM ingestion flow would fail if an invalid enrollment reference was passed. \ No newline at end of file diff --git a/changes/19688-fleet-mdm-detection b/changes/19688-fleet-mdm-detection new file mode 100644 index 0000000000..0005fd8f12 --- /dev/null +++ b/changes/19688-fleet-mdm-detection @@ -0,0 +1 @@ +* Improved the logic used by Fleet to detect if a host is currently MDM-managed. diff --git a/changes/conf-6385-host-policy-table-fixes b/changes/conf-6385-host-policy-table-fixes new file mode 100644 index 0000000000..c61124d153 --- /dev/null +++ b/changes/conf-6385-host-policy-table-fixes @@ -0,0 +1 @@ +- Host policy table can be sortable by response and View all host link preserves the team diff --git a/changes/feature_19010-ipad-ios-wipe b/changes/feature_19010-ipad-ios-wipe new file mode 100644 index 0000000000..872132eea9 --- /dev/null +++ b/changes/feature_19010-ipad-ios-wipe @@ -0,0 +1 @@ +* Added support to wipe iOS/iPadOS devices. diff --git a/changes/fix-s3-back-compat b/changes/fix-s3-back-compat new file mode 100644 index 0000000000..ef24c2a980 --- /dev/null +++ b/changes/fix-s3-back-compat @@ -0,0 +1 @@ +- Fixes an issue with backwards compatibility with the deprecated `FLEET_S3_*` environment variables. \ No newline at end of file diff --git a/changes/part-of-19072-use-reader-db-for-stats b/changes/part-of-19072-use-reader-db-for-stats new file mode 100644 index 0000000000..a4ad45d70c --- /dev/null +++ b/changes/part-of-19072-use-reader-db-for-stats @@ -0,0 +1 @@ +- Improved db usage when sending statistics diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index 3f7e219e6a..99680b3527 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,7 +8,7 @@ version: v6.0.2 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.51.0 +appVersion: v4.51.1 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/templates/deployment.yaml b/charts/fleet/templates/deployment.yaml index 0e9c8ec080..8b4a4d3fa0 100644 --- a/charts/fleet/templates/deployment.yaml +++ b/charts/fleet/templates/deployment.yaml @@ -267,7 +267,7 @@ spec: - name: {{ $key }} value: {{ $value | quote }} {{- end }} - ## APEND ENVIRONMENT VARIABLES FROM SECRETS/CMs + ## APPEND ENVIRONMENT VARIABLES FROM SECRETS/CMs {{- range .Values.envsFrom }} - name: {{ .name }} valueFrom: diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 0643a710c4..394798ea00 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.51.0 # Version of Fleet to deploy +imageTag: v4.51.1 # 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/fleet/main.go b/cmd/fleet/main.go index cf922702f3..eff64d3562 100644 --- a/cmd/fleet/main.go +++ b/cmd/fleet/main.go @@ -7,8 +7,8 @@ import ( "time" "github.com/fleetdm/fleet/v4/server/config" - kitlog "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" _ "github.com/go-sql-driver/mysql" "github.com/spf13/cobra" ) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 90721bf545..d645f419b8 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -54,10 +54,10 @@ import ( "github.com/fleetdm/fleet/v4/server/sso" "github.com/fleetdm/fleet/v4/server/version" "github.com/getsentry/sentry-go" - kitlog "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" kitprometheus "github.com/go-kit/kit/metrics/prometheus" "github.com/go-kit/log" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/google/uuid" "github.com/ngrok/sqlmw" "github.com/prometheus/client_golang/prometheus" @@ -74,6 +74,8 @@ import ( var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$") +const softwareInstallerUploadTimeout = 2 * time.Minute + type initializer interface { // Initialize is used to populate a datastore with // preloaded data @@ -196,7 +198,7 @@ the way that the Fleet server works. } ds = mds - if config.S3.CarvesBucket != "" { + if config.S3.CarvesBucket != "" || config.S3.Bucket != "" { carveStore, err = s3.NewCarveStore(config.S3, ds) if err != nil { initFatal(err, "initializing S3 carvestore") @@ -1027,7 +1029,7 @@ the way that the Fleet server works. } } - // We must wrap the Handler here to set special per-endpoint Write + // We must wrap the Handler here to set special per-endpoint Read/Write // timeouts, so that we have access to the raw http.ResponseWriter. // Otherwise, the handler is wrapped by the promhttp response delegator, // which does not support the Unwrap call needed to work with @@ -1038,6 +1040,9 @@ the way that the Fleet server works. // does not implement. rootMux.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) { if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/scripts/run/sync") { + // when running a script synchronously, we wait a while for a script + // execution result, so the write timeout (to write the response) + // must be extended. rc := http.NewResponseController(rw) // add an additional 30 seconds to prevent race conditions where the // request is terminated early. @@ -1045,6 +1050,25 @@ the way that the Fleet server works. level.Error(logger).Log("msg", "http middleware failed to override endpoint write timeout", "err", err) } } + + if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package") { + // when uploading a software installer, the file might be large so + // the read timeout (to read the full request body) must be extended. + rc := http.NewResponseController(rw) + // the frontend times out waiting for the upload after 2 minutes, so + // use that same timeout: + // https://www.figma.com/design/oQl2oQUG0iRkUy0YOxc307/%2314921-Deploy-security-agents-to-macOS%2C-Windows%2C-and-Linux-hosts?node-id=773-18032&t=QjEU6tc73tddNSqn-0 + if err := rc.SetReadDeadline(time.Now().Add(softwareInstallerUploadTimeout)); err != nil { + level.Error(logger).Log("msg", "http middleware failed to override endpoint read timeout", "err", err) + } + // the write timeout should be extended as well to give the server time to + // write a response body with the right error, otherwise the connection is + // terminated abruptly. + if err := rc.SetWriteDeadline(time.Now().Add(softwareInstallerUploadTimeout + 30*time.Second)); err != nil { + level.Error(logger).Log("msg", "http middleware failed to override endpoint write timeout", "err", err) + } + req.Body = http.MaxBytesReader(rw, req.Body, service.MaxSoftwareInstallerSize) + } apiHandler.ServeHTTP(rw, req) }) rootMux.Handle("/", frontendHandler) diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 5ee78f5848..9c826d0cd1 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -26,9 +26,9 @@ import ( "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/service/schedule" - kitlog "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" "github.com/go-kit/log" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mozilla.org/pkcs7" diff --git a/cmd/fleet/vuln_process.go b/cmd/fleet/vuln_process.go index a015fcab63..5d1f7410c7 100644 --- a/cmd/fleet/vuln_process.go +++ b/cmd/fleet/vuln_process.go @@ -13,8 +13,8 @@ import ( "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" - kitlog "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/spf13/cobra" ) diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 8b8e42c67a..739e9c0b3b 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -843,9 +843,7 @@ func getHostsCommand() *cli.Command { } if c.Bool("mdm") { - // hosts enrolled (automatic or manual) in Fleet's MDM server - query.Set("mdm_name", fleet.WellKnownMDMFleet) - query.Set("mdm_enrollment_status", string(fleet.MDMEnrollStatusEnrolled)) + query.Set("connected_to_fleet", "true") } if c.Bool("mdm-pending") { // hosts pending enrollment in Fleet's MDM server diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index fb2003c54b..564f5466fe 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -181,22 +181,29 @@ func TestBasicTeamGitOps(t *testing.T) { CreatedAt: time.Now(), Name: teamName, } + var savedTeam *fleet.Team ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { - if name == teamName { - return team, nil + if name == teamName && savedTeam != nil { + return savedTeam, nil } - return nil, nil + return nil, ¬FoundError{} } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { if tid == team.ID { - return team, nil + return savedTeam, nil } return nil, nil } + var enrolledTeamSecrets []*fleet.EnrollSecret + ds.NewTeamFunc = func(ctx context.Context, newTeam *fleet.Team) (*fleet.Team, error) { + newTeam.ID = team.ID + savedTeam = newTeam + enrolledTeamSecrets = newTeam.Secrets + return newTeam, nil + } ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) { return true, nil } - var savedTeam *fleet.Team ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) { savedTeam = team return team, nil @@ -205,10 +212,6 @@ func TestBasicTeamGitOps(t *testing.T) { require.ElementsMatch(t, labels, []string{fleet.BuiltinLabelMacOS14Plus}) return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil } - ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) { - declaration.DeclarationUUID = uuid.NewString() - return declaration, nil - } ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error { return nil } @@ -216,16 +219,15 @@ func TestBasicTeamGitOps(t *testing.T) { return nil } - var enrolledSecrets []*fleet.EnrollSecret ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { - enrolledSecrets = secrets + enrolledTeamSecrets = secrets return nil } tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - t.Setenv("TEST_SECRET", secret) + t.Setenv("TEST_SECRET", "") _, err = tmpFile.WriteString( ` @@ -235,7 +237,7 @@ policies: agent_options: name: ${TEST_TEAM_NAME} team_settings: - secrets: [{"secret":"${TEST_SECRET}"}] + secrets: ${TEST_SECRET} `, ) require.NoError(t, err) @@ -255,8 +257,17 @@ team_settings: _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) require.NotNil(t, savedTeam) assert.Equal(t, teamName, savedTeam.Name) - require.Len(t, enrolledSecrets, 1) - assert.Equal(t, secret, enrolledSecrets[0].Secret) + assert.Empty(t, enrolledTeamSecrets) + + // The previous run created the team, so let's rerun with an existing team + _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) + assert.Empty(t, enrolledTeamSecrets) + + // Add a secret + t.Setenv("TEST_SECRET", fmt.Sprintf("[{\"secret\":\"%s\"}]", secret)) + _ = runAppForTest(t, []string{"gitops", "-f", tmpFile.Name()}) + require.Len(t, enrolledTeamSecrets, 1) + assert.Equal(t, secret, enrolledTeamSecrets[0].Secret) } func TestFullGlobalGitOps(t *testing.T) { @@ -407,6 +418,7 @@ func TestFullGlobalGitOps(t *testing.T) { assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName) assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL) assert.Contains(t, string(*savedAppConfig.AgentOptions), "distributed_denylist_duration") + assert.Equal(t, 2000, savedAppConfig.ServerSettings.QueryReportCap) assert.Len(t, enrolledSecrets, 2) assert.True(t, policyDeleted) assert.Len(t, appliedPolicySpecs, 5) @@ -912,7 +924,6 @@ team_settings: _ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--delete-other-teams"}) assert.True(t, ds.ListTeamsFuncInvoked) assert.True(t, ds.DeleteTeamFuncInvoked) - } func TestFullGlobalAndTeamGitOps(t *testing.T) { @@ -1048,7 +1059,6 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) { } }) } - } func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, **fleet.Team) { diff --git a/cmd/fleetctl/mdm.go b/cmd/fleetctl/mdm.go index b21231dd9f..80d6fe42e5 100644 --- a/cmd/fleetctl/mdm.go +++ b/cmd/fleetctl/mdm.go @@ -6,7 +6,6 @@ import ( "net/http" "os" "slices" - "strings" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/service" @@ -116,10 +115,7 @@ func mdmRunCommand() *cli.Command { } mdmPlatform = mdmHostPlatform - // TODO(mna): this "On" check is brittle, but looks like it's the only - // enrollment indication we have right now... - if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") || - host.MDM.Name != fleet.WellKnownMDMFleet { + if host.MDM.ConnectedToFleet == nil || !*host.MDM.ConnectedToFleet { return errors.New(`Can't run the MDM command because one or more hosts have MDM turned off. Run the following command to see a list of hosts with MDM on: fleetctl get hosts --mdm.`) } @@ -316,7 +312,6 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl if err != nil { var nfe service.NotFoundErr if errors.As(err, &nfe) { - fmt.Println(hostIdent) return nil, nil, errors.New("The host doesn't exist. Please provide a valid host identifier.") } @@ -331,8 +326,7 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl // check mdm is on for the host if fleet.MDMSupported(host.Platform) { - if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") || - host.MDM.Name != fleet.WellKnownMDMFleet { + if host.MDM.ConnectedToFleet == nil || !*host.MDM.ConnectedToFleet { return nil, nil, fmt.Errorf("Can't %s the host because it doesn't have MDM turned on.", actionType) } } diff --git a/cmd/fleetctl/mdm_test.go b/cmd/fleetctl/mdm_test.go index 117a863f7b..134b775b5f 100644 --- a/cmd/fleetctl/mdm_test.go +++ b/cmd/fleetctl/mdm_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "os" "slices" @@ -36,14 +37,14 @@ func TestMDMRunCommand(t *testing.T) { UUID: "mac-enrolled", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolled := &fleet.Host{ ID: 2, UUID: "win-enrolled", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macUnenrolled := &fleet.Host{ ID: 3, @@ -65,42 +66,42 @@ func TestMDMRunCommand(t *testing.T) { UUID: "mac-enrolled-2", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolled2 := &fleet.Host{ ID: 7, UUID: "win-enrolled-2", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macNonFleetEnrolled := &fleet.Host{ ID: 8, UUID: "mac-non-fleet-enrolled", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMJamf}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMJamf, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMJamf, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(false)}, } winNonFleetEnrolled := &fleet.Host{ ID: 9, UUID: "win-non-fleet-enrolled", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMIntune}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMIntune, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMIntune, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(false)}, } macPending := &fleet.Host{ ID: 10, UUID: "mac-pending", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winPending := &fleet.Host{ ID: 11, UUID: "win-pending", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } hostByUUID := make(map[string]*fleet.Host) hostByID := make(map[uint]*fleet.Host) @@ -233,6 +234,13 @@ func TestMDMRunCommand(t *testing.T) { } return h.MDMInfo, nil } + ds.AreHostsConnectedToFleetMDMFunc = func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) { + res := make(map[string]bool, len(hosts)) + for _, h := range hosts { + res[h.UUID] = h.MDM.ConnectedToFleet != nil && *h.MDM.ConnectedToFleet + } + return res, nil + } enqueuer.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) { return map[string]error{}, nil @@ -315,14 +323,14 @@ func TestMDMLockCommand(t *testing.T) { UUID: "mac-enrolled", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolled := &fleet.Host{ ID: 2, UUID: "win-enrolled", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } linuxEnrolled := &fleet.Host{ @@ -345,57 +353,49 @@ func TestMDMLockCommand(t *testing.T) { UUID: "mac-pending", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winPending := &fleet.Host{ ID: 7, UUID: "win-pending", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winEnrolledUP := &fleet.Host{ ID: 8, UUID: "win-enrolled-up", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } - macEnrolledUP := &fleet.Host{ - ID: 9, - UUID: "mac-enrolled-up", - Platform: "darwin", - MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, - } - winEnrolledLP := &fleet.Host{ ID: 10, UUID: "win-enrolled-lp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledLP := &fleet.Host{ ID: 11, UUID: "mac-enrolled-lp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledWP := &fleet.Host{ ID: 12, UUID: "win-enrolled-wp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledWP := &fleet.Host{ ID: 13, UUID: "mac-enrolled-wp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } hostByUUID := make(map[string]*fleet.Host) @@ -409,7 +409,6 @@ func TestMDMLockCommand(t *testing.T) { macPending, winPending, winEnrolledUP, - macEnrolledUP, winEnrolledLP, macEnrolledLP, winEnrolledWP, @@ -421,7 +420,6 @@ func TestMDMLockCommand(t *testing.T) { unlockPending := map[uint]*fleet.Host{ winEnrolledUP.ID: winEnrolledUP, - macEnrolledUP.ID: macEnrolledUP, } lockPending := map[uint]*fleet.Host{ @@ -446,9 +444,7 @@ func TestMDMLockCommand(t *testing.T) { if _, ok := unlockPending[host.ID]; ok { if fleetPlatform == "darwin" { - status.UnlockPIN = "1234" - status.UnlockRequestedAt = time.Now() - return &status, nil + return nil, errors.New("apple devices do not have an unlock pending state") } status.UnlockScript = &fleet.HostScriptResult{} @@ -504,6 +500,10 @@ func TestMDMLockCommand(t *testing.T) { } } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return host.MDMInfo != nil && host.MDMInfo.Enrolled == true && host.MDMInfo.Name == fleet.WellKnownMDMFleet, nil + } + appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() successfulOutput := func(ident string) string { @@ -542,7 +542,6 @@ fleetctl mdm unlock --host=%s {appCfgWinMDM, "valid windows but pending ", []string{"--host", winPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`}, {appCfgMacMDM, "valid macos but pending", []string{"--host", macPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`}, {appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."}, - {appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."}, {appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."}, @@ -558,14 +557,14 @@ func TestMDMUnlockCommand(t *testing.T) { UUID: "mac-enrolled", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolled := &fleet.Host{ ID: 2, UUID: "win-enrolled", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } linuxEnrolled := &fleet.Host{ ID: 3, @@ -587,56 +586,49 @@ func TestMDMUnlockCommand(t *testing.T) { UUID: "mac-pending", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winPending := &fleet.Host{ ID: 7, UUID: "win-pending", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winEnrolledUP := &fleet.Host{ ID: 8, UUID: "win-enrolled-up", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, - } - macEnrolledUP := &fleet.Host{ - ID: 9, - UUID: "mac-enrolled-up", - Platform: "darwin", - MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledLP := &fleet.Host{ ID: 10, UUID: "win-enrolled-lp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledLP := &fleet.Host{ ID: 11, UUID: "mac-enrolled-lp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledWP := &fleet.Host{ ID: 12, UUID: "win-enrolled-wp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledWP := &fleet.Host{ ID: 13, UUID: "mac-enrolled-wp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } hostByUUID := make(map[string]*fleet.Host) @@ -650,7 +642,6 @@ func TestMDMUnlockCommand(t *testing.T) { macPending, winPending, winEnrolledUP, - macEnrolledUP, winEnrolledLP, macEnrolledLP, winEnrolledWP, @@ -667,7 +658,6 @@ func TestMDMUnlockCommand(t *testing.T) { unlockPending := map[uint]*fleet.Host{ winEnrolledUP.ID: winEnrolledUP, - macEnrolledUP.ID: macEnrolledUP, } lockPending := map[uint]*fleet.Host{ @@ -701,9 +691,7 @@ func TestMDMUnlockCommand(t *testing.T) { if _, ok := unlockPending[host.ID]; ok { if fleetPlatform == "darwin" { - status.UnlockPIN = "1234" - status.UnlockRequestedAt = time.Now() - return &status, nil + return nil, errors.New("apple devices do not have an unlock pending state") } status.UnlockScript = &fleet.HostScriptResult{} @@ -761,6 +749,9 @@ func TestMDMUnlockCommand(t *testing.T) { return nil, nil } } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return host.MDM.ConnectedToFleet != nil && *host.MDM.ConnectedToFleet, nil + } appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() @@ -800,7 +791,6 @@ fleetctl get host %s {appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`}, {appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`}, {appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."}, - {appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, ""}, {appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."}, @@ -816,14 +806,14 @@ func TestMDMWipeCommand(t *testing.T) { UUID: "mac-enrolled", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolled := &fleet.Host{ ID: 2, UUID: "win-enrolled", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winNotEnrolled := &fleet.Host{ ID: 4, @@ -840,84 +830,77 @@ func TestMDMWipeCommand(t *testing.T) { UUID: "mac-pending", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winPending := &fleet.Host{ ID: 7, UUID: "win-pending", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: false, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending"), ConnectedToFleet: ptr.Bool(false)}, } winEnrolledUP := &fleet.Host{ ID: 8, UUID: "win-enrolled-up", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, - } - macEnrolledUP := &fleet.Host{ - ID: 9, - UUID: "mac-enrolled-up", - Platform: "darwin", - MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledLP := &fleet.Host{ ID: 10, UUID: "win-enrolled-lp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledLP := &fleet.Host{ ID: 11, UUID: "mac-enrolled-lp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledWP := &fleet.Host{ ID: 12, UUID: "win-enrolled-wp", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledWP := &fleet.Host{ ID: 13, UUID: "mac-enrolled-wp", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledWiped := &fleet.Host{ ID: 14, UUID: "win-enrolled-wiped", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledWiped := &fleet.Host{ ID: 15, UUID: "mac-enrolled-wiped", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)"), ConnectedToFleet: ptr.Bool(true)}, } winEnrolledLocked := &fleet.Host{ ID: 16, UUID: "win-enrolled-locked", Platform: "windows", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual"), ConnectedToFleet: ptr.Bool(true)}, } macEnrolledLocked := &fleet.Host{ ID: 17, UUID: "mac-enrolled-locked", Platform: "darwin", MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet}, - MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual")}, + MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual"), ConnectedToFleet: ptr.Bool(true)}, } linuxEnrolled := &fleet.Host{ ID: 18, @@ -950,7 +933,6 @@ func TestMDMWipeCommand(t *testing.T) { macPending, winPending, winEnrolledUP, - macEnrolledUP, winEnrolledLP, macEnrolledLP, winEnrolledWP, @@ -971,7 +953,6 @@ func TestMDMWipeCommand(t *testing.T) { unlockPending := map[uint]*fleet.Host{ winEnrolledUP.ID: winEnrolledUP, - macEnrolledUP.ID: macEnrolledUP, } lockPending := map[uint]*fleet.Host{ @@ -1010,9 +991,7 @@ func TestMDMWipeCommand(t *testing.T) { if _, ok := unlockPending[host.ID]; ok { if fleetPlatform == "darwin" { - status.UnlockPIN = "1234" - status.UnlockRequestedAt = time.Now() - return &status, nil + return nil, errors.New("apple devices do not have an unlock pending state") } status.UnlockScript = &fleet.HostScriptResult{} @@ -1104,6 +1083,9 @@ func TestMDMWipeCommand(t *testing.T) { return nil, nil } } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return host.MDM.ConnectedToFleet != nil && *host.MDM.ConnectedToFleet, nil + } appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs() appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}} @@ -1129,7 +1111,6 @@ func TestMDMWipeCommand(t *testing.T) { {appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`}, {appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`}, {appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."}, - {appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."}, {appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."}, {appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."}, diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index f697b39160..60d569c19f 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -11,8 +11,8 @@ import ( "github.com/fleetdm/fleet/v4/server/live_query/live_query_mock" "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service" - kitlog "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index 7d0df63743..c6624c7110 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -11,6 +11,7 @@ "server_settings": { "server_url": "", "live_query_disabled": false, + "query_report_cap": 0, "query_reports_disabled": false, "enable_analytics": false, "deferred_save_host": false, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 01834e56a5..92254b6052 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -59,6 +59,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_report_cap: 0 query_reports_disabled: false server_url: "" scripts_disabled: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 114ba52a9c..18d980b320 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -11,6 +11,7 @@ "server_settings": { "server_url": "", "live_query_disabled": false, + "query_report_cap": 0, "query_reports_disabled": false, "enable_analytics": false, "deferred_save_host": false, diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 203246eb0d..3138a7d349 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -98,6 +98,7 @@ spec: deferred_save_host: false enable_analytics: false live_query_disabled: false + query_report_cap: 0 query_reports_disabled: false server_url: "" scripts_disabled: false diff --git a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml index f1315fcf24..f10577a3af 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml @@ -29,6 +29,7 @@ spec: enable_release_device_manually: false macos_setup_assistant: scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json index 46f48e23d6..a2513638db 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json @@ -50,7 +50,8 @@ "enrollment_status": null, "name": "", "pending_action": "", - "server_url": null + "server_url": null, + "connected_to_fleet": null }, "team_id": null, "pack_stats": null, diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml index 52057fe4b6..5a27402334 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml @@ -40,6 +40,7 @@ spec: name: "" pending_action: "" server_url: null + connected_to_fleet: null memory: 0 orbit_version: null os_version: "" diff --git a/cmd/fleetctl/testdata/expectedListHostsJson.json b/cmd/fleetctl/testdata/expectedListHostsJson.json index 6280165fdf..06b88a568d 100644 --- a/cmd/fleetctl/testdata/expectedListHostsJson.json +++ b/cmd/fleetctl/testdata/expectedListHostsJson.json @@ -49,7 +49,8 @@ "encryption_key_available": false, "enrollment_status": null, "name": "", - "server_url": null + "server_url": null, + "connected_to_fleet": null }, "team_id": null, "pack_stats": null, @@ -124,7 +125,8 @@ "encryption_key_available": false, "enrollment_status": null, "name": "", - "server_url": null + "server_url": null, + "connected_to_fleet": null }, "team_id": null, "pack_stats": null, diff --git a/cmd/fleetctl/testdata/expectedListHostsMDM.json b/cmd/fleetctl/testdata/expectedListHostsMDM.json index de36e08df9..193e4a267d 100644 --- a/cmd/fleetctl/testdata/expectedListHostsMDM.json +++ b/cmd/fleetctl/testdata/expectedListHostsMDM.json @@ -50,7 +50,8 @@ "encryption_key_available": false, "enrollment_status": null, "name": "", - "server_url": null + "server_url": null, + "connected_to_fleet": null }, "team_id": null, "pack_stats": null, @@ -125,7 +126,8 @@ "encryption_key_available": false, "enrollment_status": null, "name": "", - "server_url": null + "server_url": null, + "connected_to_fleet": null }, "team_id": null, "pack_stats": null, diff --git a/cmd/fleetctl/testdata/expectedListHostsYaml.yml b/cmd/fleetctl/testdata/expectedListHostsYaml.yml index da049feb2e..638c4e2186 100644 --- a/cmd/fleetctl/testdata/expectedListHostsYaml.yml +++ b/cmd/fleetctl/testdata/expectedListHostsYaml.yml @@ -42,6 +42,7 @@ spec: enrollment_status: null name: "" server_url: null + connected_to_fleet: null memory: 0 orbit_version: null os_version: "" @@ -101,6 +102,7 @@ spec: encryption_key_available: false enrollment_status: null server_url: null + connected_to_fleet: null memory: 0 os_version: "" osquery_version: "" diff --git a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml index 0a73e7392d..76936e3ad5 100644 --- a/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml +++ b/cmd/fleetctl/testdata/gitops/global_config_no_paths.yml @@ -101,6 +101,7 @@ org_settings: deferred_save_host: false enable_analytics: true live_query_disabled: false + query_report_cap: 2000 query_reports_disabled: false scripts_disabled: false server_url: $FLEET_SERVER_URL diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 237ea64e3a..67bb96e8c3 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -59,6 +59,7 @@ spec: deferred_save_host: false enable_analytics: true live_query_disabled: false + query_report_cap: 0 query_reports_disabled: false server_url: https://example.org scripts_disabled: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 95b1be28ac..d73894e4a1 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -59,6 +59,7 @@ spec: deferred_save_host: false enable_analytics: true live_query_disabled: false + query_report_cap: 0 query_reports_disabled: false server_url: https://example.org scripts_disabled: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml index 28f815240d..b5a4c03e5c 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml @@ -29,6 +29,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null @@ -63,6 +64,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml index ef911ec34f..a0d15fddd7 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml @@ -29,6 +29,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null @@ -63,6 +64,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml index 19f92edbc0..8a6762468c 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml @@ -29,6 +29,7 @@ spec: windows_settings: custom_settings: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml index 9862a2d66d..2aac4b1481 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Set.yml @@ -28,6 +28,7 @@ spec: deadline_days: null grace_period_days: null scripts: null + secrets: null software: null webhook_settings: host_status_webhook: null diff --git a/cmd/fleetctl/trigger_test.go b/cmd/fleetctl/trigger_test.go index 79f695eeb9..6c19253054 100644 --- a/cmd/fleetctl/trigger_test.go +++ b/cmd/fleetctl/trigger_test.go @@ -12,7 +12,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/service/schedule" - kitlog "github.com/go-kit/kit/log" + kitlog "github.com/go-kit/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/cmd/fleetctl/user.go b/cmd/fleetctl/user.go index 38b5a43edb..ce0a842c7d 100644 --- a/cmd/fleetctl/user.go +++ b/cmd/fleetctl/user.go @@ -160,7 +160,7 @@ func createUserCommand() *cli.Command { force_reset := !sso && !apiOnly // password requirements are validated as part of `CreateUser` - err = client.CreateUser(fleet.UserPayload{ + sessionKey, err := client.CreateUser(fleet.UserPayload{ Password: &password, Email: &email, Name: &name, @@ -174,6 +174,10 @@ func createUserCommand() *cli.Command { return fmt.Errorf("Failed to create user: %w", err) } + if apiOnly && sessionKey != nil && *sessionKey != "" { + fmt.Fprintf(c.App.Writer, "Success! The API token for your new user is: %s\n", *sessionKey) + } + return nil }, } @@ -208,7 +212,6 @@ func createBulkUsersCommand() *cli.Command { } defer csvFile.Close() csvLines, err := csv.NewReader(csvFile).ReadAll() - if err != nil { return err } @@ -278,7 +281,7 @@ func createBulkUsersCommand() *cli.Command { } for _, user := range users { - err = client.CreateUser(user) + _, err = client.CreateUser(user) if err != nil { return fmt.Errorf("Failed to create user: %w", err) } @@ -351,7 +354,6 @@ func deleteBulkUsersCommand() *cli.Command { } defer csvFile.Close() csvLines, err := csv.NewReader(csvFile).ReadAll() - if err != nil { return err } @@ -362,10 +364,10 @@ func deleteBulkUsersCommand() *cli.Command { } } return nil - }, } } + func generateRandomPassword() (string, error) { password, err := password.Generate(20, 2, 2, false, true) if err != nil { diff --git a/cmd/fleetctl/users_test.go b/cmd/fleetctl/users_test.go index e65cc88d3a..c3b43a5820 100644 --- a/cmd/fleetctl/users_test.go +++ b/cmd/fleetctl/users_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/csv" + "fmt" "math/big" "os" "strings" @@ -73,31 +74,57 @@ func TestUserCreateForcePasswordReset(t *testing.T) { ) error { return nil } + ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) { + if email == "bar@example.com" { + apiOnlyUser := &fleet.User{ + ID: 1, + Email: email, + } + err := apiOnlyUser.SetPassword(pwd, 24, 10) + require.NoError(t, err) + return apiOnlyUser, nil + } + return nil, ¬FoundError{} + } + var apiOnlyUserSessionKey string + ds.NewSessionFunc = func(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) { + apiOnlyUserSessionKey = sessionKey + return &fleet.Session{ + ID: 2, + UserID: userID, + Key: sessionKey, + }, nil + } for _, tc := range []struct { name string args []string expectedAdminForcePasswordReset bool + displaysToken bool }{ { name: "sso", args: []string{"--email", "foo@example.com", "--name", "foo", "--sso"}, expectedAdminForcePasswordReset: false, + displaysToken: false, }, { name: "api-only", args: []string{"--email", "bar@example.com", "--password", pwd, "--name", "bar", "--api-only"}, expectedAdminForcePasswordReset: false, + displaysToken: true, }, { name: "api-only-sso", args: []string{"--email", "baz@example.com", "--name", "baz", "--api-only", "--sso"}, expectedAdminForcePasswordReset: false, + displaysToken: false, }, { name: "non-sso-non-api-only", args: []string{"--email", "zoo@example.com", "--password", pwd, "--name", "zoo"}, expectedAdminForcePasswordReset: true, + displaysToken: false, }, } { ds.NewUserFuncInvoked = false @@ -106,10 +133,15 @@ func TestUserCreateForcePasswordReset(t *testing.T) { return user, nil } - require.Equal(t, "", runAppForTest(t, append( + stdout := runAppForTest(t, append( []string{"user", "create"}, tc.args..., - ))) + )) + if tc.displaysToken { + require.Equal(t, stdout, fmt.Sprintf("Success! The API token for your new user is: %s\n", apiOnlyUserSessionKey)) + } else { + require.Empty(t, stdout) + } require.True(t, ds.NewUserFuncInvoked) } } diff --git a/docs/Configuration/agent-configuration.md b/docs/Configuration/agent-configuration.md index 41569959ee..0c6f851b04 100644 --- a/docs/Configuration/agent-configuration.md +++ b/docs/Configuration/agent-configuration.md @@ -144,7 +144,7 @@ You can verify that these flags have taken effect on the hosts by running a quer > If you revoked an old enroll secret, this feature won't update for hosts that were added to Fleet using this old enroll secret. This is because Fleetd uses the enroll secret to receive new flags from Fleet. For these hosts, all existing features will work as expected. -For further documentation on how to rotate enroll secrets, please see [this guide](#rotating-enroll-secrets). +For further documentation on how to rotate enroll secrets, please see [this guide](https://fleetdm.com/docs/configuration/configuration-files#rotating-enroll-secrets). If you prefer to deploy a new package with the updated enroll secret: diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md index 4319759347..5058975d77 100644 --- a/docs/Configuration/fleet-server-configuration.md +++ b/docs/Configuration/fleet-server-configuration.md @@ -2807,7 +2807,7 @@ packaging: > The [`server_private_key` configuration option](#server_private_key) is required for macOS MDM features. -> The Apple Push Notification service (APNs), Simple Certificate Enrollment Protocol (SCEP), and Apple Business Manager (ABM) [certificate and key configuration](https://github.com/fleetdm/fleet/fleet-v4.51.0/main/docs/Contributing/Configuration-for-contributors.md#mobile-device-management-mdm) are deprecated as of Fleet 4.51. They are maintained for backwards compatibility. Please upload your APNs certificate and ABM token. Learn how [here](../Using%20Fleet/MDM-setup.md). +> The Apple Push Notification service (APNs), Simple Certificate Enrollment Protocol (SCEP), and Apple Business Manager (ABM) [certificate and key configuration](https://github.com/fleetdm/fleet/blob/fleet-v4.51.0/docs/Contributing/Configuration-for-contributors.md#mobile-device-management-mdm) are deprecated as of Fleet 4.51. They are maintained for backwards compatibility. Please upload your APNs certificate and ABM token. Learn how [here](https://fleetdm.com/docs/using-fleet/mdm-setup). ##### mdm.apple_scep_signer_validity_days diff --git a/docs/Contributing/Testing-and-local-development.md b/docs/Contributing/Testing-and-local-development.md index bf293ab91f..0fa2998172 100644 --- a/docs/Contributing/Testing-and-local-development.md +++ b/docs/Contributing/Testing-and-local-development.md @@ -505,95 +505,24 @@ To run your local server with the MDM features enabled, you need to get certific ### ABM setup -To enable the [DEP](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#dep-device-enrollment-program) enrollment flow, the Fleet server needs three things: - -1. A private key. -1. A certificate. -1. An encrypted token generated by Apple. - -#### Private key, certificate, and encrypted token +To enable the [DEP](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#dep-device-enrollment-program) enrollment flow, the Fleet server needs an encrypted token generated by Apple. First ask @lukeheath to create an account for you in [ABM](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#abm-apple-business-manager). You'll need an account to generate an encrypted token. -Once you have access to ABM, follow [these guided instructions](https://fleetdm.com/docs/using-fleet/mdm-setup#apple-business-manager-abm) in the user facing docs to generate the private key, certificate, and encrypted token. +Once you have access to ABM, follow [these guided instructions](https://fleetdm.com/docs/using-fleet/mdm-setup#apple-business-manager-abm) to get and upload the encrypted token. ### APNs and SCEP setup -The server also needs a private key + certificate to identify with Apple's [APNs](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#apns-apple-push-notification-service) servers, and another for [SCEP](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#scep-simple-certificate-enrollment-protocol). +The server also needs a certificate to identify with Apple's [APNs](https://github.com/fleetdm/fleet/blob/main/tools/mdm/apple/glossary-and-protocols.md#apns-apple-push-notification-service) servers. -To generate both, follow [these guided instructions](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#apple-push-notification-service-apns). +To get a certificate and upload it, [these guided instructions](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#apple-push-notification-service-apns). Note that: -1. Fleet must be running to generate the certificates and keys. +1. Fleet must be running to generate the token and certificate. 2. You must be logged in to Fleet as a global admin. See [Building Fleet](./Building-Fleet.md) for details on getting Fleet setup locally. 3. To login into https://identity.apple.com/pushcert you can use your ABM account generated in the previous step. -4. Save all the certificates and keys in a safe place. - -Internally, the certificates are generated using this flow. Note that the fleet sails API base url can be changed using the `TEST_FLEETDM_API_URL` environment variable. - -```mermaid -sequenceDiagram - participant user as user email - participant fleetctl as fleetctl - participant server as fleet server - participant fleetdm as fleetdm.com sails app - participant apple as identity.apple.com - link apple: PushCert @ https://identity.apple.com/pushcert - - note over fleetctl: fleetctl login - fleetctl->>+server: login - server-->>-fleetctl: token - note over fleetctl: fleetctl generate mdm_apple - fleetctl->>+server: generate certificates - server->>server: generate self-signed SCEP cert & key - server->>server: generate APNs key - server->>server: generate APNs CSR - server-)+fleetdm: request vendor signature on APNs CSR - server-->>-fleetctl: SCEP cert, SCEP key, APNs key - note over fleetdm: calls /ee/tools/mdm/cert - fleetdm--)-user: vendor-signed APNs CSR - user->>+apple: vendor-signed APNs CSR - note right of apple: managed through web ui - apple-->>-user: Apple-signed APNs certificate -``` - -Another option, if for some reason, generating the certificates and keys fails or you don't have a supported email address handy is to use `openssl` to generate your SCEP key pair: - -```sh -$ openssl genrsa -out fleet-mdm-apple-scep.key 4096 - -$ openssl req -x509 -new -nodes -key fleet-mdm-apple-scep.key -sha256 -days 1826 -out fleet-mdm-apple-scep.crt -subj '/CN=Fleet Root CA/C=US/O=Fleet DM.' -``` - -### Running the server - -Try to store all the certificates and tokens you generated in the earlier steps together in a safe place outside of the repo, then start the server with: - -```sh -FLEET_MDM_APPLE_SCEP_CHALLENGE=scepchallenge \ -FLEET_MDM_APPLE_SCEP_CERT=/path/to/fleet-mdm-apple-scep.crt \ -FLEET_MDM_APPLE_SCEP_KEY=/path/to/fleet-mdm-apple-scep.key \ -FLEET_MDM_APPLE_BM_SERVER_TOKEN=/path/to/dep_encrypted_token.p7m \ -FLEET_MDM_APPLE_BM_CERT=/path/to/fleet-apple-mdm-bm-public-key.crt \ -FLEET_MDM_APPLE_BM_KEY=/path/to/fleet-apple-mdm-bm-private.key \ -FLEET_MDM_APPLE_APNS_CERT=/path/to/mdmcert.download.push.pem \ -FLEET_MDM_APPLE_APNS_KEY=/path/to/mdmcert.download.push.key \ - ./build/fleet serve --dev --dev_license --logging_debug -``` - -Note: if you need to enroll VMs using MDM, the server needs to run behind TLS with a valid certificate. In a separate terminal window/tab, create a local tunnel to your server using `ngrok` (`brew install ngrok/ngrok/ngrok` if you don't have it.) - -```sh -ngrok http https://localhost:8080 -``` - -> NOTE: If this is your first time using ngrok this command will fail and you will see a message -> about signing up. Open the sign up link and complete the sign up flow. You can rerun the same command -> and ngrok should work this time. After this open the forwarding link, you will be asked to confirm that you'd like -> to be forwarded to your local server and should accept. - -Don't forget to edit your Fleet server settings (through the UI or `fleetctl`) to use the URL `ngrok` provides to you. You need to do this whenever you restart `ngrok`. +4. Save the token and certificate in a safe place. ### Testing MDM diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index f86f6f7325..4d3b4da13d 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -1,4 +1,4 @@ - # REST API +# REST API - [Authentication](#authentication) - [Activities](#activities) @@ -880,6 +880,7 @@ None. "apple_bm_terms_expired": false, "enabled_and_configured": true, "windows_enabled_and_configured": true, + "enable_disk_encryption": true, "macos_updates": { "minimum_version": "12.3.1", "deadline": "2022-01-01" @@ -889,11 +890,20 @@ None. "grace_period_days": 1 }, "macos_settings": { - "custom_settings": ["path/to/profile1.mobileconfig"], - "enable_disk_encryption": true + "custom_settings": [ + { + "path": "path/to/profile1.mobileconfig", + "labels": ["Label 1", "Label 2"] + } + ] }, "windows_settings": { - "custom_settings": ["path/to/profile2.xml"], + "custom_settings": [ + { + "path": "path/to/profile2.xml", + "labels": ["Label 3", "Label 4"] + } + ], }, "scripts": ["path/to/script.sh"], "end_user_authentication": { @@ -983,6 +993,10 @@ None. "enable_vulnerabilities_webhook":true, "destination_url": "https://server.com", "host_batch_size": 1000 + }, + "activities_webhook":{ + "enable_activities_webhook":true, + "destination_url": "https://server.com" } }, "integrations": { @@ -1098,6 +1112,8 @@ Modifies the Fleet's configuration with the supplied information. | enable_vulnerabilities_webhook | boolean | body | _webhook_settings.vulnerabilities_webhook settings_. Whether or not the vulnerabilities webhook is enabled. | | destination_url | string | body | _webhook_settings.vulnerabilities_webhook settings_. The URL to deliver the webhook requests to. | | host_batch_size | integer | body | _webhook_settings.vulnerabilities_webhook settings_. Maximum number of hosts to batch on vulnerabilities webhook requests. The default, 0, means no batching (all vulnerable hosts are sent on one request). | +| enable_activities_webhook | boolean | body | _webhook_settings.activities_webhook settings_. Whether or not the activity feed webhook is enabled. | +| destination_url | string | body | _webhook_settings.activities_webhook settings_. The URL to deliver the webhook requests to. | | enable_software_vulnerabilities | boolean | body | _integrations.jira[] settings_. Whether or not Jira integration is enabled for software vulnerabilities. Only one vulnerability automation can be enabled at a given time (enable_vulnerabilities_webhook and enable_software_vulnerabilities). | | enable_failing_policies | boolean | body | _integrations.jira[] settings_. Whether or not Jira integration is enabled for failing policies. Only one failing policy automation can be enabled at a given time (enable_failing_policies_webhook and enable_failing_policies). | | url | string | body | _integrations.jira[] settings_. The URL of the Jira server to integrate with. | @@ -1216,6 +1232,7 @@ Note that when making changes to the `integrations` object, all integrations mus "apple_bm_enabled_and_configured": false, "enabled_and_configured": false, "windows_enabled_and_configured": false, + "enable_disk_encryption": true, "macos_updates": { "minimum_version": "12.3.1", "deadline": "2022-01-01" @@ -1225,21 +1242,24 @@ Note that when making changes to the `integrations` object, all integrations mus "grace_period_days": 1 }, "macos_settings": { - "custom_settings": { - "path": "path/to/profile1.mobileconfig", - "labels": ["Label 1", "Label 2"] - }, - { - "path": "path/to/profile2.json", - "labels": ["Label 3", "Label 4"] - }, - "enable_disk_encryption": true + "custom_settings": [ + { + "path": "path/to/profile1.mobileconfig", + "labels": ["Label 1", "Label 2"] + }, + { + "path": "path/to/profile2.json", + "labels": ["Label 3", "Label 4"] + }, + ] }, "windows_settings": { - "custom_settings": { - "path": "path/to/profile3.xml", - "labels": ["Label 1", "Label 2"] - } + "custom_settings": [ + { + "path": "path/to/profile3.xml", + "labels": ["Label 1", "Label 2"] + } + ] }, "end_user_authentication": { "entity_id": "", @@ -1300,6 +1320,10 @@ Note that when making changes to the `integrations` object, all integrations mus "enable_vulnerabilities_webhook":true, "destination_url": "https://server.com", "host_batch_size": 1000 + }, + "activities_webhook":{ + "enable_activities_webhook":true, + "destination_url": "https://server.com" } }, "integrations": { @@ -5318,7 +5342,7 @@ Deletes the custom MDM setup enrollment profile assigned to a team or no team. ### Get manual enrollment profile -Retrieves the manual enrollment profile for macOS hosts. Install this profile on macOS hosts to turn on MDM features manually. +Retrieves an unsigned manual enrollment profile for macOS hosts. Install this profile on macOS hosts to turn on MDM features manually. `GET /api/v1/fleet/enrollment_profiles/manual` @@ -6012,29 +6036,19 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th ### Add policy -There are two ways of adding a policy: -1. Preferred: By setting `name`, `query`, and `description`. -2. Legacy: By setting `query_id` to reuse the data of an existing query. If `query_id` is set, -then `query` must not be set, and `name` and `description` are ignored. - -An error is returned if both `query` and `query_id` are set on the request. - `POST /api/v1/fleet/global/policies` #### Parameters | Name | Type | In | Description | | ---------- | ------- | ---- | ------------------------------------ | -| name | string | body | The query's name. | -| query | string | body | The query in SQL. | -| description | string | body | The query's description. | +| name | string | body | The policy's name. | +| query | string | body | The policy's query in SQL. | +| description | string | body | The policy's description. | | resolution | string | body | The resolution steps for the policy. | -| query_id | integer | body | An existing query's ID (legacy). | | platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. | | critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. | -Either `query` or `query_id` must be provided. - #### Example (preferred) `POST /api/v1/fleet/global/policies` @@ -6079,47 +6093,6 @@ Either `query` or `query_id` must be provided. } ``` -#### Example (legacy) - -`POST /api/v1/fleet/global/policies` - -#### Request body - -```json -{ - "query_id": 12 -} -``` - -Where `query_id` references an existing `query`. - -##### Default response - -`Status: 200` - -```json -{ - "policy": { - "id": 43, - "name": "Gatekeeper enabled", - "query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;", - "description": "Checks if gatekeeper is enabled on macOS devices", - "critical": true, - "author_id": 42, - "author_name": "John", - "author_email": "john@example.com", - "team_id": null, - "resolution": "Resolution steps", - "platform": "darwin", - "created_at": "2022-03-17T20:15:55Z", - "updated_at": "2022-03-17T20:15:55Z", - "passing_host_count": 0, - "failing_host_count": 0, - "host_count_updated_at": null - } -} -``` - ### Remove policies `POST /api/v1/fleet/global/policies/delete` @@ -6500,11 +6473,10 @@ The semantics for creating a team policy are the same as for global policies, se | Name | Type | In | Description | | ---------- | ------- | ---- | ------------------------------------ | | id | integer | path | Defines what team ID to operate on. | -| name | string | body | The query's name. | -| query | string | body | The query in SQL. | -| description | string | body | The query's description. | +| name | string | body | The policy's name. | +| query | string | body | The policy's query in SQL. | +| description | string | body | The policy's description. | | resolution | string | body | The resolution steps for the policy. | -| query_id | integer | body | An existing query's ID (legacy). | | platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. | | critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. | @@ -8164,7 +8136,7 @@ Download a software package. | ---- | ------- | ---- | -------------------------------------------- | | software_title_id | integer | path | **Required**. The ID of the software title to download software package.| | team_id | integer | form | **Required**. The team ID. Downloads a software package added to the specified team. | -| alt | integer | path | **Required**. If specified and set to "media", downloads the specified software package. | +| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. | #### Example @@ -9009,6 +8981,7 @@ _Available in Fleet Premium_ } }, "mdm": { + "enable_disk_encryption": true, "macos_updates": { "minimum_version": "12.3.1", "deadline": "2022-01-01" @@ -9018,11 +8991,20 @@ _Available in Fleet Premium_ "grace_period_days": 1 }, "macos_settings": { - "custom_settings": ["path/to/profile1.mobileconfig"], - "enable_disk_encryption": false + "custom_settings": [ + { + "path": "path/to/profile1.mobileconfig", + "labels": ["Label 1", "Label 2"] + } + ] }, "windows_settings": { - "custom_settings": ["path/to/profile2.xml"], + "custom_settings": [ + { + "path": "path/to/profile2.xml", + "labels": ["Label 3", "Label 4"] + } + ], }, "macos_setup": { "bootstrap_package": "", @@ -9293,6 +9275,7 @@ _Available in Fleet Premium_ } }, "mdm": { + "enable_disk_encryption": true, "macos_updates": { "minimum_version": "12.3.1", "deadline": "2022-01-01" @@ -9302,11 +9285,20 @@ _Available in Fleet Premium_ "grace_period_days": 1 }, "macos_settings": { - "custom_settings": ["path/to/profile1.mobileconfig"], - "enable_disk_encryption": false + "custom_settings": [ + { + "path": "path/to/profile1.mobileconfig", + "labels": ["Label 1", "Label 2"] + } + ] }, "windows_settings": { - "custom_settings": ["path/to/profile2.xml"], + "custom_settings": [ + { + "path": "path/to/profile2.xml", + "labels": ["Label 3", "Label 4"] + } + ], }, "macos_setup": { "bootstrap_package": "", diff --git a/docs/Using Fleet/MDM-commands.md b/docs/Using Fleet/MDM-commands.md index 229ba15d14..c541c7799d 100644 --- a/docs/Using Fleet/MDM-commands.md +++ b/docs/Using Fleet/MDM-commands.md @@ -1,6 +1,6 @@ # Commands -In Fleet you can run MDM commands to take action on your macOS and Windows hosts, like restarting the host, remotely. +In Fleet you can run MDM commands to take action on your macOS, iOS, iPadOS, and Windows hosts, like restarting the host, remotely. ## Custom commands @@ -85,7 +85,7 @@ You can view a list of the 1,000 latest commands: The command ID can be used to view command results as documented in [step 4 of the previous section](#step-4-view-the-commands-results). -The possible statuses for macOS hosts are the following: +The possible statuses for macOS, iOS, and iPadOS hosts are the following: * Pending: the command has yet to run on the host. The host will run the command the next time it comes online. * NotNow: the host responded with "NotNow" status via the MDM protocol: the host received the command, but couldn’t execute it. The host will try to run the command the next time it comes online. diff --git a/docs/Using Fleet/MDM-setup.md b/docs/Using Fleet/MDM-setup.md index bfc175f23c..3b56d71531 100644 --- a/docs/Using Fleet/MDM-setup.md +++ b/docs/Using Fleet/MDM-setup.md @@ -1,14 +1,14 @@ # Setup -To turn on macOS MDM features, follow the instructions on this page to connect Fleet to Apple Push Notification service (APNs). +To turn on macOS, iOS, and iPadOS MDM features, follow the instructions on this page to connect Fleet to Apple Push Notification service (APNs). -To use automatic enrollment (aka zero-touch) features on macOS, follow instructions to connect Fleet with Apple Business Manager (ABM). +To use automatic enrollment (aka zero-touch) features on macOS, iOS, and iPadOS, follow instructions to connect Fleet with Apple Business Manager (ABM). To turn on Windows MDM features, head to this [Windows MDM setup article](https://fleetdm.com/guides/windows-mdm-setup). ## Apple Push Notification service (APNs) -Apple uses APNs to authenticate and manage interactions between Fleet and the host. +Apple uses APNs to authenticate and manage interactions between Fleet and hosts. To connect Fleet to APNs or renew APNs, head to the **Settings > Integrations > Mobile device management (MDM)** page. @@ -30,9 +30,9 @@ After connecting Fleet to ABM, set Fleet to be the MDM for all Macs: 4. Click **MDM Server Assignment** and click **Edit** next to **Default Server Assignment**. 5. Switch **Mac** to Fleet. -New or wiped macOS hosts that are in ABM, before they've been set up, appear in Fleet with **MDM status** set to "Pending". +New or wiped macOS, iOS, and iPadOS hosts that are in ABM, before they've been set up, appear in Fleet with **MDM status** set to "Pending". -All hosts that automatically enroll will be assigned to the default team. If no default team is set, then the host will be placed in "No team". +All macOS hosts that automatically enroll will be assigned to the default team. If no default team is set, then the host will be placed in "No team". > A host can be transferred to a new (not default) team before it enrolls. In the Fleet UI, you can do this under **Settings** > **Teams**. diff --git a/docs/Using Fleet/segment-hosts.md b/docs/Using Fleet/segment-hosts.md index 25ec8a05ec..1047ee4dbb 100644 --- a/docs/Using Fleet/segment-hosts.md +++ b/docs/Using Fleet/segment-hosts.md @@ -1,46 +1,36 @@ # Segment hosts -`Applies only to Fleet Premium` +_Available in Fleet Premium_ -``` -ℹ️ In Fleet 4.0, Teams were introduced. -``` +In Fleet, you can group hosts together in a "team" in Fleet. This way, you can apply queries, policies, scripts, and more that are tailored to the hosts' risk/compliance needs. -- [Overview](#overview) -- [Best practice](#best-practice) -- [Transfer hosts to a team](#transfer-hosts-to-a-team) +A host can only belong to one team. -## Overview +You can give users access to only some teams. -In Fleet, you can group hosts together in a team. - -Then, you can give users access to only some teams. - -This means you manage permissions so that some users can only run queries and manage hosts on the teams these users have access to. - -You can manage teams in the Fleet UI by selecting **Settings** > **Teams** in the top navigation. From there, you can add or remove teams, manage user access to teams, transfer hosts, or modify team settings. +You can manage teams by selecting your avatar in the top navigation and then **Settings > Teams**. ## Best practice -The best practice is to create these teams: `Workstations`, `Workstations (canary)`, `Servers`, and `Servers (canary)`. - +Fleet's best practice teams: +- `Workstations`: End user's production work computers (macOS, Windows, and Linux) +- `Workstations (canary)`: IT team's test work computers. Sometimes, for demos or testing, includes end user's work computers. Used for [dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) a new workflow or feature that may or may not be rolled out to the "Workstations" team. +- `Servers`: Security team's production servers. +- `Servers (canary)`: Security team's test servers. +- `Compliance exclusions`: All contributors' test work computers or virtual machines (VMs). Used for validating workflows for Fleet customers or reproducing bugs in the Fleet product. +- `iPhones`: All contributors' test iOS hosts. Used to dogfood Fleet's iOS features (coming soon). +If some of your hosts don't fall under the above teams, what are these hosts for? The answer determines the the hosts' risk/compliance needs, and thus their security basline, and thus their "team" in Fleet. If the hosts' have a different compliance needs, and thus different security baseline, then it's time to create a new team in Fleet. ## Adding hosts to a team -Hosts can only belong to one team in Fleet. - You can add hosts to a new team in Fleet by either enrolling the host with a team's enroll secret or by transferring the host via the Fleet UI after the host has been enrolled to Fleet. -To automatically add hosts to a team in Fleet, check out the [**Adding hosts** documentation](https://fleetdm.com/docs/using-fleet/adding-hosts#automatically-adding-hosts-to-a-team). - -> If a host was previously enrolled using a global enroll secret, changing the host's osquery enroll -> secret will not cause the host to be transferred to the desired team. You must delete the -> `osquery/osquery.db` file on the host, which forces the host to re-enroll -> using the new team enroll secret. Alternatively, you can transfer the host via the Fleet UI, the -> fleetctl CLI using `fleetctl hosts transfer`, or the [transfer host API endpoint](https://fleetdm.com/docs/using-fleet/rest-api#transfer-hosts-to-a-team). +## Advanced +You can automatically enroll hosts to a specific team in Fleet by installing a fleetd with a team enroll secret. Learn more [here](./enroll-hosts.md#enroll-host-to-a-specific-team). +Changing the host's enroll secret after enrollment will not cause the host to be transferred to a different team. diff --git a/ee/cis/win-10/README.md b/ee/cis/win-10/README.md index 880dd1445b..cab5aea0c1 100644 --- a/ee/cis/win-10/README.md +++ b/ee/cis/win-10/README.md @@ -1,6 +1,6 @@ # Windows 10 Enterprise benchmarks -Fleet's policies have been written against v2.0.0 of the benchmark. You can refer to the [CIS website](https://www.cisecurity.org/cis-benchmarks) for full details about this version. +Fleet's policies have been written against v3.0.0 of the benchmark. You can refer to the [CIS website](https://www.cisecurity.org/cis-benchmarks) for full details about this version. For requirements and usage details, see the [CIS Benchmarks](https://fleetdm.com/docs/using-fleet/cis-benchmarks) documentation. @@ -12,4 +12,4 @@ For requirements and usage details, see the [CIS Benchmarks](https://fleetdm.com ### Checks that require a Group Policy template Several items require Group Policy templates in place in order to audit them. -These items are tagged with the label `CIS_group_policy_template_required` in the YAML file, and details about the required Group Policy templates can be found in each item's `resolution`. \ No newline at end of file +These items are tagged with the label `CIS_group_policy_template_required` in the YAML file, and details about the required Group Policy templates can be found in each item's `resolution`. diff --git a/ee/cis/win-11/README.md b/ee/cis/win-11/README.md index 428a365217..bd7ba47060 100644 --- a/ee/cis/win-11/README.md +++ b/ee/cis/win-11/README.md @@ -1,6 +1,6 @@ # Windows 11 Enterprise benchmarks -Fleet's policies have been written against v2.0.0 of the benchmark. You can refer to the [CIS website](https://www.cisecurity.org/cis-benchmarks) for full details about this version. +Fleet's policies have been written against v3.0.0 of the benchmark. You can refer to the [CIS website](https://www.cisecurity.org/cis-benchmarks) for full details about this version. For requirements and usage details, see the [CIS Benchmarks](https://fleetdm.com/docs/using-fleet/cis-benchmarks) documentation. @@ -12,4 +12,4 @@ For requirements and usage details, see the [CIS Benchmarks](https://fleetdm.com ### Checks that require a Group Policy template Several items require Group Policy templates in place in order to audit them. -These items are tagged with the label `CIS_group_policy_template_required` in the YAML file, and details about the required Group Policy templates can be found in each item's `resolution`. \ No newline at end of file +These items are tagged with the label `CIS_group_policy_template_required` in the YAML file, and details about the required Group Policy templates can be found in each item's `resolution`. diff --git a/ee/fleetd-chrome/package-lock.json b/ee/fleetd-chrome/package-lock.json index 0984574666..db80f64725 100644 --- a/ee/fleetd-chrome/package-lock.json +++ b/ee/fleetd-chrome/package-lock.json @@ -2256,12 +2256,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3606,9 +3606,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/ee/server/calendar/google_calendar_test.go b/ee/server/calendar/google_calendar_test.go index 02d024792e..8d35cba69e 100644 --- a/ee/server/calendar/google_calendar_test.go +++ b/ee/server/calendar/google_calendar_test.go @@ -3,7 +3,7 @@ package calendar import ( "context" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/log" + "github.com/go-kit/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/api/calendar/v3" diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index c928b64de8..24cceec733 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -8,7 +8,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" hostctx "github.com/fleetdm/fleet/v4/server/contexts/host" "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/go-kit/kit/log/level" + "github.com/go-kit/log/level" ) func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { @@ -46,13 +46,18 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos return nil } + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) + if err != nil { + return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") + } + var bre fleet.BadRequestError switch { case !ac.MDM.MacOSMigration.Enable: bre.InternalErr = ctxerr.New(ctx, "macOS migration not enabled") case ac.MDM.MacOSMigration.WebhookURL == "": bre.InternalErr = ctxerr.New(ctx, "macOS migration webhook URL not configured") - case !host.IsEligibleForDEPMigration(): + case !host.IsEligibleForDEPMigration(connected): bre.InternalErr = ctxerr.New(ctx, "host not eligible for macOS migration") } if bre.InternalErr != nil { @@ -106,11 +111,16 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu } if appCfg.MDM.EnabledAndConfigured && appCfg.MDM.MacOSMigration.Enable { - if host.NeedsDEPEnrollment() { + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) + if err != nil { + return sum, ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") + } + + if host.NeedsDEPEnrollment(connected) { sum.Notifications.RenewEnrollmentProfile = true } - if host.IsEligibleForDEPMigration() { + if host.IsEligibleForDEPMigration(connected) { sum.Notifications.NeedsMDMMigration = true } } diff --git a/ee/server/service/embedded_scripts/linux_unlock.sh b/ee/server/service/embedded_scripts/linux_unlock.sh index 2122fb837b..e981d4ce44 100644 --- a/ee/server/service/embedded_scripts/linux_unlock.sh +++ b/ee/server/service/embedded_scripts/linux_unlock.sh @@ -6,6 +6,16 @@ do echo "$user" if [ "$user" != "root" ]; then echo "Unlocking password for $user" - passwd -u $user + STDERR=$(passwd -u "$user" 2>&1 >/dev/null) + if [ $? -eq 3 ]; then + # possibly due to the user not having a password + # use this convoluted case approach to avoid bashisms (POSIX portable) + case "$STDERR" in + *"unlocking the password would result in a passwordless account"* ) + # unlock and delete password to set it back to empty + passwd -ud "$user" + ;; + esac + fi fi done diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go index e6833ee5a2..70e249d908 100644 --- a/ee/server/service/hosts.go +++ b/ee/server/service/hosts.go @@ -38,44 +38,47 @@ func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, incl return svc.Service.OSVersion(ctx, osID, teamID, true) } -func (svc *Service) LockHost(ctx context.Context, hostID uint) error { +func (svc *Service) LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error) { // First ensure the user has access to list hosts, then check the specific // host once team_id is loaded. if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { - return err + return "", err } host, err := svc.ds.HostLite(ctx, hostID) if err != nil { - return ctxerr.Wrap(ctx, err, "get host lite") + return "", ctxerr.Wrap(ctx, err, "get host lite") } // Authorize again with team loaded now that we have the host's team_id. // Authorize as "execute mdm_command", which is the correct access // requirement and is what happens for macOS platforms. if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil { - return err + return "", err } // locking validations are based on the platform of the host switch host.FleetPlatform() { + case "ios", "ipados": + return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock iOS or iPadOS hosts. Use wipe instead.")) case "darwin": if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { if errors.Is(err, fleet.ErrMDMNotConfigured) { err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest) } - return ctxerr.Wrap(ctx, err, "check macOS MDM enabled") + return "", ctxerr.Wrap(ctx, err, "check macOS MDM enabled") } // on macOS, the lock command requires the host to be MDM-enrolled in Fleet - hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID) + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) if err != nil { - if fleet.IsNotFound(err) { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on.")) - } - return ctxerr.Wrap(ctx, err, "get host MDM information") + return "", ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") } - if !hostMDM.IsFleetEnrolled() { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on.")) + if !connected { + if fleet.IsNotFound(err) { + return "", ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."), + ) + } } case "windows", "linux": @@ -84,27 +87,30 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error { if errors.Is(err, fleet.ErrMDMNotConfigured) { err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest) } - return ctxerr.Wrap(ctx, err, "check windows MDM enabled") + return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled") } } // on windows and linux, a script is used to lock the host so scripts must // be enabled appCfg, err := svc.ds.AppConfig(ctx) if err != nil { - return ctxerr.Wrap(ctx, err, "get app config") + return "", ctxerr.Wrap(ctx, err, "get app config") } if appCfg.ServerSettings.ScriptsDisabled { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings.")) + return "", ctxerr.Wrap( + ctx, + fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."), + ) } hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID) switch { case err != nil: // If not found, then do nothing. We do not know if this host has scripts enabled or not if !fleet.IsNotFound(err) { - return ctxerr.Wrap(ctx, err, "get host orbit info") + return "", ctxerr.Wrap(ctx, err, "get host orbit info") } case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled: - return ctxerr.Wrap( + return "", ctxerr.Wrap( ctx, fleet.NewInvalidArgumentError( "host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.", ), @@ -112,26 +118,37 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error { } default: - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform))) + return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform))) } // if there's a lock, unlock or wipe action pending, do not accept the lock // request. lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host) if err != nil { - return ctxerr.Wrap(ctx, err, "get host lock/wipe status") + return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status") } switch { case lockWipe.IsPendingLock(): - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online.")) + return "", ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online."), + ) case lockWipe.IsPendingUnlock(): - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.")) + return "", ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError( + "host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.", + ), + ) case lockWipe.IsPendingWipe(): - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped.")) + return "", ctxerr.Wrap( + ctx, + fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."), + ) case lockWipe.IsWiped(): - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped.")) + return "", ctxerr.Wrap( + ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."), + ) case lockWipe.IsLocked(): - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict)) + return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict)) } // all good, go ahead with queuing the lock request. @@ -158,7 +175,7 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error) // locking validations are based on the platform of the host switch host.FleetPlatform() { - case "darwin": + case "darwin", "ios", "ipados": // all good, no need to check if MDM enrolled, will validate later that it // is currently locked. @@ -249,7 +266,7 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error { // uses scripts, not MDM. var requireMDM bool switch host.FleetPlatform() { - case "darwin": + case "darwin", "ios", "ipados": if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { if errors.Is(err, fleet.ErrMDMNotConfigured) { err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest) @@ -297,14 +314,11 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error { if requireMDM { // the wipe command requires the host to be MDM-enrolled in Fleet - hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID) + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) if err != nil { - if fleet.IsNotFound(err) { - return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe the host because it doesn't have MDM turned on.")) - } - return ctxerr.Wrap(ctx, err, "get host MDM information") + return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") } - if !hostMDM.IsFleetEnrolled() { + if !connected { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't wipe the host because it doesn't have MDM turned on.")) } } @@ -331,19 +345,21 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error { return svc.enqueueWipeHostRequest(ctx, host, lockWipe) } -func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) error { +func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) ( + unlockPIN string, err error, +) { vc, ok := viewer.FromContext(ctx) if !ok { - return fleet.ErrNoContext + return "", fleet.ErrNoContext } if lockStatus.HostFleetPlatform == "darwin" { lockCommandUUID := uuid.NewString() - if err := svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil { - return ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin") + if unlockPIN, err = svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil { + return "", ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin") } - if err := svc.NewActivity( + if err = svc.NewActivity( ctx, vc.User, fleet.ActivityTypeLockedHost{ @@ -351,10 +367,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host HostDisplayName: host.DisplayName(), }, ); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for darwin lock host request") + return "", ctxerr.Wrap(ctx, err, "create activity for darwin lock host request") } - return nil + return unlockPIN, nil } script := windowsLockScript @@ -374,7 +390,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host UserID: &vc.User.ID, SyncRequest: false, }, host.FleetPlatform()); err != nil { - return err + return "", err } if err := svc.NewActivity( @@ -385,10 +401,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host HostDisplayName: host.DisplayName(), }, ); err != nil { - return ctxerr.Wrap(ctx, err, "create activity for lock host request") + return "", ctxerr.Wrap(ctx, err, "create activity for lock host request") } - return nil + return "", nil } func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (string, error) { @@ -399,7 +415,9 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho var unlockPIN string if lockStatus.HostFleetPlatform == "darwin" { - // record the unlock request if it was not already recorded + // Record the unlock request time if it was not already recorded. + // It should be always recorded, since the UnlockRequestedAt time is created after the lock command is acknowledged. + // This code is left here to catch potential issues. if lockStatus.UnlockRequestedAt.IsZero() { if err := svc.ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC()); err != nil { return "", err @@ -449,7 +467,7 @@ func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host } switch wipeStatus.HostFleetPlatform { - case "darwin": + case "darwin", "ios", "ipados": wipeCommandUUID := uuid.NewString() if err := svc.mdmAppleCommander.EraseDevice(ctx, host, wipeCommandUUID); err != nil { return ctxerr.Wrap(ctx, err, "enqueuing wipe request for darwin") diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 7af4d4f9b8..cfaae9d50d 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -30,7 +30,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage" "github.com/fleetdm/fleet/v4/server/sso" "github.com/fleetdm/fleet/v4/server/worker" - kitlog "github.com/go-kit/kit/log" + kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/google/uuid" ) @@ -135,7 +135,7 @@ func (svc *Service) MDMAppleDeviceLock(ctx context.Context, hostID uint) error { return err } - err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String()) + _, err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String()) if err != nil { return err } @@ -892,16 +892,16 @@ func (svc *Service) MDMAppleMatchPreassignment(ctx context.Context, externalHost return err // will return a not found error if host does not exist } - hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID) - if err != nil || !hostMDM.IsFleetEnrolled() { - if err == nil || fleet.IsNotFound(err) { - err = errors.New("host is not enrolled in Fleet MDM") - return ctxerr.Wrap(ctx, &fleet.BadRequestError{ - Message: err.Error(), - InternalErr: err, - }) - } - return err + connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host) + if err != nil { + return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet") + } + if !connected { + err = errors.New("host is not enrolled in Fleet MDM") + return ctxerr.Wrap(ctx, &fleet.BadRequestError{ + Message: err.Error(), + InternalErr: err, + }) } // Collect the profiles' groups in case we need to create a new team, diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index 3fd9a8978f..424d417ee6 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -25,7 +25,7 @@ import ( "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/test" "github.com/fleetdm/fleet/v4/server/worker" - kitlog "github.com/go-kit/kit/log" + kitlog "github.com/go-kit/log" "github.com/google/uuid" "github.com/stretchr/testify/require" ) diff --git a/ee/server/service/service.go b/ee/server/service/service.go index 88314b9b76..0a0eab6861 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -10,7 +10,7 @@ import ( apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage" "github.com/fleetdm/fleet/v4/server/sso" - kitlog "github.com/go-kit/kit/log" + kitlog "github.com/go-kit/log" ) // Service wraps a free Service and implements additional premium functionality on top of it. diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 59c95de659..989b5ab26e 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -181,7 +181,7 @@ func (svc *Service) getSoftwareInstallerBinary(ctx context.Context, storageID st return nil, ctxerr.Wrap(ctx, err, "checking if installer exists") } if !exists { - return nil, ctxerr.Wrap(ctx, err, "does not exist in software installer store") + return nil, ctxerr.Wrap(ctx, notFoundError{}, "does not exist in software installer store") } // get the installer from the store diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 452bf80f9b..bc05d8960a 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -19,7 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/worker" - "github.com/go-kit/kit/log/level" + "github.com/go-kit/log/level" ) func obfuscateSecrets(user *fleet.User, teams []*fleet.Team) error { @@ -537,10 +537,7 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error { mdmHostSerials := make([]string, 0, len(hosts)) for _, host := range hosts { hostIDs = append(hostIDs, host.ID) - // FIXME: These checks don't work here because host.MDMInfo is not being populated by - // ds.ListHosts call (it populates host.MDM instead). This may be happening in other - // places too. - if host.MDMInfo.IsPendingDEPFleetEnrollment() || host.MDMInfo.IsDEPFleetEnrolled() { + if host.IsDEPAssignedToFleet() { mdmHostSerials = append(mdmHostSerials, host.HardwareSerial) } } @@ -773,10 +770,17 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, for _, spec := range specs { var secrets []*fleet.EnrollSecret - for _, secret := range spec.Secrets { - secrets = append(secrets, &fleet.EnrollSecret{ - Secret: secret.Secret, - }) + // When secrets slice is empty, all secrets are removed. + // When secrets slice is nil, existing secrets are kept. + if spec.Secrets != nil { + secrets = make([]*fleet.EnrollSecret, 0, len(*spec.Secrets)) + for _, secret := range *spec.Secrets { + secrets = append( + secrets, &fleet.EnrollSecret{ + Secret: secret.Secret, + }, + ) + } } var create bool @@ -804,7 +808,7 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, } } } - if len(spec.Secrets) > fleet.MaxEnrollSecretsCount { + if len(secrets) > fleet.MaxEnrollSecretsCount { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("secrets", "too many secrets"), "validate secrets") } if err := spec.MDM.MacOSUpdates.Validate(); err != nil { @@ -816,8 +820,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if create { - // create a new team enroll secret if none is provided for a new team. - if len(secrets) == 0 { + // create a new team enroll secret if none is provided for a new team, + // unless the user explicitly passed in an empty array + if secrets == nil { secret, err := server.GenerateRandomText(fleet.EnrollSecretDefaultLength) if err != nil { return nil, ctxerr.Wrap(ctx, err, "generate enroll secret string") @@ -1125,7 +1130,7 @@ func (svc *Service) editTeamFromSpec( team.Config.Software = spec.Software } - if len(secrets) > 0 { + if secrets != nil { team.Secrets = secrets } @@ -1179,8 +1184,8 @@ func (svc *Service) editTeamFromSpec( return err } - // only replace enroll secrets if at least one is provided (#6774) - if len(secrets) > 0 { + // If no secrets are provided and user did not explicitly specify an empty list, do not replace secrets. (#6774) + if secrets != nil { if err := svc.ds.ApplyEnrollSecrets(ctx, ptr.Uint(team.ID), secrets); err != nil { return err } diff --git a/ee/vulnerability-dashboard/docker-compose.yml b/ee/vulnerability-dashboard/docker-compose.yml index 054743ea65..4155099a18 100644 --- a/ee/vulnerability-dashboard/docker-compose.yml +++ b/ee/vulnerability-dashboard/docker-compose.yml @@ -12,6 +12,7 @@ services: sails_datastores__default__adapter: sails-postgresql sails_sockets__url: redis://redis:6379 sails_session__url: redis://redis:6379 + sails_models__migrate: safe sails_custom__fleetBaseUrl: '' #Add the base url of your Fleet instance: ex: https://fleet.example.com sails_custom__fleetApiToken: '' # Add the API token of an API-only user [?] Here's how you get one: https://fleetdm.com/docs/using-fleet/fleetctl-cli#get-the-api-token-of-an-api-only-user sails_custom__fleetApiOptionalCookie: '' # If your fleet instance requires optional cookies, use this to interact with the APIs diff --git a/frontend/__mocks__/queryReportMock.ts b/frontend/__mocks__/queryReportMock.ts index eb538473d9..016812e7d4 100644 --- a/frontend/__mocks__/queryReportMock.ts +++ b/frontend/__mocks__/queryReportMock.ts @@ -320,6 +320,7 @@ const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = { }, }, ], + report_clipped: false, }; const createMockQueryReport = ( diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index e81d9de244..b519b6427f 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -360,7 +360,7 @@ const PlatformWrapper = ({ )} {/* TODO: replace with InputFieldHiddenContent component */} (
@@ -107,7 +106,6 @@ const TargetsInput = ({ columnConfigs={selectedHostsTableConifg} data={targetedHosts} isLoading={false} - resultsTitle="" showMarkAllPages={false} isAllPagesSelected={false} disableCount diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 62d3b3c378..8f8906ee1c 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -43,7 +43,7 @@ interface IDataTableProps { showMarkAllPages: boolean; isAllPagesSelected: boolean; // TODO: make dependent on showMarkAllPages toggleAllPagesSelected?: any; // TODO: an event type and make it dependent on showMarkAllPages - resultsTitle: string; + resultsTitle?: string; defaultPageSize: number; defaultPageIndex?: number; primarySelectAction?: IActionButtonProps; @@ -85,7 +85,7 @@ const DataTable = ({ showMarkAllPages, isAllPagesSelected, toggleAllPagesSelected, - resultsTitle, + resultsTitle = "results", defaultPageSize, defaultPageIndex, primarySelectAction, diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 15e1c290c5..3b68cbb470 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -12,7 +12,6 @@ import Icon from "components/Icon/Icon"; import { COLORS } from "styles/var/colors"; import DataTable from "./DataTable/DataTable"; -import TableContainerUtils from "./utilities/TableContainerUtils"; import { IActionButtonProps } from "./DataTable/ActionButton/ActionButton"; export interface ITableQueryData { @@ -44,7 +43,8 @@ interface ITableContainerProps { inputPlaceHolder?: string; disableActionButton?: boolean; disableMultiRowSelect?: boolean; - resultsTitle: string; + /** resultsTitle used in DataTable for matching results text */ + resultsTitle?: string; resultsHtml?: JSX.Element; additionalQueries?: string; emptyComponent: React.ElementType; @@ -64,10 +64,6 @@ interface ITableContainerProps { primarySelectAction?: IActionButtonProps; /** Secondary button/s after selecting a row */ secondarySelectActions?: IActionButtonProps[]; // TODO: Combine with primarySelectAction as these are all rendered in the same spot - /** - * @deprecated please use renderCount instead - * */ - filteredCount?: number; searchToolTipText?: string; // TODO - consolidate this functionality within `filters` searchQueryColumn?: string; @@ -103,7 +99,6 @@ interface ITableContainerProps { * bar and API call so TableContainer will reset its page state to 0 */ resetPageIndex?: boolean; disableTableHeader?: boolean; - show0Count?: boolean; } const baseClass = "table-container"; @@ -140,7 +135,6 @@ const TableContainer = ({ disableCount, primarySelectAction, secondarySelectActions, - filteredCount, searchToolTipText, isClientSidePagination, onClientSidePaginationChange, @@ -160,7 +154,6 @@ const TableContainer = ({ setExportRows, resetPageIndex, disableTableHeader, - show0Count, }: ITableContainerProps) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const [sortHeader, setSortHeader] = useState(defaultSortHeader || ""); @@ -252,16 +245,6 @@ const TableContainer = ({ additionalQueries, ]); - // TODO: refactor existing components relying on displayCount to use renderCount pattern - const displayCount = useCallback((): any => { - if (typeof filteredCount === "number") { - return filteredCount; - } else if (typeof clientFilterCount === "number") { - return clientFilterCount; - } - return data?.length || 0; - }, [filteredCount, clientFilterCount, data]); - const renderPagination = useCallback(() => { if (disablePagination || isClientSidePagination) { return null; @@ -309,37 +292,16 @@ const TableContainer = ({ stackControls ? "stack-table-controls" : "" }`} > - - {renderCount && ( -
- {renderCount()} -
- )} - {!renderCount && - !disableCount && - (isMultiColumnFilter || displayCount() || show0Count) ? ( -
- {TableContainerUtils.generateResultsCountText( - resultsTitle, - displayCount(), - show0Count - )} - {resultsHtml} -
- ) : ( -
- )} - + {renderCount && !disableCount && ( +
+ {renderCount()} +
+ )} {actionButton && !actionButton.hideButton && ( )} -
+ ); }, [isLoadingHostsCount, hostsCount]); diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index b052b1f504..7053e754a1 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -273,14 +273,6 @@ } &__export-btn { - margin-left: $pad-medium; - - img { - width: 13px; - height: 13px; - margin-left: 8px; - position: relative; - top: -2px; - } + margin-left: $pad-xsmall; } } diff --git a/frontend/pages/hosts/details/DeviceUserPage/AutoEnrollMdmModal/AutoEnrollMdmModal.tsx b/frontend/pages/hosts/details/DeviceUserPage/AutoEnrollMdmModal/AutoEnrollMdmModal.tsx index 3114d73530..2f40fa82b6 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/AutoEnrollMdmModal/AutoEnrollMdmModal.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/AutoEnrollMdmModal/AutoEnrollMdmModal.tsx @@ -2,16 +2,27 @@ import React from "react"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; +import { IDeviceUserResponse } from "interfaces/host"; interface IAutoEnrollMdmModalProps { + host: IDeviceUserResponse["host"]; onCancel: () => void; } const baseClass = "auto-enroll-mdm-modal"; const AutoEnrollMdmModal = ({ + host: { platform, os_version }, onCancel, }: IAutoEnrollMdmModalProps): JSX.Element => { + let isMacOsSonomaOrLater = false; + if (platform === "darwin" && os_version.startsWith("macOS ")) { + const [major] = os_version + .replace("macOS ", "") + .split(".") + .map((s) => parseInt(s, 10)); + isMacOsSonomaOrLater = major >= 14; + } return (
  1. - Open your Mac’s notification center by selecting the date and time - in the top right corner of your screen. + From the Apple menu in the top left corner of your screen, select{" "} + System Settings or System Preferences.
  2. - Select the Device Enrollment notification. This will open{" "} - System Settings or System Preferences. Select{" "} - Allow. + {isMacOsSonomaOrLater ? ( + <> + In the sidebar menu, select Enroll in Remote Management, + and select Enroll. + + ) : ( + <> + In the search bar, type “Profiles.” Select Profiles, find + and select Enrollment Profile, and select Install. + + )}
  3. Enter your password, and select Enroll. diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index b052371d50..275baa8c32 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -311,7 +311,7 @@ const DeviceUserPage = ({ const renderEnrollMdmModal = () => { return host?.dep_assigned_to_fleet ? ( - + ) : ( )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 82a7f85dac..8ec9181d04 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -230,7 +230,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -259,7 +259,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -289,7 +289,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -319,7 +319,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -347,7 +347,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Non Fleet MDM" + isConnectedToFleetMdm={false} hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -376,7 +376,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="offline" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -409,7 +409,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={1} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="windows" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -549,7 +549,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -579,7 +579,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="debian" hostMdmDeviceStatus="unlocked" hostScriptsEnabled={false} @@ -621,7 +621,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="Off" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -651,7 +651,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Non Fleet MDM" + isConnectedToFleetMdm={false} hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -683,7 +683,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="locked" hostScriptsEnabled @@ -713,7 +713,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocking" hostScriptsEnabled @@ -743,7 +743,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="Off" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="locked" hostScriptsEnabled @@ -773,7 +773,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Non Fleet MDM" + isConnectedToFleetMdm={false} hostPlatform="darwin" hostMdmDeviceStatus="locked" hostScriptsEnabled @@ -785,7 +785,7 @@ describe("Host Actions Dropdown", () => { expect(screen.queryByText("Unlock")).not.toBeInTheDocument(); }); - it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => { + it("does not renders when a macOS host but does not have Fleet mac mdm enabled and configured", async () => { const render = createCustomRenderer({ context: { app: { @@ -804,7 +804,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="locked" hostScriptsEnabled @@ -834,7 +834,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="offline" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="windows" hostMdmDeviceStatus="locked" hostScriptsEnabled={false} @@ -878,7 +878,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -909,7 +909,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="windows" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -921,7 +921,7 @@ describe("Host Actions Dropdown", () => { expect(screen.queryByText("Wipe")).not.toBeInTheDocument(); }); - it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => { + it("does not renders when a macOS host but does not have Fleet macOS mdm enabled and configured", async () => { const render = createCustomRenderer({ context: { app: { @@ -940,7 +940,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmDeviceStatus="unlocked" hostScriptsEnabled @@ -970,7 +970,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostMdmEnrollmentStatus="On (automatic)" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="debian" hostMdmDeviceStatus="unlocked" hostScriptsEnabled={false} @@ -1011,7 +1011,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={null} onSelect={noop} hostStatus="offline" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="windows" hostMdmEnrollmentStatus={null} hostMdmDeviceStatus="unlocked" @@ -1039,7 +1039,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={null} onSelect={noop} hostStatus="offline" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="windows" hostMdmEnrollmentStatus={null} hostMdmDeviceStatus="unlocked" @@ -1085,7 +1085,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={null} onSelect={noop} hostStatus="online" - mdmName="Fleet" + isConnectedToFleetMdm hostPlatform="darwin" hostMdmEnrollmentStatus={null} hostMdmDeviceStatus="unlocked" @@ -1139,55 +1139,85 @@ describe("Host Actions Dropdown", () => { }); }); - describe("Does not render dropdown for certain platforms", () => { - it("does not render dropdown for iOS", async () => { + describe("Render options only available for iOS and iPadOS", () => { + it("renders only the transfer, wipe, and delete options for iOS", async () => { const render = createCustomRenderer({ context: { app: { + isPremiumTier: true, isGlobalAdmin: true, + isMacMdmEnabledAndConfigured: true, currentUser: createMockUser(), }, }, }); - render( + const { user } = render( ); - expect(screen.queryByText("Actions")).not.toBeInTheDocument(); + await user.click(screen.getByText("Actions")); + + expect(screen.queryByText("Transfer")).toBeInTheDocument(); + expect(screen.queryByText("Wipe")).toBeInTheDocument(); + expect(screen.queryByText("Delete")).toBeInTheDocument(); + + expect(screen.queryByText("Query")).not.toBeInTheDocument(); + expect(screen.queryByText("Run script")).not.toBeInTheDocument(); + expect( + screen.queryByText("Show disk encryption key") + ).not.toBeInTheDocument(); + expect(screen.queryByText("Turn off MDM")).not.toBeInTheDocument(); + expect(screen.queryByText("Lock")).not.toBeInTheDocument(); }); - it("does not render dropdown for iPadOS", async () => { + it("renders only the transfer, wipe, and delete options for iPadOS", async () => { const render = createCustomRenderer({ context: { app: { + isPremiumTier: true, isGlobalAdmin: true, + isMacMdmEnabledAndConfigured: true, currentUser: createMockUser(), }, }, }); - render( + const { user } = render( ); - expect(screen.queryByText("Actions")).not.toBeInTheDocument(); + await user.click(screen.getByText("Actions")); + + expect(screen.queryByText("Transfer")).toBeInTheDocument(); + expect(screen.queryByText("Wipe")).toBeInTheDocument(); + expect(screen.queryByText("Delete")).toBeInTheDocument(); + + expect(screen.queryByText("Query")).not.toBeInTheDocument(); + expect(screen.queryByText("Run script")).not.toBeInTheDocument(); + expect( + screen.queryByText("Show disk encryption key") + ).not.toBeInTheDocument(); + expect(screen.queryByText("Turn off MDM")).not.toBeInTheDocument(); + expect(screen.queryByText("Lock")).not.toBeInTheDocument(); }); }); }); diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index 21e5f807c7..c283527410 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -19,7 +19,7 @@ interface IHostActionsDropdownProps { * unlocking, locking, ...etc) */ hostMdmDeviceStatus: HostMdmDeviceStatusUIState; doesStoreEncryptionKey?: boolean; - mdmName?: string; + isConnectedToFleetMdm?: boolean; hostPlatform?: string; onSelect: (value: string) => void; hostScriptsEnabled: boolean | null; @@ -31,7 +31,7 @@ const HostActionsDropdown = ({ hostMdmEnrollmentStatus, hostMdmDeviceStatus, doesStoreEncryptionKey, - mdmName, + isConnectedToFleetMdm, hostPlatform = "", hostScriptsEnabled = false, onSelect, @@ -68,7 +68,7 @@ const HostActionsDropdown = ({ isEnrolledInMdm: ["On (automatic)", "On (manual)"].includes( hostMdmEnrollmentStatus ?? "" ), - isFleetMdm: mdmName === "Fleet", + isConnectedToFleetMdm, isMacMdmEnabledAndConfigured, isWindowsMdmEnabledAndConfigured, doesStoreEncryptionKey: doesStoreEncryptionKey ?? false, @@ -79,10 +79,6 @@ const HostActionsDropdown = ({ // No options to render. Exit early if (options.length === 0) return null; - if (hostPlatform === "ios" || hostPlatform === "ipados") { - return null; - } - return (
    { const canEditMdm = (config: IHostActionConfigOptions) => { const { + hostPlatform, isGlobalAdmin, isGlobalMaintainer, isTeamAdmin, isTeamMaintainer, isEnrolledInMdm, - isFleetMdm, + isConnectedToFleetMdm, isMacMdmEnabledAndConfigured, } = config; return ( - config.hostPlatform === "darwin" && + hostPlatform === "darwin" && isMacMdmEnabledAndConfigured && isEnrolledInMdm && - isFleetMdm && + isConnectedToFleetMdm && (isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) ); }; +const canQueryHost = ({ hostPlatform }: IHostActionConfigOptions) => { + // Currently we cannot query iOS or iPadOS + const isIosOrIpadosHost = hostPlatform === "ios" || hostPlatform === "ipados"; + + return !isIosOrIpadosHost; +}; + const canLockHost = ({ isPremiumTier, hostPlatform, isMacMdmEnabledAndConfigured, isEnrolledInMdm, - isFleetMdm, + isConnectedToFleetMdm, isGlobalAdmin, isGlobalMaintainer, isTeamAdmin, @@ -118,7 +126,7 @@ const canLockHost = ({ // macOS hosts can be locked if they are enrolled in MDM and the MDM is enabled const canLockDarwin = hostPlatform === "darwin" && - isFleetMdm && + isConnectedToFleetMdm && isMacMdmEnabledAndConfigured && isEnrolledInMdm; @@ -138,7 +146,7 @@ const canWipeHost = ({ isGlobalMaintainer, isTeamAdmin, isTeamMaintainer, - isFleetMdm, + isConnectedToFleetMdm, isEnrolledInMdm, isMacMdmEnabledAndConfigured, isWindowsMdmEnabledAndConfigured, @@ -146,17 +154,18 @@ const canWipeHost = ({ hostMdmDeviceStatus, }: IHostActionConfigOptions) => { const hostMdmEnabled = - (hostPlatform === "darwin" && isMacMdmEnabledAndConfigured) || + (isAppleDevice(hostPlatform) && isMacMdmEnabledAndConfigured) || (hostPlatform === "windows" && isWindowsMdmEnabledAndConfigured); - // macOS and Windows hosts have the same conditions and can be wiped if they + // Windows and Apple devices (i.e. macOS, iOS, iPadOS) have the same conditions and can be wiped if they // are enrolled in MDM and the MDM is enabled. - const canWipeMacOrWindows = hostMdmEnabled && isFleetMdm && isEnrolledInMdm; + const canWipeWindowsOrAppleOS = + hostMdmEnabled && isConnectedToFleetMdm && isEnrolledInMdm; return ( isPremiumTier && hostMdmDeviceStatus === "unlocked" && - (isLinuxLike(hostPlatform) || canWipeMacOrWindows) && + (isLinuxLike(hostPlatform) || canWipeWindowsOrAppleOS) && (isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) ); }; @@ -167,7 +176,7 @@ const canUnlock = ({ isGlobalMaintainer, isTeamAdmin, isTeamMaintainer, - isFleetMdm, + isConnectedToFleetMdm, isEnrolledInMdm, isMacMdmEnabledAndConfigured, hostPlatform, @@ -175,7 +184,7 @@ const canUnlock = ({ }: IHostActionConfigOptions) => { const canUnlockDarwin = hostPlatform === "darwin" && - isFleetMdm && + isConnectedToFleetMdm && isMacMdmEnabledAndConfigured && isEnrolledInMdm; @@ -205,8 +214,12 @@ const canDeleteHost = (config: IHostActionConfigOptions) => { }; const canShowDiskEncryption = (config: IHostActionConfigOptions) => { - const { isPremiumTier, doesStoreEncryptionKey } = config; - return isPremiumTier && doesStoreEncryptionKey; + const { isPremiumTier, doesStoreEncryptionKey, hostPlatform } = config; + + // Currently we cannot show disk encryption key for iOS or iPadOS + const isIosOrIpadosHost = hostPlatform === "ios" || hostPlatform === "ipados"; + + return isPremiumTier && doesStoreEncryptionKey && !isIosOrIpadosHost; }; const canRunScript = ({ @@ -237,6 +250,10 @@ const removeUnavailableOptions = ( options = options.filter((option) => option.value !== "transfer"); } + if (!canQueryHost(config)) { + options = options.filter((option) => option.value !== "query"); + } + if (!canShowDiskEncryption(config)) { options = options.filter((option) => option.value !== "diskEncryption"); } @@ -266,9 +283,8 @@ const removeUnavailableOptions = ( } // TODO: refactor to filter in one pass using predefined filters specified for each of the - // DEFAULT_OPTIONS. Note that as currently, structured the default is to include all options. For - // example, "Query" is implicitly included by default because there is no equivalent `canQuery` - // filter being applied here. This is a bit confusing since + // DEFAULT_OPTIONS. Note that as currently, structured the default is to include all options. + // This is a bit confusing since we remove options instead of add options return options; }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 91ddf98ef4..28ec8c5539 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -77,7 +77,7 @@ import TransferHostModal from "../../components/TransferHostModal"; import DeleteHostModal from "../../components/DeleteHostModal"; import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal"; -import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown"; +import HostActionsDropdown from "./HostActionsDropdown/HostActionsDropdown"; import OSSettingsModal from "../OSSettingsModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal"; import RunScriptModal from "./modals/RunScriptModal"; @@ -672,7 +672,7 @@ const HostDetailsPage = ({ } return ( - ); @@ -774,8 +774,8 @@ const HostDetailsPage = ({
    diff --git a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx index 3913e855ec..ea3110af9b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx @@ -10,8 +10,8 @@ const baseClass = "host-details-banners"; interface IHostDetailsBannersProps { hostMdmEnrollmentStatus?: MdmEnrollmentStatus | null; hostPlatform?: string; - mdmName?: string; diskEncryptionStatus: DiskEncryptionStatus | null | undefined; + connectedToFleetMdm?: boolean; } /** @@ -20,7 +20,7 @@ interface IHostDetailsBannersProps { const HostDetailsBanners = ({ hostMdmEnrollmentStatus, hostPlatform, - mdmName, + connectedToFleetMdm, diskEncryptionStatus, }: IHostDetailsBannersProps) => { const { config, isPremiumTier, apnsExpiry, abmExpiry } = useContext( @@ -63,7 +63,7 @@ const HostDetailsBanners = ({ const showDiskEncryptionUserActionRequired = !showingAppWideBanner && config?.mdm.enabled_and_configured && - mdmName === "Fleet" && + connectedToFleetMdm && diskEncryptionStatus === "action_required"; if (showTurnOnMdmInfoBanner || showDiskEncryptionUserActionRequired) { diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx index 344c0c8b36..c870ab55e4 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx @@ -37,40 +37,10 @@ interface IDropdownCellProps { }; } -const ScriptRunActionDropdownLabel = ({ - scriptId, - disabled, -}: { - scriptId: number; - disabled: boolean; -}) => { - const tipId = `run-script-${scriptId}`; - return disabled ? ( - <> - - Run - - - Script is already running. - - - ) : ( - <>Run - ); -}; - const generateActionDropdownOptions = ( currentUser: IUser | null, teamId: number | null, - { script_id, last_execution }: IHostScript + { last_execution }: IHostScript ): IDropdownOption[] => { const hasRunPermission = !!currentUser && @@ -89,14 +59,10 @@ const generateActionDropdownOptions = ( value: "showDetails", }, { - label: ( - - ), + label: "Run", disabled: last_execution?.status === "pending", value: "run", + tooltipContent: "Script is already running.", }, ]; return hasRunPermission ? options : options.slice(0, 1); diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/_styles.scss index 78a4e7d7ca..89c4c4213b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/_styles.scss @@ -25,15 +25,6 @@ } } - .Select-option { - .dropdown__option { - [data-id="tooltip"] { - font-size: $xx-small; - font-style: normal; - } - } - } - // style a basic span that doesn't use the dropdown component (which relies on react-select // and makes it difficult for us to style the disabled tooltip underline on the placeholder text. .run-script-action--disabled { diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx index 30969112a1..eb957762ee 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/HQRTable.tsx @@ -2,6 +2,7 @@ import Button from "components/buttons/Button"; import EmptyTable from "components/EmptyTable"; import Icon from "components/Icon"; import TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; import React, { useCallback, useState } from "react"; import { Row } from "react-table"; import { @@ -119,15 +120,14 @@ const HQRTable = ({ }, [lastFetched, hostName, reportClipped]); const renderCount = useCallback(() => { - const count = filteredResults.length; return ( -
    - {`${count} result${count === 1 ? "" : "s"}`} + <> + Last fetched{" "} -
    + ); }, [filteredResults.length, lastFetched]); diff --git a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss index 9fb768a1ee..917d35355b 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss +++ b/frontend/pages/hosts/details/HostQueryReport/HQRTable/_styles.scss @@ -1,30 +1,14 @@ .hqr-table { gap: $pad-medium; - &__results-count-and-last-fetched { - display: flex; - align-items: baseline; - gap: $pad-small; - .last-fetched { - font-weight: initial; - @include grey-text; - } + .last-fetched { + font-weight: initial; + @include grey-text; } + &__results-cta { display: flex; gap: $pad-medium; - .button { - height: auto; - } - } - - &__export-btn { - .children-wrapper { - align-self: flex-end; - } - .icon { - display: initial; - } } &__query-info { diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx index 640da82716..3e8abd790c 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx @@ -138,7 +138,9 @@ const OSSettingsErrorCell = ({ // we dont want the default "w250" class so we pass in empty string classes={""} className={ - showRefetchButton ? `${baseClass}__failed-message` : undefined + isFailed || showRefetchButton + ? `${baseClass}__failed-message` + : undefined } /> {showRefetchButton && ( diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 753fb99968..e45c01616c 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -111,7 +111,7 @@ interface IHostSummaryProps { toggleOSSettingsModal?: () => void; toggleBootstrapPackageModal?: () => void; hostMdmProfiles?: IHostMdmProfile[]; - mdmName?: string; + isConnectedToFleetMdm?: boolean; showRefetchSpinner: boolean; onRefetchHost: ( evt: React.MouseEvent @@ -172,7 +172,7 @@ const HostSummary = ({ toggleOSSettingsModal, toggleBootstrapPackageModal, hostMdmProfiles, - mdmName, + isConnectedToFleetMdm, showRefetchSpinner, onRefetchHost, renderActionDropdown, @@ -411,9 +411,7 @@ const HostSummary = ({ {/* Rendering of OS Settings data */} {(platform === "darwin" || platform === "windows") && isPremiumTier && - // TODO: API INTEGRATION: change this when we figure out why the API is - // returning "Fleet" or "FleetDM" for the MDM name. - mdmName?.includes("Fleet") && // show if 1 - host is enrolled in Fleet MDM, and + isConnectedToFleetMdm && // show if 1 - host is enrolled in Fleet MDM, and hostMdmProfiles && hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced - platform === "darwin" + isAppleDevice(platform) ? "Host is locked. The end user can’t use the host until the six-digit PIN has been entered." : "Host is locked. The end user can’t use the host until the host has been unlocked.", }, @@ -43,7 +44,7 @@ export const DEVICE_STATUS_TAGS: DeviceStatusTagConfig = { title: "WIPED", tagType: "error", generateTooltip: (platform) => - platform === "darwin" + isAppleDevice(platform) ? "Host is wiped. To prevent the host from automatically reenrolling to Fleet, first release the host from Apple Business Manager and then delete the host in Fleet." : "Host is wiped.", }, diff --git a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx index 53f91b5a9b..e3bb3550eb 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx @@ -1,7 +1,11 @@ -import React from "react"; +import React, { useCallback } from "react"; +import { InjectedRouter } from "react-router"; +import { Row } from "react-table"; +import { noop } from "lodash"; import { IHostPolicy } from "interfaces/policy"; -import { SUPPORT_LINK } from "utilities/constants"; +import { PolicyResponse, SUPPORT_LINK } from "utilities/constants"; +import { createHostsByPolicyPath } from "utilities/helpers"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; import Card from "components/Card"; @@ -21,6 +25,15 @@ interface IPoliciesProps { deviceUser?: boolean; togglePolicyDetailsModal: (policy: IHostPolicy) => void; hostPlatform: string; + router: InjectedRouter; + currentTeamId?: number; +} + +interface IHostPoliciesRowProps extends Row { + original: { + id: number; + response: "pass" | "fail"; + }; } const Policies = ({ @@ -29,8 +42,13 @@ const Policies = ({ deviceUser, togglePolicyDetailsModal, hostPlatform, + router, + currentTeamId, }: IPoliciesProps): JSX.Element => { - const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal); + const tableHeaders = generatePolicyTableHeaders( + togglePolicyDetailsModal, + currentTeamId + ); if (deviceUser) { // Remove view all hosts link tableHeaders.pop(); @@ -38,6 +56,23 @@ const Policies = ({ const failingResponses: IHostPolicy[] = policies.filter((policy: IHostPolicy) => policy.response === "fail") || []; + const onClickRow = useCallback( + (row: IHostPoliciesRowProps) => { + const { id: policyId, response: policyResponse } = row.original; + + const viewAllHostPath = createHostsByPolicyPath( + policyId, + policyResponse === "pass" + ? PolicyResponse.PASSING + : PolicyResponse.FAILING, + currentTeamId + ); + + router.push(viewAllHostPath); + }, + [router] + ); + const renderHostPolicies = () => { if (hostPlatform === "ios" || hostPlatform === "ipados") { return ( @@ -83,14 +118,16 @@ const Policies = ({ columnConfigs={tableHeaders} data={generatePolicyDataSet(policies)} isLoading={isLoading} - manualSortBy - resultsTitle="policy items" + defaultSortHeader="response" + defaultSortDirection="asc" + resultsTitle="policies" emptyComponent={() => <>} showMarkAllPages={false} isAllPagesSelected={false} - disablePagination disableCount - disableMultiRowSelect + disableMultiRowSelect={!deviceUser} // Removes hover/click state if deviceUser + isClientSidePagination + onClickRow={deviceUser ? noop : onClickRow} /> ); diff --git a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx index 5b2d19c6e7..96455ddad7 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/HostPoliciesTableConfig.tsx @@ -1,8 +1,11 @@ import React from "react"; -import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon"; -import Button from "components/buttons/Button"; + import { IHostPolicy } from "interfaces/policy"; import { PolicyResponse, DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; + +import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon"; +import Button from "components/buttons/Button"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; import ViewAllHostsLink from "components/ViewAllHostsLink"; import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon"; @@ -42,7 +45,8 @@ interface IDataColumn { // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generatePolicyTableHeaders = ( - togglePolicyDetails: (policy: IHostPolicy, teamId?: number) => void + togglePolicyDetails: (policy: IHostPolicy, teamId?: number) => void, + currentTeamId?: number ): IDataColumn[] => { const STATUS_CELL_VALUES: Record = { pass: { @@ -65,12 +69,17 @@ const generatePolicyTableHeaders = ( disableSortBy: true, Cell: (cellProps) => { const { name } = cellProps.row.original; + + const onClickPolicyName = (e: React.MouseEvent) => { + // Allows for button to be clickable in a clickable row + e.stopPropagation(); + togglePolicyDetails(cellProps.row.original); + }; + return (