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.
Scripts Verify 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
+
+
+
+[ ](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`
+
+
+
+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 && (
{
+ return {generateResultsCountText(name, count)} ;
+};
+
+export default TableCount;
diff --git a/frontend/components/TableContainer/TableCount/index.ts b/frontend/components/TableContainer/TableCount/index.ts
new file mode 100644
index 0000000000..0db848b744
--- /dev/null
+++ b/frontend/components/TableContainer/TableCount/index.ts
@@ -0,0 +1 @@
+export { default } from "./TableCount";
diff --git a/frontend/components/TableContainer/_styles.scss b/frontend/components/TableContainer/_styles.scss
index afb718466c..326937a5f5 100644
--- a/frontend/components/TableContainer/_styles.scss
+++ b/frontend/components/TableContainer/_styles.scss
@@ -54,9 +54,6 @@
height: 38px; // Fixes overlap with .Select outline
}
}
- .results-count {
- height: 40px; // Match height of search/filters
- }
&__results-count {
display: flex;
@@ -66,6 +63,7 @@
color: $core-fleet-black;
margin: 0;
height: 40px;
+ gap: 12px;
.count-error {
color: $ui-error;
diff --git a/frontend/components/TableContainer/utilities/TableContainerUtils.ts b/frontend/components/TableContainer/utilities/TableContainerUtils.ts
index e825324eda..92b9f704c9 100644
--- a/frontend/components/TableContainer/utilities/TableContainerUtils.ts
+++ b/frontend/components/TableContainer/utilities/TableContainerUtils.ts
@@ -1,11 +1,10 @@
const DEFAULT_RESULTS_NAME = "results";
-const generateResultsCountText = (
+export const generateResultsCountText = (
name: string = DEFAULT_RESULTS_NAME,
- resultsCount: number,
- show0Count = false
+ resultsCount?: number
): string => {
- if (resultsCount === 0 && !show0Count) return `No ${name}`;
+ if (!resultsCount || resultsCount === 0) return `0 ${name}`;
// If there is 1 result and the last 3 letters in the result
// name are "ies," we remove the "ies" and add "y"
// to make the name singular
diff --git a/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx b/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx
index 37992b099d..eb3a69d6ad 100644
--- a/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx
+++ b/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx
@@ -55,7 +55,7 @@ class UserSettingsForm extends Component {
autofocus
label="Email (required)"
helpText={renderEmailHelpText()}
- disabled={!smtpConfigured}
+ readOnly={!smtpConfigured}
tooltip={
<>
Editing your email address requires that SMTP or SES is
diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tsx b/frontend/components/forms/fields/Checkbox/Checkbox.tsx
index 9f8b5aabc7..b7455880b8 100644
--- a/frontend/components/forms/fields/Checkbox/Checkbox.tsx
+++ b/frontend/components/forms/fields/Checkbox/Checkbox.tsx
@@ -11,6 +11,9 @@ const baseClass = "fleet-checkbox";
export interface ICheckboxProps {
children?: ReactNode;
className?: string;
+ /** readOnly displays a non-editable field */
+ readOnly?: boolean;
+ /** disabled displays a greyed out non-editable field */
disabled?: boolean;
name?: string;
onChange?: any; // TODO: meant to be an event; figure out type for this
@@ -28,6 +31,7 @@ const Checkbox = (props: ICheckboxProps) => {
const {
children,
className,
+ readOnly = false,
disabled = false,
name,
onChange = noop,
@@ -57,11 +61,13 @@ const Checkbox = (props: ICheckboxProps) => {
);
const checkBoxTickClass = classnames(`${baseClass}__tick`, {
+ [`${baseClass}__tick--read-only`]: readOnly || disabled,
[`${baseClass}__tick--disabled`]: disabled,
[`${baseClass}__tick--indeterminate`]: indeterminate,
});
const checkBoxLabelClass = classnames(checkBoxClass, {
+ [`${baseClass}__label--read-only`]: readOnly || disabled,
[`${baseClass}__label--disabled`]: disabled,
});
@@ -78,7 +84,7 @@ const Checkbox = (props: ICheckboxProps) => {
{
this.input = r;
@@ -180,7 +186,7 @@ class InputField extends Component {
>
{
platform as typeof HOST_LINUX_PLATFORMS[number]
);
};
+
+export const isAppleDevice = (platform: string) => {
+ return HOST_APPLE_PLATFORMS.includes(
+ platform as typeof HOST_APPLE_PLATFORMS[number]
+ );
+};
diff --git a/frontend/interfaces/query_report.ts b/frontend/interfaces/query_report.ts
index 051357827e..97fc32dd90 100644
--- a/frontend/interfaces/query_report.ts
+++ b/frontend/interfaces/query_report.ts
@@ -9,4 +9,5 @@ export interface IQueryReportResultRow {
export interface IQueryReport {
query_id: number;
results: IQueryReportResultRow[];
+ report_clipped: boolean;
}
diff --git a/frontend/interfaces/script.ts b/frontend/interfaces/script.ts
index 0d3acce6e3..67d9572052 100644
--- a/frontend/interfaces/script.ts
+++ b/frontend/interfaces/script.ts
@@ -9,7 +9,7 @@ export interface IScript {
}
export const isScriptSupportedPlatform = (hostPlatform: string) =>
- ["darwin", "windows", ...HOST_LINUX_PLATFORMS].includes(hostPlatform); // excludes chrome, see also https://github.com/fleetdm/fleet/blob/5a21e2cfb029053ddad0508869eb9f1f23997bf2/server/fleet/hosts.go#L775
+ ["darwin", "windows", ...HOST_LINUX_PLATFORMS].includes(hostPlatform); // excludes chrome, ios, ipados see also https://github.com/fleetdm/fleet/blob/5a21e2cfb029053ddad0508869eb9f1f23997bf2/server/fleet/hosts.go#L775
export type IScriptExecutionStatus = "ran" | "pending" | "error";
diff --git a/frontend/pages/AccountPage/APITokenModal/TokenSecretField/SecretField.tsx b/frontend/pages/AccountPage/APITokenModal/TokenSecretField/SecretField.tsx
index 5f54f08b6f..e5e5f82363 100644
--- a/frontend/pages/AccountPage/APITokenModal/TokenSecretField/SecretField.tsx
+++ b/frontend/pages/AccountPage/APITokenModal/TokenSecretField/SecretField.tsx
@@ -65,7 +65,7 @@ const SecretField = ({ secret }: ISecretFieldProps): JSX.Element | null => {
return (
boolean | void;
+ onQueryChange?:
+ | ((queryData: ITableQueryData) => void)
+ | ((queryData: ITableQueryData) => number);
router: InjectedRouter;
}
@@ -53,8 +56,6 @@ const Software = ({
teamId,
router,
}: ISoftwareCardProps): JSX.Element => {
- const { noSandboxHosts } = useContext(AppContext);
-
const tableHeaders = useMemo(() => generateTableHeaders(teamId), [teamId]);
const handleRowSelect = (row: IRowProps) => {
diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx
index 97cc5006cf..bb912ce9a3 100644
--- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx
@@ -6,13 +6,13 @@ import { Row } from "react-table";
import PATHS from "router/paths";
-import { AppContext } from "context/app";
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
import CustomLink from "components/CustomLink";
import TableContainer from "components/TableContainer";
import LastUpdatedText from "components/LastUpdatedText";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
import { IOSVersionsResponse } from "services/entities/operating_systems";
@@ -54,8 +54,6 @@ const SoftwareOSTable = ({
isLoading,
resetPageIndex,
}: ISoftwareOSTableProps) => {
- const { isSandboxMode, noSandboxHosts } = useContext(AppContext);
-
const determineQueryParamChange = useCallback(
(newTableQuery: ITableQueryData) => {
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
@@ -130,34 +128,19 @@ const SoftwareOSTable = ({
router.push(path);
};
- const getItemsCountText = () => {
- const count = data?.count;
- if (!data?.os_versions || !count) return "";
-
- return count === 1 ? `${count} item` : `${count} items`;
- };
-
- const getLastUpdatedText = () => {
- if (!data?.os_versions || !data?.counts_updated_at) return "";
- return (
-
- );
- };
-
const renderSoftwareCount = () => {
- const itemText = getItemsCountText();
- const lastUpdatedText = getLastUpdatedText();
-
- if (!itemText) return null;
+ if (!data?.os_versions || !data?.count) return null;
return (
-
- {itemText}
- {lastUpdatedText}
-
+ <>
+
+ {data?.os_versions && data?.counts_updated_at && (
+
+ )}
+ >
);
};
@@ -182,7 +165,11 @@ const SoftwareOSTable = ({
isLoading={isLoading}
resultsTitle="items"
emptyComponent={() => (
-
+
)}
defaultSortHeader={orderKey}
defaultSortDirection={orderDirection}
diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/_styles.scss
index 867c2948fa..a730d8b9c4 100644
--- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/_styles.scss
+++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/_styles.scss
@@ -1,10 +1,4 @@
.software-os-table {
- &__count {
- display: flex;
- gap: 12px;
- align-items: center;
- }
-
.hosts-cell__wrapper {
display: flex;
align-items: center;
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx
index 008e78f514..8f95044ae0 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AdvancedOptionsModal/AdvancedOptionsModal.tsx
@@ -1,7 +1,5 @@
import React from "react";
-// @ts-ignore
-import InputField from "components/forms/fields/InputField";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import FleetAce from "components/FleetAce";
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx
index 81e4850763..c3032d3dcd 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTable.tsx
@@ -31,6 +31,7 @@ import Slider from "components/forms/fields/Slider";
import CustomLink from "components/CustomLink";
import LastUpdatedText from "components/LastUpdatedText";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
@@ -187,24 +188,9 @@ const SoftwareTable = ({
// determines if a user be able to search in the table
const searchable =
isSoftwareEnabled &&
- (!!tableData || query !== "" || softwareFilter === "vulnerableSoftware");
-
- const getItemsCountText = () => {
- const count = data?.count;
- if (!tableData || !count) return "";
-
- return count === 1 ? `${count} item` : `${count} items`;
- };
-
- const getLastUpdatedText = () => {
- if (!tableData || !data?.counts_updated_at) return "";
- return (
-
- );
- };
+ ((tableData && tableData.length > 0) ||
+ query !== "" ||
+ softwareFilter !== "allSoftware");
const handleShowVersionsToggle = () => {
const queryParams: Record = {
@@ -276,20 +262,26 @@ const SoftwareTable = ({
};
const renderSoftwareCount = () => {
- const itemText = getItemsCountText();
- const lastUpdatedText = getLastUpdatedText();
-
- if (!itemText) return null;
+ if (!tableData || !data?.count) return null;
return (
-
- {itemText}
- {lastUpdatedText}
-
+ <>
+
+ {tableData && data?.counts_updated_at && (
+
+ )}
+ >
);
};
const renderCustomFilters = () => {
+ // Hide filters if no software is detected with no filters present
+ if (query === "" && !showVersions && softwareFilter === "allSoftware")
+ return <>>;
+
const options = showVersions
? SOFTWARE_VERSIONS_DROPDOWN_OPTIONS
: SOFTWARE_TITLES_DROPDOWN_OPTIONS;
@@ -341,8 +333,7 @@ const SoftwareTable = ({
)}
defaultSortHeader={orderKey}
diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss
index d6e62005a9..c005ead615 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss
+++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss
@@ -1,10 +1,4 @@
.software-table {
- &__count {
- display: flex;
- gap: 12px;
- align-items: center;
- }
-
&__vuln_dropdown {
.Select-menu-outer {
width: 250px;
@@ -39,7 +33,6 @@
.table-container {
&__header {
flex-direction: column-reverse; // Search bar on top
- margin-bottom: $pad-medium;
@media (min-width: $table-controls-break) {
flex-direction: row;
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx
index 5c08be062d..0ec5fcd96b 100644
--- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tests.tsx
@@ -93,9 +93,7 @@ describe("Software Vulnerabilities table", () => {
/>
);
- expect(
- screen.getByText("No software match the current search criteria")
- ).toBeInTheDocument();
+ expect(screen.getByText("No software detected")).toBeInTheDocument();
expect(screen.queryByText("Vulnerability")).toBeNull();
});
@@ -133,7 +131,7 @@ describe("Software Vulnerabilities table", () => {
);
expect(
- screen.getByText("No software match the current search criteria")
+ screen.getByText("No items match the current search criteria")
).toBeInTheDocument();
expect(screen.queryByText("Vulnerability")).toBeNull();
});
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx
index fff3a1be4c..bb6301a9b0 100644
--- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTable.tsx
@@ -15,6 +15,7 @@ import CustomLink from "components/CustomLink";
import TableContainer from "components/TableContainer";
import LastUpdatedText from "components/LastUpdatedText";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
import { IVulnerabilitiesResponse } from "services/entities/vulnerabilities";
@@ -60,9 +61,7 @@ const SoftwareVulnerabilitiesTable = ({
isLoading,
resetPageIndex,
}: ISoftwareVulnerabilitiesTableProps) => {
- const { isPremiumTier, isSandboxMode, noSandboxHosts } = useContext(
- AppContext
- );
+ const { isPremiumTier } = useContext(AppContext);
const determineQueryParamChange = useCallback(
(newTableQuery: ITableQueryData) => {
@@ -139,7 +138,6 @@ const SoftwareVulnerabilitiesTable = ({
if (!data) return [];
return generateTableConfig(
isPremiumTier,
- isSandboxMode,
router,
{
includeName: true,
@@ -182,34 +180,19 @@ const SoftwareVulnerabilitiesTable = ({
router.push(path);
};
- const getItemsCountText = () => {
- const count = data?.count;
- if (!data?.vulnerabilities || !count) return "";
-
- return count === 1 ? `${count} item` : `${count} items`;
- };
-
- const getLastUpdatedText = () => {
- if (!data?.vulnerabilities || !data?.counts_updated_at) return "";
- return (
-
- );
- };
-
const renderVulnerabilityCount = () => {
- const itemText = getItemsCountText();
- const lastUpdatedText = getLastUpdatedText();
-
- if (!itemText) return null;
+ if (!data?.vulnerabilities || !data?.count) return null;
return (
-
- {itemText}
- {lastUpdatedText}
-
+ <>
+
+ {data?.vulnerabilities && data?.counts_updated_at && (
+
+ )}
+ >
);
};
@@ -269,7 +252,11 @@ const SoftwareVulnerabilitiesTable = ({
isLoading={isLoading}
resultsTitle={"items"}
emptyComponent={() => (
-
+
)}
defaultSortHeader={orderKey}
defaultSortDirection={orderDirection}
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx
index 558e98c2c8..9c5418bc1d 100644
--- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx
@@ -14,7 +14,6 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import LinkCell from "components/TableContainer/DataTable/LinkCell";
import TooltipWrapper from "components/TooltipWrapper";
-import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
interface ICellProps {
@@ -57,7 +56,6 @@ interface IVulnerabilitiesTableConfigOptions {
const generateTableHeaders = (
isPremiumTier?: boolean,
- isSandboxMode?: boolean,
router?: InjectedRouter,
configOptions?: IVulnerabilitiesTableConfigOptions,
teamId?: number
@@ -124,7 +122,6 @@ const generateTableHeaders = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode && }
>
);
},
@@ -157,7 +154,6 @@ const generateTableHeaders = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode && }
>
);
},
@@ -191,7 +187,6 @@ const generateTableHeaders = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode && }
>
);
},
@@ -225,7 +220,6 @@ const generateTableHeaders = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode && }
>
);
},
diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx b/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx
index 7aa3c39e63..3f83cac725 100644
--- a/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx
+++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/AddSoftwareModal.tsx
@@ -1,21 +1,23 @@
import React, { useContext, useEffect, useState } from "react";
import { InjectedRouter } from "react-router";
+import { AxiosResponse } from "axios";
import PATHS from "router/paths";
-import team, { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
-import { getErrorReason } from "interfaces/errors";
+import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
import softwareAPI from "services/entities/software";
import { NotificationContext } from "context/notification";
import { QueryParams, buildQueryStringFromParams } from "utilities/url";
+import { IApiError } from "interfaces/errors";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import AddSoftwareForm from "../AddSoftwareForm";
import { IAddSoftwareFormData } from "../AddSoftwareForm/AddSoftwareForm";
+import { getErrorMessage } from "./helpers";
-// 2 minutes
-const UPLOAD_TIMEOUT = 120000;
+// 2 minutes + 15 seconds to account for extra roundtrip time.
+const UPLOAD_TIMEOUT = (2 * 60 + 15) * 1000;
const MAX_FILE_SIZE_MB = 500;
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
@@ -97,7 +99,7 @@ const AddSoftwareModal = ({
// TODO: confirm we are deleting the second sentence (not modifying it) for non-self-service installers
try {
- await softwareAPI.addSoftwarePackage(formData, teamId);
+ await softwareAPI.addSoftwarePackage(formData, teamId, UPLOAD_TIMEOUT);
renderFlash(
"success",
<>
@@ -120,7 +122,7 @@ const AddSoftwareModal = ({
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
);
} catch (e) {
- renderFlash("error", getErrorReason(e));
+ renderFlash("error", getErrorMessage(e));
onExit();
}
diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts b/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts
new file mode 100644
index 0000000000..bf6bef64a8
--- /dev/null
+++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/helpers.ts
@@ -0,0 +1,13 @@
+import { getErrorReason } from "interfaces/errors";
+
+const UPLOAD_ERROR_MESSAGES = {
+ default: {
+ message: "Couldn't add. Please try again.",
+ },
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export const getErrorMessage = (err: unknown) => {
+ if (typeof err === "string") return err;
+ return getErrorReason(err) || UPLOAD_ERROR_MESSAGES.default.message;
+};
diff --git a/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
index fffeba692d..f53f86fa0e 100644
--- a/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
+++ b/frontend/pages/SoftwarePage/components/EmptySoftwareTable/EmptySoftwareTable.tsx
@@ -10,32 +10,46 @@ import { ISoftwareDropdownFilterVal } from "pages/SoftwarePage/SoftwareTitles/So
export interface IEmptySoftwareTableProps {
softwareFilter?: ISoftwareDropdownFilterVal;
+ /** tableName is displayed in the search empty state */
+ tableName?: string;
isSoftwareDisabled?: boolean;
+ /** isNotDetectingSoftware renders empty states when no search string is present */
+ isNotDetectingSoftware?: boolean;
+ /** isCollectingSoftware is only used on the Dashboard page with a TODO to revisit */
isCollectingSoftware?: boolean;
- isSearching?: boolean;
}
-const generateTypeText = (softwareFilter?: ISoftwareDropdownFilterVal) => {
+const generateTypeText = (
+ tableName: string,
+ softwareFilter?: ISoftwareDropdownFilterVal
+) => {
if (softwareFilter === "installableSoftware") {
- return "installable";
+ return "installable software";
}
- return softwareFilter === "vulnerableSoftware" ? "vulnerable" : "";
+ if (softwareFilter === "vulnerableSoftware") {
+ return "vulnerable software";
+ }
+ return tableName;
};
const EmptySoftwareTable = ({
softwareFilter,
+ tableName = "software",
isSoftwareDisabled,
+ isNotDetectingSoftware,
isCollectingSoftware,
- isSearching,
}: IEmptySoftwareTableProps): JSX.Element => {
- const softwareTypeText = generateTypeText(softwareFilter);
+ const softwareTypeText = generateTypeText(tableName, softwareFilter);
const emptySoftware: IEmptyTableProps = {
- header: `No ${softwareTypeText} software match the current search criteria`,
- info:
- "This report is updated every hour to protect the performance of your devices.",
+ header: "No items match the current search criteria",
+ info: `Expecting to see ${softwareTypeText}? Check back later.`,
};
+ if (isNotDetectingSoftware) {
+ emptySoftware.header = "No software detected";
+ }
+
if (isCollectingSoftware) {
emptySoftware.header = "No software detected";
emptySoftware.info =
@@ -56,11 +70,6 @@ const EmptySoftwareTable = ({
>
);
}
- if (softwareFilter === "vulnerableSoftware" && !isSearching) {
- emptySoftware.header = "No vulnerable software detected";
- emptySoftware.info =
- "This report is updated every hour to protect the performance of your devices.";
- }
return (
{
- const { isPremiumTier, isSandboxMode } = useContext(AppContext);
+ const { isPremiumTier } = useContext(AppContext);
const classNames = classnames(baseClass, className);
@@ -88,14 +87,8 @@ const SoftwareVulnerabilitiesTable = ({
};
const tableHeaders = useMemo(
- () =>
- generateTableConfig(
- Boolean(isPremiumTier),
- Boolean(isSandboxMode),
- router,
- teamIdForApi
- ),
- [isPremiumTier, isSandboxMode]
+ () => generateTableConfig(Boolean(isPremiumTier), router, teamIdForApi),
+ [isPremiumTier]
);
return (
diff --git a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx
index 2dcd3ec14f..6dce48bf74 100644
--- a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx
+++ b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx
@@ -10,7 +10,6 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCel
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
-import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import ProbabilityOfExploit from "components/ProbabilityOfExploit/ProbabilityOfExploit";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import LinkCell from "components/TableContainer/DataTable/LinkCell";
@@ -49,7 +48,6 @@ interface IDataColumn {
const generateTableConfig = (
isPremiumTier: boolean,
- isSandboxMode: boolean,
router: InjectedRouter,
teamId?: number
): IDataColumn[] => {
@@ -108,7 +106,6 @@ const generateTableConfig = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode &&
}
>
);
},
@@ -141,7 +138,6 @@ const generateTableConfig = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode &&
}
>
);
},
@@ -175,7 +171,6 @@ const generateTableConfig = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode &&
}
>
);
},
@@ -209,7 +204,6 @@ const generateTableConfig = (
value={titleWithTooltip}
isSortedDesc={headerProps.column.isSortedDesc}
/>
- {isSandboxMode &&
}
>
);
},
diff --git a/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
index e99b23e262..2e93fc9272 100644
--- a/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
+++ b/frontend/pages/SoftwarePage/components/icons/SoftwareIcon/SoftwareIcon.tsx
@@ -1,10 +1,5 @@
-import React, { ComponentType, SVGProps } from "react";
-import {
- SOFTWARE_NAME_TO_ICON_MAP,
- SOFTWARE_SOURCE_TO_ICON_MAP,
- SOFTWARE_ICON_SIZES,
- SoftwareIconSizes,
-} from "../";
+import React from "react";
+import getMatchedSoftwareIcon from "../";
const baseClass = "software-icon";
@@ -14,43 +9,20 @@ interface ISoftwareIconProps {
size?: SoftwareIconSizes;
}
-const matchInMap = (
- map: Record
>>,
- potentialKey?: string
-) => {
- if (!potentialKey) {
- return null;
- }
+const SOFTWARE_ICON_SIZES: Record = {
+ medium: "24",
+ meduim_large: "64", // TODO: rename this to large and update large to xlarge
+ large: "96",
+} as const;
- const sanitizedKey = potentialKey.trim().toLowerCase();
- const match = Object.entries(map).find(([namePrefix, icon]) => {
- if (sanitizedKey.startsWith(namePrefix)) {
- return icon;
- }
- return null;
- });
-
- return match ? match[1] : null;
-};
+type SoftwareIconSizes = keyof typeof SOFTWARE_ICON_SIZES;
const SoftwareIcon = ({
- name,
- source,
+ name = "",
+ source = "",
size = "medium",
}: ISoftwareIconProps) => {
- // try to find a match for name
- let MatchedIcon = matchInMap(SOFTWARE_NAME_TO_ICON_MAP, name);
-
- // otherwise, try to find a match for source
- if (!MatchedIcon) {
- MatchedIcon = matchInMap(SOFTWARE_SOURCE_TO_ICON_MAP, source);
- }
-
- // default to 'package'
- if (!MatchedIcon) {
- MatchedIcon = SOFTWARE_SOURCE_TO_ICON_MAP.package;
- }
-
+ const MatchedIcon = getMatchedSoftwareIcon({ name, source });
return (
= {
- medium: "24",
- meduim_large: "64", // TODO: rename this to large and update large to xlarge
- large: "96",
-} as const;
+/**
+ * This attempts to loosely match the provided string to a key in a provided dictionary, returning the key if the
+ * provided string starts with the key or undefined otherwise.
+ */
+const matchLoosePrefixToKey = >(
+ dict: T,
+ s: string
+) => {
+ s = s.trim().toLowerCase();
+ if (!s) {
+ return undefined;
+ }
+ const match = Object.keys(dict).find((k) =>
+ s.startsWith(k.trim().toLowerCase())
+ );
-export type SoftwareIconSizes = keyof typeof SOFTWARE_ICON_SIZES;
+ return match ? (match as keyof T) : undefined;
+};
+
+/**
+ * This strictly matches the provided name and source to a software icon, returning the icon if a match is found or
+ * null otherwise. It is intended to be used for special cases where a strict match is required
+ * (e.g. Zoom). The caller should handle null cases by falling back to loose matching on name prefixes.
+ */
+const matchStrictNameSourceToIcon = ({
+ name = "",
+ source = "",
+}: Pick) => {
+ name = name.trim().toLowerCase();
+ source = source.trim().toLowerCase();
+ switch (true) {
+ case name === "zoom.us.app" && source === "apps":
+ return Zoom;
+ case name === "zoom":
+ return Zoom;
+ case name === "google chrome":
+ return ChromeApp;
+ default:
+ return null;
+ }
+};
+
+/**
+ * This returns the icon component for a given software name and source. If a strict match is found,
+ * it will be returned, otherwise it will fall back to loose matching on name and source prefixes.
+ * If no match is found, the default package icon will be returned.
+ */
+const getMatchedSoftwareIcon = ({
+ name = "",
+ source = "",
+}: Pick) => {
+ // first, try strict matching on name and source
+ let Icon = matchStrictNameSourceToIcon({
+ name,
+ source,
+ });
+
+ // if no match, try loose matching on name prefixes
+ if (!Icon) {
+ const matchedName = matchLoosePrefixToKey(SOFTWARE_NAME_TO_ICON_MAP, name);
+ if (matchedName) {
+ Icon = SOFTWARE_NAME_TO_ICON_MAP[matchedName];
+ }
+ }
+
+ // if still no match, try loose matching on source prefixes
+ if (!Icon) {
+ const matchedSource = matchLoosePrefixToKey(
+ SOFTWARE_SOURCE_TO_ICON_MAP,
+ source
+ );
+ if (matchedSource) {
+ Icon = SOFTWARE_SOURCE_TO_ICON_MAP[matchedSource];
+ }
+ }
+
+ // if still no match, default to 'package'
+ if (!Icon) {
+ Icon = SOFTWARE_SOURCE_TO_ICON_MAP.package;
+ }
+
+ return Icon;
+};
+
+export default getMatchedSoftwareIcon;
diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx
index a91acb0771..89760f8cc3 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/AppleAutomaticEnrollmentPage/AppleAutomaticEnrollmentPage.tsx
@@ -189,7 +189,7 @@ const AppleAutomaticEnrollmentPage = ({
/>
If your organization doesn’t have an account, select{" "}
- Enroll now .
+ Sign up now .
diff --git a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
index 019d5f192e..63573337d7 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx
@@ -375,7 +375,7 @@ const Calendars = (): JSX.Element => {
For the OAuth scopes, paste the following value:
{
page.
>
}
- helpText="Only policy queries (SQL) are sent to the LLM. Fleet doesn’t use this data to train models."
+ helpText="If enabled, only policy queries (SQL) are sent to the LLM. Fleet doesn’t use this data to train models."
>
Disable generative AI features
diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
index eae662cee6..cf87b9f6b9 100644
--- a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
@@ -412,7 +412,7 @@ const UserForm = ({
onChange={onInputChange("email")}
placeholder="Email"
value={formData.email || ""}
- disabled={!isNewUser && !(smtpConfigured || sesConfigured)}
+ readOnly={!isNewUser && !(smtpConfigured || sesConfigured)}
tooltip={
<>
Editing an email address requires that SMTP or SES is configured in
@@ -450,7 +450,7 @@ const UserForm = ({
name="sso_enabled"
onChange={onCheckboxChange("sso_enabled")}
value={formData.sso_enabled}
- disabled={!canUseSso}
+ readOnly={!canUseSso}
wrapperClassName={`${baseClass}__invite-admin`}
helpText={
canUseSso ? (
@@ -473,11 +473,7 @@ const UserForm = ({
)
}
>
-
- Enable single sign-on
-
+ Enable single sign-on
)}
diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss
index a2a2a41541..386ea703ca 100644
--- a/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss
+++ b/frontend/pages/admin/UserManagementPage/components/UserForm/_styles.scss
@@ -3,20 +3,12 @@
.fleet-checkbox {
margin-top: 5px;
- &__tick--disabled {
- @include disabled;
- }
-
&__label {
font-size: $x-small;
font-weight: $bold;
color: $core-fleet-black;
}
}
-
- &--disabled {
- @include disabled;
- }
}
.sso-disabled {
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index 3cf61f1a66..0c794a7cea 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -77,6 +77,7 @@ import Dropdown from "components/forms/fields/Dropdown";
import TableContainer from "components/TableContainer";
import InfoBanner from "components/InfoBanner/InfoBanner";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import TableDataError from "components/DataError";
import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton/ActionButton";
import TeamsDropdown from "components/TeamsDropdown";
@@ -1403,18 +1404,10 @@ const ManageHostsPage = ({
};
const renderHostCount = useCallback(() => {
- const count = hostsCount;
-
return (
-
- {count !== undefined && (
-
{`${count} host${count === 1 ? "" : "s"}`}
- )}
- {!!count && (
+ <>
+
+ {!!hostsCount && (
)}
-
+ >
);
}, [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 (
- 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 .
- 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 .
+ >
+ )}
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 (
{
- togglePolicyDetails(cellProps.row.original);
- }}
+ onClick={onClickPolicyName}
variant="text-icon"
>
{name}
@@ -80,9 +89,15 @@ const generatePolicyTableHeaders = (
},
{
title: "Status",
- Header: "Status",
+ Header: (cellProps) => (
+
+ ),
+ disableSortBy: false,
+ sortType: "caseInsensitive",
accessor: "response",
- disableSortBy: true,
Cell: (cellProps) => {
if (cellProps.row.original.response === "") {
return <>{DEFAULT_EMPTY_CELL_VALUE}>;
@@ -114,6 +129,7 @@ const generatePolicyTableHeaders = (
cellProps.row.original.response === "pass"
? PolicyResponse.PASSING
: PolicyResponse.FAILING,
+ team_id: currentTeamId,
}}
className="policy-link"
/>
diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx
index 0e2cc324c9..518f699490 100644
--- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx
+++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx
@@ -37,6 +37,7 @@ export interface ITableSoftware extends Omit {
interface IHostSoftwareProps {
/** This is the host id or the device token */
id: number | string;
+ softwareUpdatedAt?: string;
isFleetdHost: boolean;
router: InjectedRouter;
queryParams: ReturnType;
@@ -79,6 +80,7 @@ export const parseHostSoftwareQueryParams = (queryParams: {
const HostSoftware = ({
id,
+ softwareUpdatedAt,
isFleetdHost,
router,
queryParams,
@@ -119,6 +121,7 @@ const HostSoftware = ({
{
scope: "host_software",
id: id as number,
+ softwareUpdatedAt,
...queryParams,
},
],
@@ -149,6 +152,7 @@ const HostSoftware = ({
{
scope: "device_software",
id: id as string,
+ softwareUpdatedAt,
...queryParams,
},
],
diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx
index 91a504ba66..05aefbcda9 100644
--- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx
+++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx
@@ -7,8 +7,10 @@ import { getNextLocationPath } from "utilities/helpers";
import TableContainer from "components/TableContainer";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import { generateResultsCountText } from "components/TableContainer/utilities/TableContainerUtils";
import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable";
+import TableCount from "components/TableContainer/TableCount";
const DEFAULT_PAGE_SIZE = 20;
@@ -26,16 +28,6 @@ interface IHostSoftwareTableProps {
pagePath: string;
}
-const SoftwareCount = ({ count }: { count: number }) => {
- return (
-
-
- {count === 1 ? `${count} software item` : `${count} software items`}
-
-
- );
-};
-
const HostSoftwareTable = ({
tableConfig,
data,
@@ -106,20 +98,25 @@ const HostSoftwareTable = ({
[determineQueryParamChange, pagePath, generateNewQueryParams, router]
);
+ const count = data?.count || data?.software.length || 0;
+ const isSoftwareNotDetected = count === 0 && searchQuery === "";
+
const memoizedSoftwareCount = useCallback(() => {
- const count = data?.count || data?.software.length || 0;
- return ;
+ if (isSoftwareNotDetected) {
+ return null;
+ }
+
+ return ;
}, [data?.count, data?.software.length]);
const memoizedEmptyComponent = useCallback(() => {
- return ;
+ return ;
}, [searchQuery]);
return (
diff --git a/frontend/pages/hosts/details/cards/Users/Users.tsx b/frontend/pages/hosts/details/cards/Users/Users.tsx
index ed7c01ad56..1feaf6920f 100644
--- a/frontend/pages/hosts/details/cards/Users/Users.tsx
+++ b/frontend/pages/hosts/details/cards/Users/Users.tsx
@@ -1,8 +1,9 @@
-import React from "react";
+import React, { useCallback } from "react";
import { IHostUser } from "interfaces/host_users";
import TableContainer from "components/TableContainer";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import Card from "components/Card";
@@ -28,6 +29,10 @@ const Users = ({
}: IUsersProps): JSX.Element => {
const tableHeaders = generateUsersTableHeaders();
+ const renderUsersCount = useCallback(() => {
+ return ;
+ }, [usersState.length]);
+
if (!hostUsersEnabled) {
return (
(
) : (
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index ba2b5a2eac..bc0d9765b7 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -34,6 +34,7 @@ import teamPoliciesAPI, {
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import { ITableQueryData } from "components/TableContainer/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import Button from "components/buttons/Button";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
@@ -624,19 +625,21 @@ const ManagePolicyPage = ({
}
const renderPoliciesCount = (count?: number) => {
- // Show count if there is no errors AND there are policy results or a search filter
- const showCount =
- count !== undefined &&
- !policiesErrors &&
- (policyResults || searchQuery !== "");
+ // Hide count if fetching count || there are errors OR there are no policy results with no a search filter
+ const isFetchingCount = isAnyTeamSelected
+ ? isFetchingTeamCountMergeInherited
+ : isFetchingGlobalCount;
- return (
-
- {showCount && (
- {`${count} polic${count === 1 ? "y" : "ies"}`}
- )}
-
- );
+ const hideCount =
+ isFetchingCount ||
+ policiesErrors ||
+ (!policyResults && searchQuery === "");
+
+ if (hideCount) {
+ return null;
+ }
+
+ return ;
};
const renderMainTable = () => {
@@ -658,9 +661,7 @@ const ManagePolicyPage = ({
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
renderPoliciesCount={() =>
- (!isFetchingTeamCountMergeInherited &&
- renderPoliciesCount(teamPoliciesCountMergeInherited)) ||
- null
+ renderPoliciesCount(teamPoliciesCountMergeInherited)
}
isPremiumTier={isPremiumTier}
searchQuery={searchQuery}
@@ -683,11 +684,7 @@ const ManagePolicyPage = ({
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
- renderPoliciesCount={() =>
- (!isFetchingGlobalCount &&
- renderPoliciesCount(globalPoliciesCount)) ||
- null
- }
+ renderPoliciesCount={() => renderPoliciesCount(globalPoliciesCount)}
searchQuery={searchQuery}
sortHeader={sortHeader}
sortDirection={sortDirection}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
index 3d395c64c2..f9a6d9c9bb 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
@@ -17,7 +17,7 @@ import { IPolicyStats } from "interfaces/policy";
import PATHS from "router/paths";
import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
-import { buildQueryStringFromParams } from "utilities/url";
+import { createHostsByPolicyPath } from "utilities/helpers";
import InheritedBadge from "components/InheritedBadge";
import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils";
import PassingColumnHeader from "../PassingColumnHeader";
@@ -60,18 +60,6 @@ interface IDataColumn {
sortType?: string;
}
-const createHostsByPolicyPath = (
- policyId: number,
- policyResponse: PolicyResponse,
- teamId?: number | null
-) => {
- return `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({
- policy_id: policyId,
- policy_response: policyResponse,
- team_id: teamId,
- })}`;
-};
-
const getPolicyRefreshTime = (ms: number): string => {
const seconds = ms / 1000;
if (seconds < 60) {
diff --git a/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss
index 045ce4b631..87e8aba720 100644
--- a/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss
+++ b/frontend/pages/policies/PolicyPage/components/PolicyResults/_styles.scss
@@ -3,16 +3,6 @@
margin: 2rem auto 1.25rem;
}
- &__export-btn {
- img {
- width: 13px;
- height: 13px;
- margin-left: 8px;
- position: relative;
- bottom: 2px;
- }
- }
-
.data-table__wrapper {
overflow-x: scroll;
}
diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx
index d098dec5cb..b8b850673f 100644
--- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx
+++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx
@@ -43,7 +43,7 @@ interface IManageQueriesPageProps {
location: {
pathname: string;
query: {
- platform?: string;
+ platform?: SupportedPlatform;
page?: string;
query?: string;
order_key?: string;
diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx
index f25578beec..dc1b754d8e 100644
--- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx
+++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx
@@ -1,16 +1,25 @@
/* eslint-disable react/prop-types */
-import React, { useContext, useCallback, useMemo } from "react";
+import React, {
+ useContext,
+ useCallback,
+ useMemo,
+ useState,
+ useEffect,
+} from "react";
import { InjectedRouter } from "react-router";
import { AppContext } from "context/app";
import { IEmptyTableProps } from "interfaces/empty_table";
+import { SupportedPlatform } from "interfaces/platform";
import { IEnhancedQuery } from "interfaces/schedulable_query";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton/ActionButton";
import PATHS from "router/paths";
import { getNextLocationPath } from "utilities/helpers";
+import { checkPlatformCompatibility } from "utilities/sql_tools";
import Button from "components/buttons/Button";
import TableContainer from "components/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
import CustomLink from "components/CustomLink";
import EmptyTable from "components/EmptyTable";
// @ts-ignore
@@ -29,7 +38,7 @@ export interface IQueriesTableProps {
isAnyTeamObserverPlus: boolean;
router?: InjectedRouter;
queryParams?: {
- platform?: string;
+ platform?: SupportedPlatform;
page?: string;
query?: string;
order_key?: string;
@@ -92,6 +101,38 @@ const QueriesTable = ({
}: IQueriesTableProps): JSX.Element | null => {
const { currentUser } = useContext(AppContext);
+ // Client side filtering bugs fixed with bypassing TableContainer filters
+ // queriesState tracks search filter and compatible platform filter
+ // to correctly show filtered queries and filtered count
+ // isQueryStateLoading prevents flashing of unfiltered count during clientside filtering
+ const [queriesState, setQueriesState] = useState([]);
+ const [isQueriesStateLoading, setIsQueriesStateLoading] = useState(true);
+
+ useEffect(() => {
+ setIsQueriesStateLoading(true);
+ if (queriesList) {
+ setQueriesState(
+ queriesList.filter((query) => {
+ const filterSearchQuery = queryParams?.query
+ ? query.name
+ .toLowerCase()
+ .includes(queryParams?.query.toLowerCase())
+ : true;
+
+ const compatiblePlatforms =
+ checkPlatformCompatibility(query.query).platforms || [];
+
+ const filterCompatiblePlatform = queryParams?.platform
+ ? compatiblePlatforms.includes(queryParams?.platform)
+ : true;
+
+ return filterSearchQuery && filterCompatiblePlatform;
+ }) || []
+ );
+ }
+ setIsQueriesStateLoading(false);
+ }, [queriesList, queryParams?.query]);
+
// Functions to avoid race conditions
const initialSearchQuery = (() => queryParams?.query ?? "")();
const initialSortHeader = (() =>
@@ -236,6 +277,15 @@ const QueriesTable = ({
);
}, [platform, queryParams, router]);
+ const renderQueriesCount = useCallback(() => {
+ // Fixes flashing incorrect count before clientside filtering
+ if (isQueriesStateLoading) {
+ return null;
+ }
+
+ return ;
+ }, [queriesState, isQueriesStateLoading]);
+
const columnConfigs = useMemo(
() =>
currentUser &&
@@ -281,14 +331,15 @@ const QueriesTable = ({
} as IActionButtonProps),
[onDeleteQueryClick]
);
+
return columnConfigs && !isLoading ? (
) : (
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
index 712ae3a0f5..f7177bf718 100644
--- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
@@ -42,7 +42,6 @@ import NoResults from "../components/NoResults/NoResults";
import {
DEFAULT_SORT_HEADER,
DEFAULT_SORT_DIRECTION,
- QUERY_REPORT_RESULTS_LIMIT,
} from "./QueryDetailsPageConfig";
interface IQueryDetailsPageProps {
@@ -199,8 +198,7 @@ const QueryDetailsPage = ({
const isLoading = isStoredQueryLoading || isQueryReportLoading;
const isApiError = storedQueryError || queryReportError;
- const isClipped =
- (queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT;
+ const isClipped = queryReport?.report_clipped;
const disabledLiveQuery = config?.server_settings.live_query_disabled;
const renderHeader = () => {
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx
index 05ef2ba604..10cc329d00 100644
--- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPageConfig.tsx
@@ -11,5 +11,3 @@ export type QueryDetailsPageQueryParams = Record<
export const DEFAULT_SORT_HEADER = "host_name";
export const DEFAULT_SORT_DIRECTION = "asc";
-
-export const QUERY_REPORT_RESULTS_LIMIT = 1000;
diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tests.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tests.tsx
index 17a04dffda..93492b3556 100644
--- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tests.tsx
+++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tests.tsx
@@ -24,6 +24,7 @@ describe("QueryReport", () => {
columns: { col1: "value3", col2: "value4" },
},
],
+ report_clipped: false,
},
];
render( );
@@ -56,6 +57,7 @@ describe("QueryReport", () => {
},
},
],
+ report_clipped: false,
},
];
render( );
@@ -83,6 +85,7 @@ describe("QueryReport", () => {
columns: { col1: "value1", col2: "value2" },
},
],
+ report_clipped: true,
},
];
render( );
diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx
index 7ccd7fb166..9bebede919 100644
--- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx
+++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx
@@ -14,6 +14,8 @@ import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report";
import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
import TableContainer from "components/TableContainer";
+import TableCount from "components/TableContainer/TableCount";
+import { generateResultsCountText } from "components/TableContainer/utilities/TableContainerUtils";
import TooltipWrapper from "components/TooltipWrapper";
import EmptyTable from "components/EmptyTable";
@@ -102,7 +104,7 @@ const QueryReport = ({
if (isClipped) {
return (
-
+ <>
@@ -115,16 +117,13 @@ const QueryReport = ({
>
}
>
- {`${count} result${count === 1 ? "" : "s"}`}
+ {generateResultsCountText("results", count)}
-
+ >
);
}
- return (
-
- {`${count} result${count === 1 ? "" : "s"}`}
-
- );
+
+ return ;
}, [filteredResults.length, isClipped]);
const renderTable = () => {
diff --git a/frontend/pages/queries/edit/components/QueryResults/_styles.scss b/frontend/pages/queries/edit/components/QueryResults/_styles.scss
index 36ea6b4290..7b7fe41c57 100644
--- a/frontend/pages/queries/edit/components/QueryResults/_styles.scss
+++ b/frontend/pages/queries/edit/components/QueryResults/_styles.scss
@@ -8,21 +8,6 @@
margin-right: $pad-medium;
}
- &__export-btn {
- img {
- width: 13px;
- margin-left: 8px;
- position: relative;
- bottom: 2px;
- }
- }
- &__show-query-btn {
- img {
- width: 13px;
- margin-left: 8px;
- }
- }
-
.data-table__wrapper {
overflow-x: scroll;
}
diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts
index 33955a419d..11aead8495 100644
--- a/frontend/services/entities/device_user.ts
+++ b/frontend/services/entities/device_user.ts
@@ -10,6 +10,7 @@ export type ILoadHostDetailsExtension = "device_mapping" | "macadmins";
export interface IDeviceSoftwareQueryKey extends IHostSoftwareQueryParams {
scope: "device_software";
id: string;
+ softwareUpdatedAt?: string;
}
export interface IGetDeviceSoftwareResponse {
diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts
index ac1f71673f..259d9ae9a2 100644
--- a/frontend/services/entities/hosts.ts
+++ b/frontend/services/entities/hosts.ts
@@ -169,6 +169,7 @@ export interface IHostSoftwareQueryParams extends QueryParams {
export interface IHostSoftwareQueryKey extends IHostSoftwareQueryParams {
scope: "host_software";
id: number;
+ softwareUpdatedAt?: string;
}
export type ILoadHostDetailsExtension = "device_mapping" | "macadmins";
@@ -439,7 +440,7 @@ export default {
},
loadHostDetails: (hostID: number) => {
const { HOSTS } = endpoints;
- const path = `${HOSTS}/${hostID}`;
+ const path = `${HOSTS}/${hostID}?exclude_software=true`;
return sendRequest("GET", path);
},
diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts
index 3b4067dd7d..372b8b5268 100644
--- a/frontend/services/entities/software.ts
+++ b/frontend/services/entities/software.ts
@@ -191,7 +191,11 @@ export default {
return sendRequest("GET", path);
},
- addSoftwarePackage: (data: IAddSoftwareFormData, teamId?: number) => {
+ addSoftwarePackage: (
+ data: IAddSoftwareFormData,
+ teamId?: number,
+ timeout?: number
+ ) => {
const { SOFTWARE_PACKAGE_ADD } = endpoints;
if (!data.software) {
@@ -208,7 +212,14 @@ export default {
formData.append("post_install_script", data.postInstallScript);
teamId && formData.append("team_id", teamId.toString());
- return sendRequest("POST", SOFTWARE_PACKAGE_ADD, formData);
+ return sendRequest(
+ "POST",
+ SOFTWARE_PACKAGE_ADD,
+ formData,
+ undefined,
+ timeout,
+ true
+ );
},
deleteSoftwarePackage: (softwareId: number, teamId: number) => {
diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx
index b84f8c3fa7..f050ef02c4 100644
--- a/frontend/utilities/helpers.tsx
+++ b/frontend/utilities/helpers.tsx
@@ -19,7 +19,6 @@ import {
intlFormat,
intervalToDuration,
isAfter,
- isBefore,
addDays,
} from "date-fns";
import yaml from "js-yaml";
@@ -41,6 +40,7 @@ import {
import { ITeam } from "interfaces/team";
import { UserRole } from "interfaces/user";
+import PATHS from "router/paths";
import stringUtils from "utilities/strings";
import sortUtils from "utilities/sort";
import { checkTable } from "utilities/sql_tools";
@@ -54,6 +54,7 @@ import {
INITIAL_FLEET_DATE,
PLATFORM_LABEL_DISPLAY_TYPES,
isPlatformLabelNameFromAPI,
+ PolicyResponse,
} from "utilities/constants";
import { ISchedulableQueryStats } from "interfaces/schedulable_query";
import { IDropdownOption } from "interfaces/dropdownOption";
@@ -91,6 +92,18 @@ export const addGravatarUrlToResource = (resource: any): any => {
};
};
+export const createHostsByPolicyPath = (
+ policyId: number,
+ policyResponse: PolicyResponse,
+ teamId?: number | null
+) => {
+ return `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({
+ policy_id: policyId,
+ policy_response: policyResponse,
+ team_id: teamId,
+ })}`;
+};
+
const labelSlug = (label: ILabel): string => {
const { id, name } = label;
@@ -965,6 +978,7 @@ export function getCustomDropdownOptions(
export default {
addGravatarUrlToResource,
+ createHostsByPolicyPath,
formatConfigDataForServer,
formatLabelResponse,
formatFloatAsPercentage,
diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md
index 5fb48ff42d..081cf7ffb5 100644
--- a/handbook/business-operations/README.md
+++ b/handbook/business-operations/README.md
@@ -68,14 +68,17 @@ Every quarter, payroll and tax filings are due for each state. Gusto can handle
### Inform managers about hours worked
-Every Friday at 1:00pm CT, we gather hours worked for anyone who gets paid hourly by Fleet. This includes core team members and consultants, regardless of employment classification, and regardless whether inside or outside of the United States.
+
+Every Friday at 2:00 PM CT, we collect hours worked for all hourly employees at Fleet, including core team members and consultants, regardless of their location.
Here's how:
-1. For every hourly core team member in Gusto or Plane.com, find the DRI by looking up ([who they report to](https://fleetdm.com/handbook/company#org-chart)).
-2. If any direct report is hourly in Plane.com and does not submit their hours until the end of the month, still list them, but explain.
-3. [Consultants](https://fleetdm.com/handbook/business-operations#hiring) don't have a formal reporting structure. They submit their hours through Gusto (US consultants) or Plane.com (international consultants), which require approval from a DRI. You can find the DRI by looking at the Business Operations [KPIs](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit#gid=0)
-Then, send the DRI a direct message in Slack with a screenshot of the HRIS portal, showing hours logged since last Saturday at midnight and tasks undertaken during those hours (if provided). Ensure the screenshot does not contain any compensation information.
+1. For each hourly core team member in Gusto or Plane.com, find the DRI by checking [who they report to](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0).
+ - If any direct report is hourly in Plane.com and submits hours monthly, still list them and provide an explanation.
+2. Consultants submit their hours through Gusto (US consultants) or Plane.com (international consultants) and require DRI approval. Find the DRI using the [Business Operations KPIs](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit#gid=0).
+3. Send the teammate's DRI a direct message in Slack with a screenshot of the HRIS portal, showing hours logged since last Saturday at midnight, and ask them to confirm the hours are expected. Ensure the screenshot does not include compensation information.
+4. The following Monday, check for updates to logged hours and ensure the KPI sheet aligns with HRIS records.
+ - If there are discrepancies between what was previously reported, reconfirm logged hours with the teammate's DRI and update the KPI sheet to reflect the correct amount.
### Change the DRI of a consultant
diff --git a/handbook/business-operations/security-policies.md b/handbook/business-operations/security-policies.md
index 514754eac3..501dd7d881 100644
--- a/handbook/business-operations/security-policies.md
+++ b/handbook/business-operations/security-policies.md
@@ -378,7 +378,7 @@ Fleet policy requires all workforce members to comply with the HR Security Polic
Fleet policy requires that:
-- Background verification checks on candidates for employees and contractors with production access to the Fleet infrastructure resources must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk.
+- Background verification checks on candidates for all employees and contractors must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk.
- Employees, contractors, and third-party users must agree to and sign the terms and conditions of their employment contract and comply with acceptable use.
- Employees will perform an onboarding process that familiarizes them with the environments, systems, security requirements, and procedures that Fleet already has in place. Employees will also have ongoing security awareness training that is audited.
- Employee offboarding will include reiterating any duties and responsibilities still valid after terminations, verifying that access to any Fleet systems has been removed, and ensuring that all company-owned assets are returned.
diff --git a/handbook/business-operations/vendor-questionnaires.md b/handbook/business-operations/vendor-questionnaires.md
index b171346023..8af1763870 100644
--- a/handbook/business-operations/vendor-questionnaires.md
+++ b/handbook/business-operations/vendor-questionnaires.md
@@ -67,7 +67,7 @@ Please also see [privacy](https://fleetdm.com/legal/privacy)
## Sub-processors
| Question | Answer |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Does Fleet possess an APEC PRP certification issued by a certification body (or Accountability Agent)? If not, is Fleet able to provide any evidence that the PRP requirements are being met as it relates to the Scoped Services provided to its customers? | Fleet has not undergone APEC PRP certification but has undergone an external security audit that included pen testing. For a complete list of subprocessors, please refer to https://trust.fleetdm.com/subprocessors |
+| Does Fleet possess an APEC PRP certification issued by a certification body (or Accountability Agent)? If not, is Fleet able to provide any evidence that the PRP requirements are being met as it relates to the Scoped Services provided to its customers? | Fleet has not undergone APEC PRP certification but has undergone an external security audit that included pen testing. For a complete list of subprocessors, please refer to our [trust page](https://trust.fleetdm.com/subprocessors). |
diff --git a/handbook/company/pricing-features-table.yml b/handbook/company/pricing-features-table.yml
index c0b34f8ff7..12d67f61f4 100644
--- a/handbook/company/pricing-features-table.yml
+++ b/handbook/company/pricing-features-table.yml
@@ -558,10 +558,10 @@
waysToUse:
- description: Deploy configuration profiles on macOS and Windows and verify that they're installed.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/13281
- - description: Deploy custom declaration (DDM) profiles on macOS. Coming soon (2024-03-31).
+ - description: Deploy custom declaration (DDM) profiles on macOS.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14550
- - description: Target profiles to specific hosts using SQL.
- moreInfoUrl: https://github.com/fleetdm/fleet/issues/14715
+ - description: Target profiles to specific hosts using SQL. Exclusions coming soon (2024-07-15) #customer-rosner
+ moreInfoUrl: https://github.com/fleetdm/fleet/issues/17315
- description: Automatically re-deploy configuration profiles when they're not installed.
- description: Deploy configuration profiles on iOS/iPadOS. Coming soon (2024-07-15).
productCategories: [Device management]
@@ -581,6 +581,9 @@
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
+ waysToUse:
+ - description: Support ACME as a protocol for MDM certificate generation. Coming soon (2024-12-31) #customer-rosner
+ moreInfoUrl: https://github.com/fleetdm/fleet/issues/15611
- industryName: Low-level MDM commands for macOS, iOS/iPadOS*, and Windows (e.g. remote restart)
documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-commands
tier: Free
@@ -618,7 +621,7 @@
pricingTableCategories: [Device management]
waysToUse:
- description: Enforce macOS updates via Nudge.
- - description: Progressively enhance from Nudge to DDM-based OS updates. Coming soon (2024-03-31).
+ - description: Progressively enhance from Nudge to DDM-based OS updates.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/17295
- description: Automatically update Windows after the end user reaches a deadline.
- industryName: Cross-platform remote lock and wipe
@@ -637,12 +640,11 @@
description: Easily configure and install SentinelOne, Crowdstrike, and other security tools.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14921
tier: Premium
- comingSoonOn: 2024-05-13 #customer-flacourtia
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Update apps on macOS, Windows, and Linux computers.
- description: Install Zoom, Slack, Chrome, and other apps without interrupting your end users.
+ description: Update Zoom, Slack, Chrome, and other apps without interrupting your end users.
tier: Premium
comingSoonOn: 2024-07-15
usualDepartment: IT
@@ -660,7 +662,7 @@
- industryName: License apps on macOS and iOS/iPadOS through Volume Purchasing Program (VPP).
description: Offer licenses for Photoshop and other App Sore apps for your end users.
tier: Premium
- comingSoonOn: 2024-06-30
+ comingSoonOn: 2024-07-15 #customer-rosner
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
@@ -757,6 +759,15 @@
pricingTableCategories: [Endpoint operations]
usualDepartment: IT
tier: Free
+- industryName: Two-factor authentication
+ moreInfoUrl: https://github.com/fleetdm/fleet/issues/5478
+ productCategories: [Endpoint operations,Device management,Vulnerability management]
+ pricingTableCategories: [Endpoint operations]
+ usualDepartment: IT
+ tier: Free
+ waysToUse:
+ - description: Enforce two-factor authentication when logging in to Fleet for added security.
+ comingSoonOn: 2024-12-31 #customer-rosner
- industryName: Disk encryption
documentationURL: https://fleetdm.com/docs/using-fleet/mdm-disk-encryption
friendlyName: Ensure hard disks are encrypted
@@ -789,6 +800,13 @@
pricingTableCategories: [Endpoint operations]
usualDepartment: IT
tier: Premium
+- industryName: System for Cross-domain Identity Management (SCIM) provisioning
+ moreInfoUrl: https://github.com/fleetdm/fleet/issues/15671
+ productCategories: [Endpoint operations,Device management,Vulnerability management]
+ pricingTableCategories: [Endpoint operations]
+ usualDepartment: IT
+ tier: Premium
+ comingSoonOn: 2024-12-31 #customer-rosner
- industryName: Automated user role sync via Okta, AD, or any IDP
documentationUrl:
productCategories: [Endpoint operations,Device management,Vulnerability management]
diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md
index 4084572a0c..0fd7a55057 100644
--- a/handbook/digital-experience/README.md
+++ b/handbook/digital-experience/README.md
@@ -14,7 +14,7 @@ This page details processes specific to working [with](#contact-us) and [within]
## Contact us
- To **make a request** of this department, [create an issue](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=digital-experience-request.md&title=TODO%3A+) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel.
- - Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/g-sales-64fbb46c65f9ff003a1530a8/board?sprints=none) for this department, including pending tasks and the status of new requests.
+ - Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/g-digital-experience-6451748b4eb15200131d4bab/board) for this department, including pending tasks and the status of new requests.
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/docker/.gitignore b/infrastructure/dogfood/terraform/aws-tf-module/docker/.gitignore
new file mode 100644
index 0000000000..b0bcff9fe7
--- /dev/null
+++ b/infrastructure/dogfood/terraform/aws-tf-module/docker/.gitignore
@@ -0,0 +1,2 @@
+osquery
+osquery-docker.patch
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/docker/main.tf b/infrastructure/dogfood/terraform/aws-tf-module/docker/main.tf
index 46e5038957..a2d1655ed2 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/docker/main.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/docker/main.tf
@@ -11,31 +11,65 @@ terraform {
}
}
-variable "osquery_tag" {
- description = "The osquery tag to take from dockerhub to your ecr repo."
+variable "osquery_version" {
+ description = "The osquery version to push to your ecr repo."
type = string
}
+variable "osquery_tags" {
+ description = "The tags that you wish to push among the built images"
+ type = list(string)
+}
+
variable "ecr_repo" {
description = "The ecr repo to push to"
type = string
}
-resource "docker_image" "dockerhub" {
- name = "osquery/osquery:${var.osquery_tag}"
+resource "local_file" "osquery_patch" {
+ content = templatefile("${path.module}/osquery-docker.patch.tmpl", { osquery_version = var.osquery_version })
+ filename = "${path.module}/osquery-docker.patch"
+ file_permission = "0644"
+}
+
+resource "null_resource" "build_osquery" {
+ depends_on = [local_file.osquery_patch]
+ triggers = {
+ osquery_version_changed = var.osquery_version
+ osquery_tags_changed = sha256(jsonencode(var.osquery_tags))
+ }
+ provisioner "local-exec" {
+ working_dir = "${path.module}"
+ command = <<-EOT
+ mkdir -p osquery
+ cd osquery
+ if [ "$(git remote -vvv | head -n1 | awk '{ print $2 }')" = "https://github.com/osquery/osquery.git" ]; then
+ git reset --hard
+ git pull
+ else
+ git clone https://github.com/osquery/osquery.git .
+ fi
+ git apply ../osquery-docker.patch
+ cd tools/docker
+ ./build.sh
+ EOT
+ }
}
resource "docker_tag" "osquery" {
- source_image = docker_image.dockerhub.name
+ depends_on = [null_resource.build_osquery]
+ for_each = toset(var.osquery_tags)
+ source_image = "osquery/osquery:${each.key}"
# We can't include the sha256 when pushing even if they match
- target_image = "${var.ecr_repo}:${split("@sha256", var.osquery_tag)[0]}"
+ target_image = "${var.ecr_repo}:${each.key}"
}
resource "docker_registry_image" "osquery" {
- name = docker_tag.osquery.target_image
+ for_each = toset(var.osquery_tags)
+ name = docker_tag.osquery[each.key].target_image
keep_remotely = true
}
-output "ecr_image" {
- value = docker_tag.osquery.target_image
+output "ecr_images" {
+ value = { for docker_tag in docker_tag.osquery : split(":", docker_tag.target_image)[1] => docker_tag.target_image }
}
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/docker/osquery-docker.patch.tmpl b/infrastructure/dogfood/terraform/aws-tf-module/docker/osquery-docker.patch.tmpl
new file mode 100644
index 0000000000..2ba4208e47
--- /dev/null
+++ b/infrastructure/dogfood/terraform/aws-tf-module/docker/osquery-docker.patch.tmpl
@@ -0,0 +1,28 @@
+diff --git a/tools/docker/build.sh b/tools/docker/build.sh
+index 9efba34f6..34ecd8a4e 100755
+--- a/tools/docker/build.sh
++++ b/tools/docker/build.sh
+@@ -6,7 +6,7 @@ build_deb() {
+
+ TAG=$(echo $OS | sed 's/://g')
+
+- docker build -f deb-dockerfile . --build-arg OSQUERY_URL=https://pkg.osquery.io/deb/osquery_$${VERSION}-1.linux_amd64.deb --build-arg OS_IMAGE=$OS -t osquery/osquery:$${VERSION}-$${TAG}
++ docker build --platform=linux/amd64 -f deb-dockerfile . --build-arg OSQUERY_URL=https://pkg.osquery.io/deb/osquery_$${VERSION}-1.linux_amd64.deb --build-arg OS_IMAGE=$OS -t osquery/osquery:$${VERSION}-$${TAG}
+ }
+
+ build_rpm() {
+@@ -15,11 +15,11 @@ build_rpm() {
+
+ TAG=$(echo $OS | sed 's/://g')
+
+- docker build -f rpm-dockerfile . --build-arg OSQUERY_URL=https://pkg.osquery.io/rpm/osquery-$${VERSION}-1.linux.x86_64.rpm --build-arg OS_IMAGE=$OS -t osquery/osquery:$${VERSION}-$${TAG}
++ docker build --platform=linux/amd64 -f rpm-dockerfile . --build-arg OSQUERY_URL=https://pkg.osquery.io/rpm/osquery-$${VERSION}-1.linux.x86_64.rpm --build-arg OS_IMAGE=$OS -t osquery/osquery:$${VERSION}-$${TAG}
+ }
+
+-versions='5.2.3'
+-deb_platforms='ubuntu:16.04 ubuntu:18.04 ubuntu:20.04 ubuntu:22.04 debian:10 debian:9 debian:8 debian:7'
++versions='${osquery_version}'
++deb_platforms='ubuntu:16.04 ubuntu:18.04 ubuntu:20.04 ubuntu:22.04 ubuntu:24.04 debian:10 debian:9 debian:8 debian:7'
+ rpm_platforms='centos:6 centos:7 centos:8'
+
+ for v in $versions
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/free-ecs-hosts.tf b/infrastructure/dogfood/terraform/aws-tf-module/free-ecs-hosts.tf
index 8021f5892c..0e3ebac81d 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/free-ecs-hosts.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/free-ecs-hosts.tf
@@ -1,16 +1,18 @@
## Linux hosts in ECS
locals {
+ osquery_version = "5.12.2"
osquery_hosts = {
- "5.8.2-ubuntu22.04@sha256:b77c7b06c4d7f2a3c58cc3a34e51fffc480e97795fb3c75cb1dc1cf3709e3dc6" = "Skys-laptop"
- "5.8.2-ubuntu20.04@sha256:3496ffd0ad570c88a9f405e6ef517079cfeed6ce405b9d22db4dc5ef6ed3faac" = "Cloud-City-server"
- "5.8.2-ubuntu18.04@sha256:372575e876c218dde3c5c0e24fd240d193800fca9b314e94b4ad4e6e22006c9b" = "Mists-laptop"
- "5.8.2-ubuntu16.04@sha256:112655c42951960d8858c116529fb4c64951e4cf2e34cb7c08cd599a009025bb" = "Ethers-laptop"
- "5.8.2-debian10@sha256:de29337896aac89b2b03c7642805859d3fb6d52e5dc08230f987bbab4eeba9c5" = "Breezes-laptop"
- "5.8.2-debian9@sha256:47e46c19cebdf0dc704dd0061328856bda7e1e86b8c0fefdd6f78bd092c6200e" = "Aero-server"
- "5.8.2-centos8@sha256:88a8adde80bd3b1b257e098bc6e41b6afea840f60033653dcb9fe984f36b0f97" = "Stratuss-laptop"
- "5.8.2-centos7@sha256:ff251de4935b80a91c5fc1ac352aebdab9a6bbbf5bda1aaada8e26d22b50202d" = "Zephyrs-Laptop"
- "5.8.2-centos6@sha256:b56736be8436288d3fbd2549ec6165e0588cd7197e91600de4a2f00f1df28617" = "Halo-server"
+ "${local.osquery_version}-ubuntu24.04" = "Atmosphere-database"
+ "${local.osquery_version}-ubuntu22.04" = "Skys-laptop"
+ "${local.osquery_version}-ubuntu20.04" = "Cloud-City-server"
+ "${local.osquery_version}-ubuntu18.04" = "Mists-laptop"
+ "${local.osquery_version}-ubuntu16.04" = "Ethers-laptop"
+ "${local.osquery_version}-debian10" = "Breezes-laptop"
+ "${local.osquery_version}-debian9" = "Aero-server"
+ "${local.osquery_version}-centos8" = "Stratuss-laptop"
+ "${local.osquery_version}-centos7" = "Zephyrs-Laptop"
+ "${local.osquery_version}-centos6" = "Halo-server"
}
}
@@ -123,10 +125,10 @@ provider "docker" {
}
module "osquery_docker" {
- for_each = local.osquery_hosts
- source = "./docker"
- ecr_repo = aws_ecr_repository.osquery.repository_url
- osquery_tag = each.key
+ source = "./docker"
+ ecr_repo = aws_ecr_repository.osquery.repository_url
+ osquery_version = local.osquery_version
+ osquery_tags = keys(local.osquery_hosts)
}
resource "random_uuid" "osquery" {
@@ -135,7 +137,7 @@ resource "random_uuid" "osquery" {
resource "aws_ecs_task_definition" "osquery" {
for_each = local.osquery_hosts
- // e.g. 5-8-2-ubuntu22-04 to match naming requirements
+ // e.g. ${osquery_version}-ubuntu22-04 to match naming requirements
family = "osquery-${replace(split("@sha256", each.key)[0], ".", "-")}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
@@ -147,7 +149,7 @@ resource "aws_ecs_task_definition" "osquery" {
[
{
name = "osquery"
- image = module.osquery_docker[each.key].ecr_image
+ image = module.osquery_docker.ecr_images[each.key]
cpu = 256
memory = 512
mountPoints = []
@@ -215,8 +217,8 @@ resource "aws_ecs_task_definition" "osquery" {
resource "aws_ecs_service" "osquery" {
for_each = local.osquery_hosts
- # Name must match ^[A-Za-z-_]+$ e.g. 5-8-2-ubuntu22-04
- name = "osquery_${replace(split("@sha256", each.key)[0], ".", "-")}"
+ # Name must match ^[A-Za-z-_]+$ e.g. 5.12.2-ubuntu22-04
+ name = "osquery_${replace(each.key, ".", "-")}"
launch_type = "FARGATE"
cluster = module.free.byo-db.byo-ecs.service.cluster
task_definition = aws_ecs_task_definition.osquery[each.key].arn
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/main.tf b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
index f8a055fff2..4256d37d25 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/main.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
@@ -40,6 +40,7 @@ variable "fleet_calendar_periodicity" {
default = "30s"
description = "The refresh period for the calendar integration."
}
+variable "dogfood_sidecar_enroll_secret" {}
data "aws_caller_identity" "current" {}
@@ -68,7 +69,7 @@ locals {
}
module "main" {
- source = "github.com/fleetdm/fleet//terraform?ref=tf-mod-root-v1.8.0"
+ source = "github.com/fleetdm/fleet//terraform?ref=tf-mod-root-v1.9.1"
certificate_arn = module.acm.acm_certificate_arn
vpc = {
name = local.customer
@@ -97,10 +98,13 @@ module "main" {
cluster_name = local.customer
}
fleet_config = {
- image = local.geolite2_image
- family = local.customer
- cpu = 1024
- mem = 4096
+ image = local.geolite2_image
+ family = local.customer
+ task_cpu = 1024
+ task_mem = 4096
+ cpu = 1024
+ mem = 4096
+ pid_mode = "task"
autoscaling = {
min_capacity = 2
max_capacity = 5
@@ -120,7 +124,7 @@ module "main" {
}
}
extra_iam_policies = concat(module.firehose-logging.fleet_extra_iam_policies, module.osquery-carve.fleet_extra_iam_policies, module.ses.fleet_extra_iam_policies)
- extra_execution_iam_policies = concat(module.mdm.extra_execution_iam_policies, [aws_iam_policy.sentry.arn]) #, module.saml_auth_proxy.fleet_extra_execution_policies)
+ extra_execution_iam_policies = concat(module.mdm.extra_execution_iam_policies, [aws_iam_policy.sentry.arn, aws_iam_policy.osquery_sidecar.arn]) #, module.saml_auth_proxy.fleet_extra_execution_policies)
extra_environment_variables = merge(
module.mdm.extra_environment_variables,
module.firehose-logging.fleet_extra_environment_variables,
@@ -137,6 +141,71 @@ module "main" {
# container_name = "fleet"
# container_port = 8080
# }]
+ software_installers = {
+ bucket_prefix = "${local.customer}-software-installers-"
+ }
+ # sidecars = [
+ # {
+ # name = "osquery"
+ # image = module.osquery_docker.ecr_images["${local.osquery_version}-ubuntu24.04"]
+ # cpu = 1024
+ # memory = 1024
+ # mountPoints = []
+ # volumesFrom = []
+ # essential = true
+ # ulimits = [
+ # {
+ # softLimit = 999999,
+ # hardLimit = 999999,
+ # name = "nofile"
+ # }
+ # ]
+ # networkMode = "awsvpc"
+ # logConfiguration = {
+ # logDriver = "awslogs"
+ # options = {
+ # awslogs-group = local.customer
+ # awslogs-region = "us-east-2"
+ # awslogs-stream-prefix = "osquery"
+ # }
+ # }
+ # secrets = [
+ # {
+ # name = "ENROLL_SECRET"
+ # valueFrom = aws_secretsmanager_secret.dogfood_sidecar_enroll_secret.arn
+ # }
+ # ]
+ # workingDirectory = "/",
+ # command = [
+ # "osqueryd",
+ # "--tls_hostname=dogfood.fleetdm.com",
+ # "--force=true",
+ # # Ensure that the host identifier remains the same between invocations
+ # # "--host_identifier=specified",
+ # # "--specified_identifier=${random_uuid.osquery[each.key].result}",
+ # "--verbose=true",
+ # "--tls_dump=true",
+ # "--enroll_secret_env=ENROLL_SECRET",
+ # "--enroll_tls_endpoint=/api/osquery/enroll",
+ # "--config_plugin=tls",
+ # "--config_tls_endpoint=/api/osquery/config",
+ # "--config_refresh=10",
+ # "--disable_distributed=false",
+ # "--distributed_plugin=tls",
+ # "--distributed_interval=10",
+ # "--distributed_tls_max_attempts=3",
+ # "--distributed_tls_read_endpoint=/api/osquery/distributed/read",
+ # "--distributed_tls_write_endpoint=/api/osquery/distributed/write",
+ # "--logger_plugin=tls",
+ # "--logger_tls_endpoint=/api/osquery/log",
+ # "--logger_tls_period=10",
+ # "--disable_carver=false",
+ # "--carver_start_endpoint=/api/osquery/carve/begin",
+ # "--carver_continue_endpoint=/api/osquery/carve/block",
+ # "--carver_block_size=8000000",
+ # ]
+ # }
+ # ]
}
alb_config = {
name = local.customer
@@ -293,10 +362,9 @@ module "firehose-logging" {
}
module "osquery-carve" {
- source = "github.com/fleetdm/fleet//terraform/addons/osquery-carve?ref=tf-mod-addon-osquery-carve-v1.0.1"
+ source = "github.com/fleetdm/fleet//terraform/addons/osquery-carve?ref=tf-mod-addon-osquery-carve-v1.1.0"
osquery_carve_s3_bucket = {
name = "fleet-${local.customer}-osquery-carve"
- expires_days = 3650
}
}
@@ -455,7 +523,7 @@ module "geolite2" {
}
module "vuln-processing" {
- source = "github.com/fleetdm/fleet//terraform/addons/external-vuln-scans?ref=tf-mod-addon-external-vuln-scans-v2.1.0"
+ source = "github.com/fleetdm/fleet//terraform/addons/external-vuln-scans?ref=tf-mod-addon-external-vuln-scans-v2.2.0"
ecs_cluster = module.main.byo-vpc.byo-db.byo-ecs.service.cluster
execution_iam_role_arn = module.main.byo-vpc.byo-db.byo-ecs.execution_iam_role_arn
subnets = module.main.byo-vpc.byo-db.byo-ecs.service.network_configuration[0].subnets
@@ -468,4 +536,49 @@ module "vuln-processing" {
region = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.region
prefix = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.prefix
}
+ fleet_s3_software_installers_config = module.main.byo-vpc.byo-db.byo-ecs.fleet_s3_software_installers_config
+}
+
+resource "aws_secretsmanager_secret" "dogfood_sidecar_enroll_secret" {
+ name = "dogfood-sidecar-enroll-secret"
+}
+
+resource "aws_secretsmanager_secret_version" "dogfood_sidecar_enroll_secret" {
+ secret_id = aws_secretsmanager_secret.dogfood_sidecar_enroll_secret.id
+ secret_string = var.dogfood_sidecar_enroll_secret
+}
+
+data "aws_iam_policy_document" "osquery_sidecar" {
+ statement {
+ actions = [
+ "ecr:BatchCheckLayerAvailability",
+ "ecr:BatchGetImage",
+ "ecr:GetDownloadUrlForLayer",
+ "ecr:GetAuthorizationToken"
+ ]
+ resources = ["*"]
+ }
+ statement {
+ actions = [ #tfsec:ignore:aws-iam-no-policy-wildcards
+ "kms:Encrypt*",
+ "kms:Decrypt*",
+ "kms:ReEncrypt*",
+ "kms:GenerateDataKey*",
+ "kms:Describe*"
+ ]
+ resources = [aws_kms_key.osquery.arn]
+ }
+ statement {
+ actions = [ #tfsec:ignore:aws-iam-no-policy-wildcards
+ "secretsmanager:GetSecretValue"
+ ]
+ resources = [aws_secretsmanager_secret.dogfood_sidecar_enroll_secret.arn]
+
+ }
+}
+
+resource "aws_iam_policy" "osquery_sidecar" {
+ name = "osquery-sidecar-policy"
+ description = "IAM policy that Osquery sidecar containers use to define access to AWS resources"
+ policy = data.aws_iam_policy_document.osquery_sidecar.json
}
diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf
index 9e267bcf97..f02ec39fa9 100644
--- a/infrastructure/dogfood/terraform/aws/variables.tf
+++ b/infrastructure/dogfood/terraform/aws/variables.tf
@@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
- default = "fleetdm/fleet:v4.51.0"
+ default = "fleetdm/fleet:v4.51.1"
}
variable "software_inventory" {
diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf
index 4da8bf454b..ab2e8ee55e 100644
--- a/infrastructure/dogfood/terraform/gcp/variables.tf
+++ b/infrastructure/dogfood/terraform/gcp/variables.tf
@@ -68,5 +68,5 @@ variable "redis_mem" {
}
variable "image" {
- default = "fleet:v4.51.0"
+ default = "fleet:v4.51.1"
}
diff --git a/infrastructure/render/README.md b/infrastructure/render/README.md
index 322231d5bb..23bd1618d0 100644
--- a/infrastructure/render/README.md
+++ b/infrastructure/render/README.md
@@ -39,4 +39,5 @@ Click the deploy on render button or import the blueprint from the Render servic
### Post-Deployment
-Navigate to the generated URL and run through the initial setup.
+Navigate to the generated URL and run through the initial setup. If you have a license key you can add it post-deploy as
+an environment variable `FLEET_LICENSE_KEY=value` in the Fleet service configuration.
diff --git a/it-and-security/lib/macos-cis.policies.yml b/it-and-security/lib/macos-cis.policies.yml
new file mode 100644
index 0000000000..71cd37efb8
--- /dev/null
+++ b/it-and-security/lib/macos-cis.policies.yml
@@ -0,0 +1,105 @@
+- name: CIS - Ensure Auto Update Is Enabled (MDM Required)
+ critical: false
+ platform: darwin
+ description: Checks that the system is configured via MDM to automatically install updates.
+ resolution: "Ask your system administrator to deploy an MDM profile that enables automatic updates."
+ query: |
+ SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticCheckEnabled' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticCheckEnabled' AND
+ (value != 1 AND value != 'true')
+ );
+- name: CIS - Ensure Download New Updates When Available Is Enabled (MDM Required)
+ critical: false
+ platform: darwin
+ description: Checks that the system is configured via MDM to automatically download updates.
+ resolution: "Ask your system administrator to deploy an MDM profile that enables automatic update downloads."
+ query: |
+ SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticDownload' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticDownload' AND
+ (value != 1 AND value != 'true')
+ );
+- name: CIS - Ensure Install of macOS Updates Is Enabled (MDM Required)
+ critical: false
+ platform: darwin
+ description: Ensure that macOS updates are installed after they are available from Apple.
+ resolution: "Ask your system administrator to deploy an MDM profile that enables automatic install of macOS updates."
+ query: |
+ SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticallyInstallMacOSUpdates' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticallyInstallMacOSUpdates' AND
+ (value != 1 AND value != 'true')
+ );
+- name: CIS - Ensure Install Application Updates from the App Store Is Enabled (MDM Required)
+ critical: false
+ platform: darwin
+ description: Ensure that application updates are installed after they are available from Apple.
+ resolution: Ask your system administrator to deploy an MDM profile that enables automatic updates of Apple apps.
+ query: |
+ SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticallyInstallAppUpdates' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='AutomaticallyInstallAppUpdates' AND
+ (value != 1 AND value != 'true')
+ );
+
+- name: CIS - Ensure Install Security Responses and System Files Is Enabled (MDM Required)
+ critical: false
+ platform: darwin
+ description: |
+ Ensure that system and security updates are installed after they are available from
+ Apple. This setting enables definition updates for XProtect and Gatekeeper. With this
+ setting in place, new malware and adware that Apple has added to the list of malware or
+ untrusted software will not execute.
+ resolution: "Ask your system administrator to deploy an MDM profile that enables automatic critical system and security updates."
+ query: |
+ SELECT 1 WHERE
+ EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='CriticalUpdateInstall' AND
+ (value = 1 OR value = 'true') AND
+ username = ''
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM managed_policies WHERE
+ domain='com.apple.SoftwareUpdate' AND
+ name='CriticalUpdateInstall' AND
+ (value != 1 AND value != 'true')
+ );
diff --git a/it-and-security/lib/macos-system-maintenance.sh b/it-and-security/lib/macos-system-maintenance.sh
new file mode 100644
index 0000000000..d82e599222
--- /dev/null
+++ b/it-and-security/lib/macos-system-maintenance.sh
@@ -0,0 +1,84 @@
+#!/bin/zsh
+
+# Function to start System Events if it isn't running
+start_system_events() {
+ osascript -e '
+ tell application "System Events"
+ if not running then
+ launch
+ delay 2
+ end if
+ end tell'
+}
+
+# Define the URL of the new wallpaper
+new_wallpaper_url="https://fleetdm.com/images/demo/fleet-system-maintenance.png"
+
+# Define the path where the new wallpaper will be saved
+new_wallpaper_path="/tmp/fleet-system-maintenance.png"
+
+current_user=$(ls -l /dev/console | awk '{print $3}')
+
+# Download the new wallpaper
+curl -o "$new_wallpaper_path" "$new_wallpaper_url"
+
+# Check if the download was successful
+if [[ ! -f "$new_wallpaper_path" ]] || [[ ! -s "$new_wallpaper_path" ]]; then
+ echo "Failed to download the new wallpaper."
+ exit 1
+fi
+
+# Start System Events if it isn't running
+start_system_events
+
+# Get the current wallpaper
+current_wallpaper=$(osascript -e '
+tell application "System Events"
+ set currentDesktop to a reference to current desktop
+ set desktopPicture to picture of currentDesktop
+ try
+ return POSIX path of desktopPicture
+ on error
+ return desktopPicture
+ end try
+end tell
+')
+
+# Check if the current wallpaper path is valid
+if [[ -z "$current_wallpaper" ]]; then
+ echo "Failed to get the current wallpaper path."
+ exit 1
+fi
+
+echo "Current wallpaper: $current_wallpaper"
+echo "Fleet wallpaper: $new_wallpaper_path"
+
+# Function to change wallpaper using Finder
+change_wallpaper() {
+ local wallpaper_path=$1
+ osascript -e "
+ tell application \"Finder\"
+ set desktop picture to POSIX file \"$wallpaper_path\"
+ end tell"
+}
+
+# Function to check the result of the previous command
+check_result() {
+ if [[ $? -ne 0 ]]; then
+ echo "Failed to change to the new wallpaper."
+ exit 1
+ fi
+}
+
+# Set the new wallpaper
+change_wallpaper "$new_wallpaper_path"
+check_result
+
+# Wait for 30 seconds
+sleep 30
+
+# Revert to the original wallpaper
+change_wallpaper "$current_wallpaper"
+check_result
+
+echo "Wallpaper changed to $new_wallpaper_path for 30 seconds, then reverted back to $current_wallpaper"
diff --git a/it-and-security/lib/windows-cis.policies.yml b/it-and-security/lib/windows-cis.policies.yml
new file mode 100644
index 0000000000..acb8ec3f13
--- /dev/null
+++ b/it-and-security/lib/windows-cis.policies.yml
@@ -0,0 +1,58 @@
+- name: CIS - Ensure 'Enforce password history' is set to '24' or more passwords
+ critical: false
+ platform: windows
+ description: |
+ This policy check determines the number of renewed, unique passwords that have to be associated with a user account before you can reuse an old password.
+ resolution: |
+ Automatic method:
+ Ask your system administrator to establish the recommended configuration via GP, set the following UI path to 24 or more passwords:
+ 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Enforce password history'
+ query: |
+ SELECT 1 FROM security_profile_info WHERE password_history_size >= 24;
+- name: CIS - Ensure 'Maximum password age' is set to '365 or fewer days, but not 0'
+ critical: false
+ platform: windows
+ description: |
+ This policy setting defines how long a user can use their password before it expires.
+ resolution: |
+ Automatic method:
+ Ask your system administrator to establish the recommended configuration via GP, set the following UI path to 365 or fewer days, but not 0:
+ 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Maximum password age'
+ query: |
+ SELECT 1 FROM security_profile_info WHERE (maximum_password_age <= 365 AND maximum_password_age != 0);
+- name: CIS - Ensure 'Minimum password age' is set to '1 or more days'
+ critical: false
+ platform: windows
+ description: |
+ This policy setting determines the number of days that you must use a password before you can
+ change it. The range of values for this policy setting is between 1 and 999 days.
+ resolution: |
+ Automatic method:
+ Ask your system administrator to establish the recommended configuration via GP, set the following UI path to 1 or more days:
+ 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Minimum password age'
+ query: |
+ SELECT 1 FROM security_profile_info WHERE minimum_password_age >= 1;
+- name: CIS - Ensure 'Minimum password length' is set to '14 or more characters'
+ critical: false
+ platform: windows
+ description: |
+ This policy setting determines the least number of characters that make up a password for a user account.
+ resolution: |
+ Automatic method:
+ Ask your system administrator to establish the recommended configuration via GP, set the following UI path to 14 or more characters
+ 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Minimum password length'
+ query: |
+ SELECT 1 FROM security_profile_info WHERE minimum_password_length >= 14;
+- name: CIS - Ensure 'Password must meet complexity requirements' is set to 'Enabled'
+ critical: false
+ platform: windows
+ description: |
+ This policy setting checks all new passwords to ensure that they meet basic requirements for
+ strong passwords. Passwords that contain only alphanumeric characters are extremely easy to
+ discover with several publicly available tools.
+ resolution: |
+ Automatic method:
+ Ask your system administrator to establish the recommended configuration via GP, set the following UI path to 'Enabled':
+ 'Computer Configuration\Policies\Windows Settings\Security Settings\Account Policies\Password Policy\Password must meet complexity requirements'
+ query: |
+ SELECT 1 FROM security_profile_info WHERE password_complexity = 1;
diff --git a/it-and-security/lib/windows-remove-fleetd.ps1 b/it-and-security/lib/windows-remove-fleetd.ps1
index f532843c09..77a420781d 100644
--- a/it-and-security/lib/windows-remove-fleetd.ps1
+++ b/it-and-security/lib/windows-remove-fleetd.ps1
@@ -1,5 +1,5 @@
-function Test-Administrator
-{
+function Test-Administrator
+{
[OutputType([bool])]
param()
process {
@@ -58,11 +58,11 @@ function Force-Remove-Orbit {
#Remove HKLM registry entries
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse -ErrorAction "SilentlyContinue" | Where-Object {($_.ValueCount -gt 0)} | ForEach-Object {
-
+
# Filter for osquery entries
- $properties = Get-ItemProperty $_.PSPath -ErrorAction "SilentlyContinue" | Where-Object {($_.DisplayName -eq "Fleet osquery")}
+ $properties = Get-ItemProperty $_.PSPath -ErrorAction "SilentlyContinue" | Where-Object {($_.DisplayName -eq "Fleet osquery")}
if ($properties) {
-
+
#Remove Registry Entries
$regKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + $_.PSChildName
@@ -72,12 +72,12 @@ function Force-Remove-Orbit {
}
}
}
- catch {
+ catch {
Write-Host "There was a problem running Force-Remove-Orbit"
Write-Host "$(Resolve-Error-Detailed)"
return $false
}
-
+
return $true
}
@@ -92,19 +92,41 @@ function Main {
Write-Host "About to uninstall fleetd..."
- if (Force-Remove-Orbit) {
- Write-Host "fleetd was uninstalled."
- Exit 0
+ if ($mode -eq "remove") {
+ # "remove" is received as argument to the script when called as the
+ # sub-process that will actually remove the fleet agent.
+
+ # sleep to give time to fleetd to send the script results to Fleet
+ Start-Sleep -Seconds 20
+ if (Force-Remove-Orbit) {
+ Write-Host "fleetd was uninstalled."
+ Exit 0
+ } else {
+ Write-Host "There was a problem uninstalling fleetd."
+ Exit -1
+ }
} else {
- Write-Host "There was a problem uninstalling fleetd."
- Exit -1
+ # when this script is executed from fleetd, it does not immediately
+ # remove the agent. Instead, it starts a new detached process that
+ # will do the actual removal. This is done to avoid the agent being
+ # killed by the removal process, which prevents it from sending the
+ # script execution results to Fleet, causing the script to remain
+ # "Pending" and being re-executed when/if the host reinstalls the
+ # agent.
+ #
+ # See https://github.com/fleetdm/fleet/issues/19197#issuecomment-2150020270
+ $execName = $MyInvocation.ScriptName
+ $proc = Start-Process -PassThru -FilePath "powershell" -WindowStyle Hidden -ArgumentList "-MTA", "-ExecutionPolicy", "Bypass", "-File", "$execName remove"
+ Start-Sleep -Seconds 5 # give time to process to start running
+ Write-Host "Removal process started: $($proc.Id)."
}
} catch {
- Write-Host "Errorr: Entry point"
+ Write-Host "Errorr: Entry point"
Write-Host "$(Resolve-Error-Detailed)"
Exit -1
}
}
+$mode = $args[0]
$null = Main
diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml
index 63b5b07c3d..e151b6c08d 100644
--- a/it-and-security/teams/workstations-canary.yml
+++ b/it-and-security/teams/workstations-canary.yml
@@ -105,6 +105,7 @@ controls:
- path: ../lib/macos-see-automatic-enrollment-profile.sh
- path: ../lib/macos-remove-old-nudge.sh
- path: ../lib/macos-mdm-migration.sh
+ - path: ../lib/macos-system-maintenance.sh
- path: ../lib/windows-remove-fleetd.ps1
- path: ../lib/windows-turn-off-mdm.ps1
- path: ../lib/windows-install-bitdefender.ps1
@@ -121,11 +122,18 @@ policies:
platform: darwin
calendar_events_enabled: false
- name: macOS - MDM migration complete
- query: SELECT 1 AS result FROM system_info WHERE computer_name NOT IN ('Titanosauria', 'Drew’s MacBook Pro','fleetwoodmike');
+ query: SELECT 1 AS result FROM system_info WHERE computer_name NOT IN ('Titanosauria2', 'Drew’s MacBook Pro2','fleetwoodmike2');
critical: false
description: Determines if the device has completed MDM migration to Fleet.
resolution: We will migrate your macOS MDM to Fleet.
platform: darwin
+ calendar_events_enabled: false
+ - name: macOS - System maintenance complete
+ query: SELECT 1 AS result FROM system_info WHERE computer_name NOT IN ('Titanosauria', 'Drew’s MacBook Pro','fleetwoodmike');
+ critical: false
+ description: Determines if the device has completed system maintenance.
+ resolution: We will perform system maintenance on your device.
+ platform: darwin
calendar_events_enabled: true
queries:
- path: ../lib/collect-failed-login-attempts.queries.yml
diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml
index 6e28a28f6f..ee8d50e96c 100644
--- a/it-and-security/teams/workstations.yml
+++ b/it-and-security/teams/workstations.yml
@@ -61,8 +61,10 @@ policies:
- path: ../lib/macos-device-health.policies.yml
- path: ../lib/windows-device-health.policies.yml
- path: ../lib/linux-device-health.policies.yml
+ - path: ../lib/macos-cis.policies.yml
+ - path: ../lib/windows-cis.policies.yml
- name: macOS - Check if latest version
- query: SELECT 1 FROM os_version WHERE major = '14' AND minor = '5';
+ query: SELECT 1 FROM os_version WHERE major = '14' OR major = '15';
critical: false
description: Using an outdated macOS version risks exposure to security vulnerabilities and potential system instability.
resolution: We will update your macOS to the latest version.
diff --git a/orbit/CHANGELOG.md b/orbit/CHANGELOG.md
index a4427bd7c2..785fd97aa6 100644
--- a/orbit/CHANGELOG.md
+++ b/orbit/CHANGELOG.md
@@ -1,3 +1,19 @@
+## Orbit 1.26.0 (Jun 11, 2024)
+
+* Added `tcc_access` table to `fleetd` for macOS.
+
+* Fixed fleetd agent to identify HTTP calls from the SOFA macOS tables.
+
+* Fixed Orbit to ignore-and-log osquery errors when it gets valid host info from osquery at startup.
+
+* Added `fleetd_logs` table
+
+* Fixed scripts that were blocking execution of other scripts after timing out on Windows.
+
+* Added the `Self-service` menu item to Fleet Desktop.
+
+* Updated Go version to go1.22.3
+
## Orbit 1.25.0 (May 22, 2024)
* Added code to detect value of `DISPLAY` variable of user instead of defaulting to `:0` (to support Ubuntu 24.04 with Xorg).
diff --git a/orbit/TUF.md b/orbit/TUF.md
index 0b78db8e89..937878c739 100644
--- a/orbit/TUF.md
+++ b/orbit/TUF.md
@@ -7,8 +7,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
| Component\OS | macOS | Linux | Windows |
|--------------|--------------|--------|---------|
-| orbit | 1.25.0 | 1.25.0 | 1.25.0 |
-| desktop | 1.25.0 | 1.25.0 | 1.25.0 |
+| orbit | 1.26.0 | 1.26.0 | 1.26.0 |
+| desktop | 1.26.0 | 1.26.0 | 1.26.0 |
| osqueryd | 5.12.1 | 5.12.1 | 5.12.1 |
| nudge | 1.1.10.81462 | - | - |
| swiftDialog | 2.1.0 | - | - |
@@ -17,8 +17,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
| Component\OS | macOS | Linux | Windows |
|--------------|--------|--------|---------|
-| orbit | 1.25.0 | 1.25.0 | 1.25.0 |
-| desktop | 1.25.0 | 1.25.0 | 1.25.0 |
+| orbit | 1.26.0 | 1.26.0 | 1.26.0 |
+| desktop | 1.26.0 | 1.26.0 | 1.26.0 |
| osqueryd | 5.12.2 | 5.12.2 | 5.12.2 |
| nudge | - | - | - |
| swiftDialog | - | - | - |
diff --git a/orbit/changes/16795-update-go b/orbit/changes/16795-update-go
deleted file mode 100644
index d4684530a3..0000000000
--- a/orbit/changes/16795-update-go
+++ /dev/null
@@ -1 +0,0 @@
-* Update Go version to go1.22.3
diff --git a/orbit/changes/17934-mac-overwrite b/orbit/changes/17934-mac-overwrite
new file mode 100644
index 0000000000..94b52f0a09
--- /dev/null
+++ b/orbit/changes/17934-mac-overwrite
@@ -0,0 +1,3 @@
+- Fixes an issue related to hardware UUIDs being cached in osquery's database. When an orbit install
+ is transferred from one machine to another (e.g. via MacOS Migration Assistant), the new machine
+ now shows up in Fleet as a separate host from the old one.
\ No newline at end of file
diff --git a/orbit/changes/18222-add-tcc-table b/orbit/changes/18222-add-tcc-table
deleted file mode 100644
index 1178f6824a..0000000000
--- a/orbit/changes/18222-add-tcc-table
+++ /dev/null
@@ -1 +0,0 @@
-- Add `tcc_access` table to `fleetd` for macOS.
diff --git a/orbit/changes/18835-add-fleet-desktop-self-service b/orbit/changes/18835-add-fleet-desktop-self-service
deleted file mode 100644
index c0d54803d1..0000000000
--- a/orbit/changes/18835-add-fleet-desktop-self-service
+++ /dev/null
@@ -1 +0,0 @@
-* Added the `Self-service` menu item to Fleet Desktop.
diff --git a/orbit/changes/19218-exit-status-78 b/orbit/changes/19218-exit-status-78
deleted file mode 100644
index a8cba01f84..0000000000
--- a/orbit/changes/19218-exit-status-78
+++ /dev/null
@@ -1 +0,0 @@
-When orbit gets host info from osquery at startup, ignore and log osquery error when valid data is returned.
diff --git a/orbit/changes/19276-sofa-user-agent b/orbit/changes/19276-sofa-user-agent
deleted file mode 100644
index 9c63fce2d7..0000000000
--- a/orbit/changes/19276-sofa-user-agent
+++ /dev/null
@@ -1 +0,0 @@
-HTTP calls from the SOFA macOS tables now identified by a 'fleetd' user agent
\ No newline at end of file
diff --git a/orbit/cmd/fleetd_tables/fleetd_tables.go b/orbit/cmd/fleetd_tables/fleetd_tables.go
index df388cce9e..5c2deda5e2 100644
--- a/orbit/cmd/fleetd_tables/fleetd_tables.go
+++ b/orbit/cmd/fleetd_tables/fleetd_tables.go
@@ -54,7 +54,11 @@ func main() {
opts := orbittable.PluginOpts{
Socket: *socket,
}
- plugins = append(plugins, orbittable.PlatformTables(opts)...)
+ platformTables, err := orbittable.PlatformTables(opts)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ plugins = append(plugins, platformTables...)
server.RegisterPlugin(plugins...)
if err := server.Run(); err != nil {
log.Fatalln(err)
diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go
index b2c19fd74f..a794b877b6 100644
--- a/orbit/cmd/orbit/orbit.go
+++ b/orbit/cmd/orbit/orbit.go
@@ -31,6 +31,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
"github.com/fleetdm/fleet/v4/orbit/pkg/table"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/table/fleetd_logs"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/orbit_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/token"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
@@ -42,6 +43,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
+ "github.com/google/uuid"
"github.com/oklog/run"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -244,16 +246,24 @@ func main() {
}
if runtime.GOOS == "windows" {
// On Windows, Orbit runs as a "Windows Service", which fails to write to os.Stderr with
- // "write /dev/stderr: The handle is invalid" (see #3100). Thus, we log to the logFile only.
- log.Logger = log.Output(zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true})
+ // "write /dev/stderr: The handle is invalid" (see
+ // #3100). Thus, we log to the logFile only.
+ log.Logger = log.Output(zerolog.MultiLevelWriter(
+ zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true},
+ &fleetd_logs.Logger,
+ ))
} else {
log.Logger = log.Output(zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: logFile, TimeFormat: time.RFC3339Nano, NoColor: true},
zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true},
+ &fleetd_logs.Logger,
))
}
} else {
- log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true})
+ log.Logger = log.Output(zerolog.MultiLevelWriter(
+ zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano, NoColor: true},
+ &fleetd_logs.Logger,
+ ))
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
@@ -616,6 +626,36 @@ func main() {
Platform: osqueryHostInfo.Platform,
}
+ // Get the hardware UUID. We use a temporary osquery DB location in order to guarantee that
+ // we're getting true UUID, not a cached UUID. See
+ // https://github.com/fleetdm/fleet/issues/17934 and
+ // https://github.com/osquery/osquery/issues/7509 for more details.
+
+ tmpDBPath := filepath.Join(os.TempDir(), strings.Join([]string{uuid.NewString(), "tmp-db"}, "-"))
+ oi, err := getHostInfo(osquerydPath, tmpDBPath)
+ if err != nil {
+ return fmt.Errorf("get UUID from temp db: %w", err)
+ }
+
+ if err := os.RemoveAll(tmpDBPath); err != nil {
+ log.Info().Err(err).Msg("failed to remove temporary osquery db")
+ }
+
+ if oi.HardwareUUID != orbitHostInfo.HardwareUUID {
+ // Then we have moved to a new physical machine, so we should restart!
+ // Removing the osquery DB should trigger a re-enrollment when fleetd is restarted.
+ if err := os.RemoveAll(osqueryDB); err != nil {
+ return fmt.Errorf("removing old osquery.db: %w", err)
+ }
+
+ // We can remove this because we want it to be regenerated during the re-enrollment.
+ if err := os.RemoveAll(filepath.Join(c.String("root-dir"), constant.OrbitNodeKeyFileName)); err != nil {
+ return fmt.Errorf("removing old orbit node key file: %w", err)
+ }
+
+ return errors.New("found a new hardware uuid, restarting")
+ }
+
// Only send osquery's `instance_id` if the user is running orbit with `--host-identifier=instance`.
// When not set, orbit and osquery will be matched using the hardware UUID (orbitHostInfo.HardwareUUID).
if c.String("host-identifier") == "instance" {
diff --git a/orbit/pkg/profiles/profiles_darwin.go b/orbit/pkg/profiles/profiles_darwin.go
index b530e15b19..93d197a587 100644
--- a/orbit/pkg/profiles/profiles_darwin.go
+++ b/orbit/pkg/profiles/profiles_darwin.go
@@ -247,6 +247,6 @@ func parseEnrollmentProfileValue(line []byte, key string) (string, bool) {
// showEnrollmentProfileCmd is declared as a variable so it can be overwritten by tests.
var showEnrollmentProfileCmd = func() ([]byte, error) {
- cmd := exec.Command("/usr/bin/profiles", "show", "-type", "enrollment")
+ cmd := exec.Command("sh", "-c", `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles show -type enrollment`)
return cmd.Output()
}
diff --git a/orbit/pkg/scripts/exec_windows.go b/orbit/pkg/scripts/exec_windows.go
index f0d58e17ef..fa44467247 100644
--- a/orbit/pkg/scripts/exec_windows.go
+++ b/orbit/pkg/scripts/exec_windows.go
@@ -6,6 +6,7 @@ import (
"context"
"os/exec"
"path/filepath"
+ "time"
)
func ExecCmd(ctx context.Context, scriptPath string, env []string) (output []byte, exitCode int, err error) {
@@ -16,8 +17,15 @@ func ExecCmd(ctx context.Context, scriptPath string, env []string) (output []byt
cmd := exec.CommandContext(ctx, "powershell", "-MTA", "-ExecutionPolicy", "Bypass", "-File", scriptPath)
cmd.Env = env
cmd.Dir = filepath.Dir(scriptPath)
+ cmd.WaitDelay = time.Second
output, err = cmd.CombinedOutput()
- if cmd.ProcessState != nil {
+
+ // we still check if the context was cancelled before setting an exitCode !=
+ // -1, as killing a process on Windows is not straightforward (see the
+ // WaitDelay documentation) and may have timed out even if exit code is
+ // reported as 1, so keep it to -1 in that case so that all user messages are
+ // as expected.
+ if cmd.ProcessState != nil && ctx.Err() == nil {
// The windows exit code is a 32-bit unsigned integer, but the
// interpreter treats it like a signed integer. When a process
// is killed, it returns 0xFFFFFFFF (interpreted as -1). We
diff --git a/orbit/pkg/scripts/scripts.go b/orbit/pkg/scripts/scripts.go
index 397e67a916..08207c16ae 100644
--- a/orbit/pkg/scripts/scripts.go
+++ b/orbit/pkg/scripts/scripts.go
@@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/rs/zerolog/log"
)
// Client defines the methods required for the API requests to the server. The
@@ -65,6 +66,7 @@ func (r *Runner) Run(execIDs []string) error {
break
}
+ log.Debug().Msgf("running script %v", execID)
if err := r.runOne(script); err != nil {
errs = append(errs, err)
}
@@ -120,7 +122,9 @@ func (r *Runner) runOne(script *fleet.HostScriptResult) (finalErr error) {
execCmdFn = ExecCmd
}
start := time.Now()
+ log.Debug().Msgf("starting script execution of %v", script.ExecutionID)
output, exitCode, execErr := execCmdFn(ctx, scriptFile, nil)
+ log.Debug().Msgf("after script execution of %v", script.ExecutionID)
duration := time.Since(start)
// report the output or the error
diff --git a/orbit/pkg/table/extension.go b/orbit/pkg/table/extension.go
index 516bd1a497..6df655422d 100644
--- a/orbit/pkg/table/extension.go
+++ b/orbit/pkg/table/extension.go
@@ -11,6 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/cryptoinfotable"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/dataflattentable"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/firefox_preferences"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/table/fleetd_logs"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/sntp_request"
"github.com/macadmins/osquery-extension/tables/chromeuserprofiles"
"github.com/macadmins/osquery-extension/tables/fileline"
@@ -110,7 +111,12 @@ func (r *Runner) Execute() error {
plugins := OrbitDefaultTables()
opts := PluginOpts{Socket: r.socket}
- plugins = append(plugins, PlatformTables(opts)...)
+ platformTables, err := PlatformTables(opts)
+ if err != nil {
+ return fmt.Errorf("populating platform tabeles: %w", err)
+ }
+
+ plugins = append(plugins, platformTables...)
for _, t := range r.tableExtensions {
plugins = append(plugins, table.NewPlugin(
t.Name(),
@@ -138,6 +144,7 @@ func OrbitDefaultTables() []osquery.OsqueryPlugin {
// Orbit extensions.
table.NewPlugin("sntp_request", sntp_request.Columns(), sntp_request.GenerateFunc),
+ fleetd_logs.TablePlugin(),
firefox_preferences.TablePlugin(osqueryLogger),
cryptoinfotable.TablePlugin(osqueryLogger),
diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go
index 45a4f56253..bf6cfe9c41 100644
--- a/orbit/pkg/table/extension_darwin.go
+++ b/orbit/pkg/table/extension_darwin.go
@@ -38,7 +38,7 @@ import (
"github.com/osquery/osquery-go/plugin/table"
)
-func PlatformTables(opts PluginOpts) []osquery.OsqueryPlugin {
+func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
plugins := []osquery.OsqueryPlugin{
// Fleet tables
table.NewPlugin("icloud_private_relay", privaterelay.Columns(), privaterelay.Generate),
@@ -96,5 +96,5 @@ func PlatformTables(opts PluginOpts) []osquery.OsqueryPlugin {
// append platform specific tables
plugins = appendTables(plugins)
- return plugins
+ return plugins, nil
}
diff --git a/orbit/pkg/table/extension_linux.go b/orbit/pkg/table/extension_linux.go
index 1e243e849d..3724537678 100644
--- a/orbit/pkg/table/extension_linux.go
+++ b/orbit/pkg/table/extension_linux.go
@@ -10,10 +10,10 @@ import (
"github.com/osquery/osquery-go"
)
-func PlatformTables(_ PluginOpts) []osquery.OsqueryPlugin {
+func PlatformTables(_ PluginOpts) ([]osquery.OsqueryPlugin, error) {
return []osquery.OsqueryPlugin{
cryptsetup.TablePlugin(osqueryLogger), // table name is "cryptsetup_status"
falconctl.NewFalconctlOptionTable(osqueryLogger), // table name is "falconctl_option"
falcon_kernel_check.TablePlugin(osqueryLogger), // table name is "falcon_kernel_check"
- }
+ }, nil
}
diff --git a/orbit/pkg/table/extension_windows.go b/orbit/pkg/table/extension_windows.go
index 8fdc0afd40..f59c7774a1 100644
--- a/orbit/pkg/table/extension_windows.go
+++ b/orbit/pkg/table/extension_windows.go
@@ -3,20 +3,54 @@
package table
import (
+ "fmt"
+
cisaudit "github.com/fleetdm/fleet/v4/orbit/pkg/table/cis_audit"
mdmbridge "github.com/fleetdm/fleet/v4/orbit/pkg/table/mdm"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/windowsupdatetable"
+ "golang.org/x/sys/windows/registry"
"github.com/osquery/osquery-go"
"github.com/osquery/osquery-go/plugin/table"
)
-func PlatformTables(_ PluginOpts) []osquery.OsqueryPlugin {
- return []osquery.OsqueryPlugin{
+func PlatformTables(_ PluginOpts) ([]osquery.OsqueryPlugin, error) {
+ plugins := []osquery.OsqueryPlugin{
// Fleet tables
- table.NewPlugin("mdm_bridge", mdmbridge.Columns(), mdmbridge.Generate),
table.NewPlugin("cis_audit", cisaudit.Columns(), cisaudit.Generate),
windowsupdatetable.TablePlugin(windowsupdatetable.UpdatesTable, osqueryLogger), // table name is "windows_updates"
}
+
+ windowsServer, err := IsWindowsServer()
+ if err != nil {
+ return nil, err
+ }
+
+ if !windowsServer {
+ plugins = append(plugins, table.NewPlugin("mdm_bridge", mdmbridge.Columns(), mdmbridge.Generate))
+ }
+
+ return plugins, nil
+}
+
+func IsWindowsServer() (bool, error) {
+ // If the registry can't be read, it's safer to assume we're a
+ // server and not load the broken table
+ k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
+ if err != nil {
+ return false, fmt.Errorf("windows server check: %w", err)
+ }
+ defer k.Close()
+
+ s, _, err := k.GetStringValue("InstallationType")
+ if err != nil {
+ return false, fmt.Errorf("windows server check: %w", err)
+ }
+
+ if s == "Server" {
+ return true, nil
+ }
+
+ return false, nil
}
diff --git a/orbit/pkg/table/fleetd_logs/fleetd_logs.go b/orbit/pkg/table/fleetd_logs/fleetd_logs.go
new file mode 100644
index 0000000000..803f0027db
--- /dev/null
+++ b/orbit/pkg/table/fleetd_logs/fleetd_logs.go
@@ -0,0 +1,181 @@
+package fleetd_logs
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/osquery/osquery-go/plugin/table"
+ "github.com/rs/zerolog"
+)
+
+// No timezone, always return in UTC. Use this format because SQLite3
+// knows how to parse it.
+// See https://www.sqlite.org/lang_datefunc.html
+const timeFormatString = "2006-01-02 15:04:05.999999999"
+
+var Logger = logger{}
+var MaxEntries uint = 10_000
+
+func TablePlugin() *table.Plugin {
+ columns := []table.ColumnDefinition{
+ table.TextColumn("time"),
+ table.TextColumn("level"),
+ table.TextColumn("payload"),
+ table.TextColumn("message"),
+ table.TextColumn("error"),
+ }
+
+ return table.NewPlugin("fleetd_logs", columns, generate)
+}
+
+func generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
+ output := []map[string]string{}
+
+ for _, entry := range Logger.logs {
+ row := make(map[string]string, 5)
+ // It would be nice if we could return NULL instead of an
+ // empty string when the error is empty
+ row["time"] = entry.Time
+ row["level"] = entry.Level.String()
+ row["payload"] = entry.Payload
+ row["message"] = entry.Message
+ row["error"] = entry.Error
+ output = append(output, row)
+ }
+ return output, nil
+}
+
+type Event struct {
+ Time string
+ Level zerolog.Level
+ Payload string
+ Message string
+ Error string
+}
+
+type logger struct {
+ writeMutex sync.Mutex
+ logs []Event
+}
+
+func (l *logger) Write(event []byte) (int, error) {
+ msgs, err := processLogEntry(event)
+ if err != nil {
+ return 0, fmt.Errorf("fleet_logs.Write: %w", err)
+ }
+
+ l.writeMutex.Lock()
+ defer l.writeMutex.Unlock()
+
+ l.logs = append(l.logs, msgs...)
+
+ if MaxEntries > 0 && len(l.logs) > int(MaxEntries) {
+ l.logs = l.logs[len(l.logs)-int(MaxEntries):]
+ }
+
+ return len(event), nil
+}
+
+func (l *logger) WriteLevel(level zerolog.Level, event []byte) (int, error) {
+ msgs, err := processLogEntry(event)
+ if err != nil {
+ return 0, fmt.Errorf("fleet_logs.WriteLevel: %w", err)
+ }
+
+ for idx := range msgs {
+ msgs[idx].Level = level
+ }
+
+ l.writeMutex.Lock()
+ defer l.writeMutex.Unlock()
+
+ l.logs = append(l.logs, msgs...)
+
+ if MaxEntries > 0 && len(l.logs) > int(MaxEntries) {
+ l.logs = l.logs[len(l.logs)-int(MaxEntries):]
+ }
+
+ return len(event), nil
+}
+
+func processLogEntry(event []byte) ([]Event, error) {
+ var evts []map[string]interface{}
+ dec := json.NewDecoder(bytes.NewReader(event))
+ dec.UseNumber()
+ for {
+ var evt map[string]interface{}
+ if err := dec.Decode(&evt); err == io.EOF {
+ break
+ } else if err != nil {
+ return nil, fmt.Errorf("cannot decode: %w", err)
+ }
+ evts = append(evts, evt)
+ }
+
+ var entries []Event
+
+ for _, evt := range evts {
+ level := zerolog.GlobalLevel()
+ var err error
+ evtLevel, ok := evt["level"].(string)
+ if ok {
+ level, err = zerolog.ParseLevel(evtLevel)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse log event level: %w", err)
+ }
+ delete(evt, "level")
+ }
+
+ var sqliteTime string
+ evtTime, ok := evt["time"].(string)
+ if ok {
+ goTime, err := time.Parse("2006-01-02T15:04:05-07:00", evtTime)
+ if err != nil {
+ return nil, fmt.Errorf("processLogEntry parsing time: %w", err)
+ }
+ sqliteTime = goTime.UTC().Format(timeFormatString)
+ delete(evt, "time")
+ } else {
+ sqliteTime = time.Now().UTC().Format(timeFormatString)
+ }
+
+ evtMessage, ok := evt["message"].(string)
+ if ok {
+ delete(evt, "message")
+ } else {
+ evtMessage = ""
+ }
+
+ evtError, ok := evt["error"].(string)
+ if ok {
+ delete(evt, "error")
+ } else {
+ evtError = ""
+ }
+
+ payload := []byte{}
+ if len(evt) > 0 {
+ payload, err = json.Marshal(evt)
+ if err != nil {
+ return nil, fmt.Errorf("unable to marshall log event: %w", err)
+ }
+ }
+
+ entry := Event{
+ Time: sqliteTime,
+ Level: level,
+ Payload: string(payload),
+ Message: evtMessage,
+ Error: evtError,
+ }
+
+ entries = append(entries, entry)
+ }
+
+ return entries, nil
+}
diff --git a/orbit/pkg/table/tcc_access/tcc_access.go b/orbit/pkg/table/tcc_access/tcc_access.go
index de0929aadd..e5f83f7a66 100644
--- a/orbit/pkg/table/tcc_access/tcc_access.go
+++ b/orbit/pkg/table/tcc_access/tcc_access.go
@@ -8,10 +8,12 @@ import (
"context"
"errors"
"fmt"
+ "os"
"os/exec"
"strings"
"github.com/osquery/osquery-go/plugin/table"
+ "github.com/rs/zerolog/log"
)
var (
@@ -69,6 +71,13 @@ func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[strin
if satisfiesUidConstraints {
tccPath := tccPathPrefix + "/Users/" + username + tccPathSuffix
+ if _, err := os.Stat(tccPath); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ log.Debug().Err(err).Msgf("file for user %s not found: %s", username, tccPath)
+ continue
+ }
+ return nil, err
+ }
uRs, err := getTCCAccessRows(uid, tccPath)
if err != nil {
return nil, err
@@ -98,7 +107,7 @@ func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[strin
}
func getTCCAccessRows(uid, tccPath string) ([]map[string]string, error) {
- // querying direclty with sqlite3 avoids additional C compilation requirements that would be introduced by using
+ // querying directly with sqlite3 avoids additional C compilation requirements that would be introduced by using
// https://github.com/mattn/go-sqlite3
cmd := exec.Command(sqlite3Path, tccPath, dbQuery)
var dbOut bytes.Buffer
diff --git a/orbit/pkg/update/execcmd_darwin.go b/orbit/pkg/update/execcmd_darwin.go
index 65cbdf7c27..6eb8847282 100644
--- a/orbit/pkg/update/execcmd_darwin.go
+++ b/orbit/pkg/update/execcmd_darwin.go
@@ -3,5 +3,6 @@
package update
func runRenewEnrollmentProfile() error {
- return runCmdCollectErr("/usr/bin/profiles", "renew", "--type", "enrollment")
+ cmd := `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles renew -type enrollment`
+ return runCmdCollectErr("sh", "-c", cmd)
}
diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go
index c185ee64bd..63224ac859 100644
--- a/orbit/pkg/update/notifications.go
+++ b/orbit/pkg/update/notifications.go
@@ -104,14 +104,15 @@ func (h *renewEnrollmentProfileConfigReceiver) Run(config *fleet.OrbitConfig) er
fn = runRenewEnrollmentProfile
}
if err := fn(); err != nil {
- // TODO: Look into whether we should increment lastRun here or implement a
- // backoff to avoid unnecessary user notification popups and mitigate rate
- // limiting by Apple.
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
- } else {
- h.lastRun = time.Now()
- log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
+ // TODO: Design a better way to backoff `profiles show` so that the device doesn't get rate
+ // limited by Apple. For now, wait at least 2 minutes before retrying.
+ h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
+ return nil
}
+ h.lastRun = time.Now()
+ log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
+
} else {
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
}
diff --git a/orbit/pkg/useraction/mdm_migration_darwin.go b/orbit/pkg/useraction/mdm_migration_darwin.go
index a282a9a05c..323fc847fa 100644
--- a/orbit/pkg/useraction/mdm_migration_darwin.go
+++ b/orbit/pkg/useraction/mdm_migration_darwin.go
@@ -8,6 +8,8 @@ import (
"fmt"
"os"
"os/exec"
+ "strconv"
+ "strings"
"sync"
"text/template"
"time"
@@ -48,7 +50,7 @@ const mdmUnenrollmentTotalWaitTime = 90 * time.Second
// between unenrollment checks.
const defaultUnenrollmentRetryInterval = 5 * time.Second
-var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
+var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemplate").Parse(`
## Migrate to Fleet
Select **Start** and look for this notification in your notification center:` +
@@ -56,6 +58,14 @@ Select **Start** and look for this notification in your notification center:` +
"After you start, this window will popup every 15-20 minutes until you finish.",
))
+var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
+## Migrate to Fleet
+
+Select **Start** and Remote Management window will appear soon:` +
+ "\n\n\n\n" +
+ "After you start, this window will popup every 15-20 minutes until you finish.",
+))
+
var errorTemplate = template.Must(template.New("").Parse(`
### Something's gone wrong.
@@ -291,29 +301,9 @@ func (m *swiftDialogMDMMigrator) waitForUnenrollment() error {
}
func (m *swiftDialogMDMMigrator) renderMigration() error {
- var message bytes.Buffer
- if err := mdmMigrationTemplate.Execute(
- &message,
- m.props,
- ); err != nil {
- return fmt.Errorf("execute template: %w", err)
- }
-
- flags := []string{
- // main button
- "--button1text", "Start",
- // secondary button
- "--button2text", "Later",
- "--height", "440",
- }
-
- if m.props.OrgInfo.ContactURL != "" {
- flags = append(flags,
- // info button
- "--infobuttontext", "Unsure? Contact IT",
- "--infobuttonaction", m.props.OrgInfo.ContactURL,
- "--quitoninfo",
- )
+ message, flags, err := m.getMessageAndFlags()
+ if err != nil {
+ return fmt.Errorf("getting mdm migrator message: %w", err)
}
exitCodeCh, errCh := m.render(message.String(), flags...)
@@ -419,3 +409,71 @@ func (m *swiftDialogMDMMigrator) ShowInterval() error {
func (m *swiftDialogMDMMigrator) SetProps(props MDMMigratorProps) {
m.props = props
}
+
+func (m *swiftDialogMDMMigrator) getMessageAndFlags() (*bytes.Buffer, []string, error) {
+ vers, err := m.getMacOSMajorVersion()
+ if err != nil {
+ // log error for debugging and continue with default template
+ log.Error().Err(err).Msg("getting macOS major version failed: using default migration template")
+ }
+
+ tmpl := mdmMigrationTemplate
+ height := "669"
+ if vers != 0 && vers < 14 {
+ height = "440"
+ tmpl = mdmMigrationTemplatePreSonoma
+ }
+
+ var message bytes.Buffer
+ if err := tmpl.Execute(
+ &message,
+ m.props,
+ ); err != nil {
+ return nil, nil, fmt.Errorf("executing migrqation template: %w", err)
+ }
+
+ flags := []string{
+ // main button
+ "--button1text", "Start",
+ // secondary button
+ "--button2text", "Later",
+ "--height", height,
+ }
+
+ if m.props.OrgInfo.ContactURL != "" {
+ flags = append(flags,
+ // info button
+ "--infobuttontext", "Unsure? Contact IT",
+ "--infobuttonaction", m.props.OrgInfo.ContactURL,
+ "--quitoninfo",
+ )
+ }
+
+ return &message, flags, nil
+}
+
+// TODO: make this a variable for testing
+func (m *swiftDialogMDMMigrator) getMacOSMajorVersion() (int, error) {
+ cmd := exec.Command("sw_vers", "-productVersion")
+ out, err := cmd.Output()
+ if err != nil {
+ return 0, fmt.Errorf("getting macOS version: %w", err)
+ }
+ parts := strings.SplitN(string(out), ".", 2)
+ switch len(parts) {
+ case 0:
+ // this should never happen
+ return 0, errors.New("getting macOS version: sw_vers command returned no output")
+ case 1:
+ // unexpected, so log for debugging
+ log.Debug().Msgf("parsing macOS version: expected 2 parts, got 1: %s", out)
+ default:
+ // ok
+ }
+
+ major, err := strconv.Atoi(parts[0])
+ if err != nil {
+ return 0, fmt.Errorf("parsing macOS major version: %w", err)
+ }
+ return major, nil
+}
diff --git a/pkg/mdm/mdmtest/apple.go b/pkg/mdm/mdmtest/apple.go
index 8d18c1c2a1..52bfdd1754 100644
--- a/pkg/mdm/mdmtest/apple.go
+++ b/pkg/mdm/mdmtest/apple.go
@@ -28,9 +28,9 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util"
"github.com/fleetdm/fleet/v4/server/mdm/scep/scep"
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
- "github.com/go-kit/kit/log"
- kitlog "github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
+ "github.com/go-kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/groob/plist"
"go.mozilla.org/pkcs7"
diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go
index 0fbebe70b5..a416c33787 100644
--- a/pkg/spec/gitops.go
+++ b/pkg/spec/gitops.go
@@ -244,7 +244,8 @@ func parseSecrets(result *GitOps, multiError *multierror.Error) *multierror.Erro
return multierror.Append(multiError, errors.New("'team_settings.secrets' is required"))
}
}
- var enrollSecrets []*fleet.EnrollSecret
+ // When secrets slice is empty, all secrets are removed.
+ enrollSecrets := make([]*fleet.EnrollSecret, 0)
if rawSecrets != nil {
secrets, ok := rawSecrets.([]interface{})
if !ok {
diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go
index 586915a33d..8d9b00bffc 100644
--- a/pkg/spec/gitops_test.go
+++ b/pkg/spec/gitops_test.go
@@ -137,6 +137,7 @@ func TestValidGitOpsYaml(t *testing.T) {
serverSettings, ok := gitops.OrgSettings["server_settings"]
assert.True(t, ok, "server_settings not found")
assert.Equal(t, "https://fleet.example.com", serverSettings.(map[string]interface{})["server_url"])
+ assert.EqualValues(t, 2000, serverSettings.(map[string]interface{})["query_report_cap"])
assert.Contains(t, gitops.OrgSettings, "org_info")
orgInfo, ok := gitops.OrgSettings["org_info"].(map[string]interface{})
assert.True(t, ok)
diff --git a/pkg/spec/testdata/global_config_no_paths.yml b/pkg/spec/testdata/global_config_no_paths.yml
index cdc6e78923..7fabc5119a 100644
--- a/pkg/spec/testdata/global_config_no_paths.yml
+++ b/pkg/spec/testdata/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: https://fleet.example.com
diff --git a/pkg/spec/testdata/org-settings.yml b/pkg/spec/testdata/org-settings.yml
index 22f17b97a5..fb51b15a2a 100644
--- a/pkg/spec/testdata/org-settings.yml
+++ b/pkg/spec/testdata/org-settings.yml
@@ -4,6 +4,7 @@ server_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: https://fleet.example.com
diff --git a/render.yaml b/render.yaml
index bcf77cf770..709a1bdc95 100644
--- a/render.yaml
+++ b/render.yaml
@@ -7,7 +7,13 @@ services:
url: 'fleetdm/fleet:latest'
preDeployCommand: "fleet prepare --no-prompt=true db"
healthCheckPath: /healthz
+ disk:
+ name: installers
+ mountPath: /opt/fleet/installers
+ sizeGB: 10
envVars:
+ - key: FLEET_SOFTWARE_INSTALLER_STORE_DIR
+ value: '/opt/fleet/installers'
- key: FLEET_SERVER_PRIVATE_KEY
generateValue: true
- key: FLEET_MYSQL_ADDRESS
diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json
index 87ce3656c3..fc665ac993 100644
--- a/schema/osquery_fleet_schema.json
+++ b/schema/osquery_fleet_schema.json
@@ -9730,7 +9730,7 @@
},
{
"name": "etc_hosts",
- "description": "Line-parsed /etc/hosts.",
+ "description": "The `hosts` file comprises a local, plain-text configuration for mapping IP addresses to host names. It does not necessarily rely on an external Domain Name System (DNS) for routing. The `etc_hosts` osquery table expresses the data in the `hosts` file.",
"url": "https://fleetdm.com/tables/etc_hosts",
"platforms": [
"darwin",
@@ -9739,8 +9739,8 @@
],
"evented": false,
"cacheable": true,
- "notes": "",
- "examples": "Identify host\"name\"s pointed to IP addresses using the hosts file. This\ntechnique is often abused by malware, but can also indicate services that do\nnot have proper DNS configuration to be reached from workstations.\n\n```\nSELECT * FROM etc_hosts WHERE address!='127.0.0.1' AND address!='::1' AND address!='255.255.255.255';\n```",
+ "notes": "The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites.\n\nFile paths to `hosts`:\n- Linux: `/etc/hosts`\n- macOS: `/private/etc/hosts`\n- Windows: `C:\\Windows\\system32\\drivers\\etc`\n\n**More info**:\n- [DNS](https://en.wikipedia.org/wiki/Domain_Name_System)\n- The `/etc/hosts` [Guide For Linux](https://thelinuxcode.com/etc-hosts-file-complete-guide-for-linux/)\n- [How to edit the hosts file on Windows](https://www.howtogeek.com/784196/how-to-edit-the-hosts-file-on-windows-10-or-11)",
+ "examples": "This query detects if the macOS `/private/etc/hosts` file has been modified from its default state:\n\n```\nSELECT * FROM etc_hosts WHERE address != '127.0.0.1' AND address != '::1' AND address != '255.255.255.255';\n```",
"columns": [
{
"name": "address",
@@ -11047,6 +11047,51 @@
"url": "https://fleetdm.com/tables/firmwarepasswd",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/firmwarepasswd.yml"
},
+ {
+ "name": "fleetd_logs",
+ "evented": false,
+ "platforms": [
+ "darwin",
+ "windows",
+ "linux"
+ ],
+ "description": "Returns the logs from fleetd's current session. Logs are stored in memory, so they are erased when it restarts.",
+ "examples": "```\nSELECT * FROM fleetd_logs\n```\n\nReturn only log entries with errors attached\n\n```\nSELECT * FROM fleetd_logs WHERE error != \"\"\n```",
+ "columns": [
+ {
+ "name": "time",
+ "description": "The time the event was captured, UTC.",
+ "type": "text",
+ "required": false
+ },
+ {
+ "name": "level",
+ "description": "The log-level of the event. Info, Debug, etc.",
+ "type": "text",
+ "required": false
+ },
+ {
+ "name": "error",
+ "description": "The error attached to the event",
+ "type": "text",
+ "required": false
+ },
+ {
+ "name": "message",
+ "description": "The message attached to the event",
+ "type": "text",
+ "required": false
+ },
+ {
+ "name": "payload",
+ "description": "Any extra data attached to the event, JSON",
+ "type": "text",
+ "required": false
+ }
+ ],
+ "url": "https://fleetdm.com/tables/fleetd_logs",
+ "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/fleetd_logs.yml"
+ },
{
"name": "gatekeeper",
"description": "macOS Gatekeeper Details.",
@@ -18625,15 +18670,15 @@
},
{
"name": "package_bom",
- "description": "macOS package bill of materials (BOM) file list.",
+ "description": "The \"bill of materials\" (`.bom`) file in a macOS installer package (`.pkg`) lists all files installed by the package. The `package_bom` osquery table collects the data from the `.bom` files created in `/private/var/db/receipts` by macOS when a `.pkg` file is executed.",
"url": "https://fleetdm.com/tables/package_bom",
"platforms": [
"darwin"
],
"evented": false,
"cacheable": false,
- "notes": "",
- "examples": "List the bill of materials of a package. The receipts directory contains\npackages to installed applications.\n\n```\nSELECT * FROM package_bom WHERE path='/private/var/db/receipts/com.yubico.ykman.bom';\n```",
+ "notes": "Keeping track of files installed by applications is critical for upholding software management best security practices.\n\nApple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)",
+ "examples": "This query collects the filepath and time of installation for the libVFXCore.dylib (Dynamic Library) file installed as part of Xcode.app:\n\n```\nSELECT filepath,modified_time FROM package_bom WHERE path='/private/var/db/receipts/com.apple.pkg.Xcode.bom' AND filepath LIKE '%libVFXCore.dylib';\n```",
"columns": [
{
"name": "filepath",
@@ -18703,15 +18748,15 @@
},
{
"name": "package_install_history",
- "description": "macOS package install history.",
+ "description": "The `package_install_history` table provides a detailed log of all packages installled on macOS.",
"url": "https://fleetdm.com/tables/package_install_history",
"platforms": [
"darwin"
],
"evented": false,
"cacheable": false,
- "notes": "",
- "examples": "See a list of packages installed in the last week.\n\n```\nSELECT name, version, source, datetime(time,'unixepoch') AS install_time from package_install_history WHERE install_time |-= datetime('now','-7 days');\n```",
+ "notes": "\nMonitoring the macOS package install history is useful for:\n- Regularly checking for newly installed packages and identifying suspicious software\n- Verifying that only approved packages are installed\n- Creating a Fleet policy to receive alerts for any unauthorized or vulnerable installations\n\nApple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)",
+ "examples": "Basic query:\n\n```\nSELECT name,package_id,version,source,datetime(time,'unixepoch') AS install_time FROM package_install_history WHERE install_time >= datetime('now','-7 days');\n```\n\nThis query fetches the following data for a macOS package:\n- Name\n- Package ID\n- Version\n- Source\n- Install time\n\nThe `WHERE` clause filters the results to show only packages installed in the past 7 days.",
"columns": [
{
"name": "package_id",
@@ -19455,15 +19500,15 @@
},
{
"name": "pipes",
- "description": "Named and Anonymous pipes.",
+ "description": "Named pipes in Windows can be used to provide communication between processes on a computer or between processes on different computers across a network. The `pipes` osquery table lists the named pipes currently running on a Windows computer.",
"url": "https://fleetdm.com/tables/pipes",
"platforms": [
"windows"
],
"evented": false,
"cacheable": false,
- "notes": "",
- "examples": "```\nselect * from pipes\n```",
+ "notes": "Running the following command at a prompt in PowerShell lists the named pipes currently open on a Windows computer:\n\n```\nget-childitem \\\\.\\pipe\\\n```\n\nLinks:\n- Microsoft documentation on [named pipes](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes)\n- Discover files linked to processes with Windows [Process Explorer](https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer)",
+ "examples": "This query displays all attributes (columns) for the named pipe enabled by opening PowerShell:\n\n```\nSELECT * FROM pipes WHERE name LIKE '%powershell';\n```",
"columns": [
{
"name": "pid",
@@ -19511,12 +19556,11 @@
"index": false
}
],
- "osqueryRepoUrl": "https://github.com/osquery/osquery/blob/master/specs/windows/pipes.table",
- "fleetRepoUrl": "https://github.com/fleetdm/fleet/new/main/schema?filename=tables%2Fpipes.yml&value=name%3A%20pipes%0Adescription%3A%20%7C-%20%23%20(required)%20string%20-%20The%20description%20for%20this%20table.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%23%20Add%20description%20here%0Aexamples%3A%20%7C-%20%23%20(optional)%20string%20-%20An%20example%20query%20for%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown%0A%09%23%20Add%20examples%20here%0Anotes%3A%20%7C-%20%23%20(optional)%20string%20-%20Notes%20about%20this%20table.%20Note%3A%20This%20field%20supports%20Markdown.%0A%09%23%20Add%20notes%20here%0Acolumns%3A%20%23%20(required)%0A%09-%20name%3A%20%23%20(required)%20string%20-%20The%20name%20of%20the%20column%0A%09%20%20description%3A%20%23%20(required)%20string%20-%20The%20column's%20description.%20Note%3A%20this%20field%20supports%20Markdown%0A%09%20%20type%3A%20%23%20(required)%20string%20-%20the%20column's%20data%20type%0A%09%20%20required%3A%20%23%20(required)%20boolean%20-%20whether%20or%20not%20this%20column%20is%20required%20to%20query%20this%20table."
+ "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/pipes.yml"
},
{
"name": "platform_info",
- "description": "Information about EFI/UEFI/ROM and platform/boot.",
+ "description": "The `platform_info` osquery table collects boot platform information from a computer. The `platform_info` table works on Linux, macOS and Windows.",
"url": "https://fleetdm.com/tables/platform_info",
"platforms": [
"darwin",
@@ -19525,8 +19569,8 @@
],
"evented": false,
"cacheable": false,
- "notes": "",
- "examples": "See version information about the boot system, such as iBoot on Apple Silicon\n\n```\nSELECT version FROM platform_info;\n```",
+ "notes": "Links:\n- EFI: https://en.wikipedia.org/wiki/EFI_system_partition \n- iboot: https://en.wikipedia.org/wiki/IBoot \n- UEFI: https://en.wikipedia.org/wiki/UEFI#Classes \n- System booting: https://en.wikipedia.org/wiki/Booting ",
+ "examples": "Basic query:\n\n```\nSELECT extra,firmware_type,vendor FROM platform_info;\n```\n\nThis query results in a listing of the following attributes on a macOS host running a Windows 11 virtual machine in the Parallels.app:\n\nMac -\n- extra = \"Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020\"\n- firmware_type = \"iboot\"\n- vendor = \"Apple Inc.\"\n\nWindows -\n- extra = \"\"\n- firmware_type = \"uefi\"\n- vendor = \"Parallels International GmbH.\"",
"columns": [
{
"name": "vendor",
diff --git a/schema/tables/etc_hosts.yml b/schema/tables/etc_hosts.yml
index 0808897836..63adce5acb 100644
--- a/schema/tables/etc_hosts.yml
+++ b/schema/tables/etc_hosts.yml
@@ -10,9 +10,9 @@ notes: |-
The `hosts` file is customized by many organizations. As part of a defense-in-depth security posture it's important to track `hosts` modifications. Endpoints with a modified `hosts` configuration connected to enterprise networks can potentially bypass network rules, proxies and firewalls or be routed to malicious sites.
File paths to `hosts`:
- - Linux: /etc/hosts
- - macOS: /private/etc/hosts
- - Windows: C:\Windows\system32\drivers\etc
+ - Linux: `/etc/hosts`
+ - macOS: `/private/etc/hosts`
+ - Windows: `C:\Windows\system32\drivers\etc`
**More info**:
- [DNS](https://en.wikipedia.org/wiki/Domain_Name_System)
diff --git a/schema/tables/fleed_logs.yml b/schema/tables/fleed_logs.yml
new file mode 100644
index 0000000000..bed48c9b3e
--- /dev/null
+++ b/schema/tables/fleed_logs.yml
@@ -0,0 +1,39 @@
+name: fleetd_logs
+evented: false
+platforms:
+ - darwin
+ - windows
+ - linux
+description: |-
+ Returns the logs from fleetd's current session. Logs are stored in memory, so they are erased when it restarts.
+examples: |-
+ ```
+ SELECT * FROM fleetd_logs
+ ```
+
+ Return only log entries with errors attached
+
+ ```
+ SELECT * FROM fleetd_logs WHERE error != ""
+ ```
+columns:
+ - name: time
+ description: The time the event was captured, UTC.
+ type: text
+ required: false
+ - name: level
+ description: The log-level of the event. Info, Debug, etc.
+ type: text
+ required: false
+ - name: error
+ description: The error attached to the event
+ type: text
+ required: false
+ - name: message
+ description: The message attached to the event
+ type: text
+ required: false
+ - name: payload
+ description: Any extra data attached to the event, JSON
+ type: text
+ required: false
diff --git a/schema/tables/package_bom.yml b/schema/tables/package_bom.yml
index 999e583e80..0c3974c98f 100644
--- a/schema/tables/package_bom.yml
+++ b/schema/tables/package_bom.yml
@@ -1,8 +1,12 @@
name: package_bom
+description: The "bill of materials" (`.bom`) file in a macOS installer package (`.pkg`) lists all files installed by the package. The `package_bom` osquery table collects the data from the `.bom` files created in `/private/var/db/receipts` by macOS when a `.pkg` file is executed.
examples: |-
- List the bill of materials of a package. The receipts directory contains
- packages to installed applications.
+ This query collects the filepath and time of installation for the libVFXCore.dylib (Dynamic Library) file installed as part of Xcode.app:
```
- SELECT * FROM package_bom WHERE path='/private/var/db/receipts/com.yubico.ykman.bom';
+ SELECT filepath,modified_time FROM package_bom WHERE path='/private/var/db/receipts/com.apple.pkg.Xcode.bom' AND filepath LIKE '%libVFXCore.dylib';
```
+notes: |-
+ Keeping track of files installed by applications is critical for upholding software management best security practices.
+
+ Apple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)
diff --git a/schema/tables/package_install_history.yml b/schema/tables/package_install_history.yml
index 0c34d2b1a9..7073521346 100644
--- a/schema/tables/package_install_history.yml
+++ b/schema/tables/package_install_history.yml
@@ -1,7 +1,25 @@
name: package_install_history
+description: The `package_install_history` table provides a detailed log of all packages installled on macOS.
examples: |-
- See a list of packages installed in the last week.
+ Basic query:
```
- SELECT name, version, source, datetime(time,'unixepoch') AS install_time from package_install_history WHERE install_time |-= datetime('now','-7 days');
+ SELECT name,package_id,version,source,datetime(time,'unixepoch') AS install_time FROM package_install_history WHERE install_time >= datetime('now','-7 days');
```
+
+ This query fetches the following data for a macOS package:
+ - Name
+ - Package ID
+ - Version
+ - Source
+ - Install time
+
+ The `WHERE` clause filters the results to show only packages installed in the past 7 days.
+notes: |-
+
+ Monitoring the macOS package install history is useful for:
+ - Regularly checking for newly installed packages and identifying suspicious software
+ - Verifying that only approved packages are installed
+ - Creating a Fleet policy to receive alerts for any unauthorized or vulnerable installations
+
+ Apple’s [installer package documentation](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution)
diff --git a/schema/tables/patches.yml b/schema/tables/patches.yml
new file mode 100644
index 0000000000..2c8cfc4416
--- /dev/null
+++ b/schema/tables/patches.yml
@@ -0,0 +1,23 @@
+name: patches
+description: |- # (required) string - The description for this table. Note: this field supports Markdown
+ The `patches` osquery table lists Windows security patch updates.
+examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
+ Basic query:
+
+ ```
+ SELECT * FROM patches;
+ ```
+
+ This query determines if a specific hotfix patch is installed:
+
+ ```
+ SELECT * FROM patches WHERE hotfix_id='kb5037663';
+ ```
+notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
+ Microsoft creates a support page per hotfix patch. Support pages can be discovered by doing a web browser search for the hotfix ID string (e.g., KB5037663).
+
+ Microsoft documentation for [KB5037663](https://support.microsoft.com/en-us/topic/may-29-2024-kb5037853-os-builds-22621-3672-and-22631-3672-preview-dcf14fd8-84d6-4234-9d5b-784c319cd7cf)
+
+ The `patches` table does not include updates that are applied via Windows Installer / Microsoft Standard Installer packages (.msi) or updates downloaded directly from Windows Update (e.g., Service Packs).
+
+ [Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)
diff --git a/schema/tables/pipes.yml b/schema/tables/pipes.yml
new file mode 100644
index 0000000000..b6865ef92e
--- /dev/null
+++ b/schema/tables/pipes.yml
@@ -0,0 +1,20 @@
+name: pipes
+description: |- # (required) string - The description for this table. Note: this field supports Markdown
+ Named pipes in Windows can be used to provide communication between processes on a computer or between processes on different computers across a network. The `pipes` osquery table lists the named pipes currently running on a Windows computer.
+examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
+ This query displays all attributes (columns) for the named pipe enabled by opening PowerShell:
+
+ ```
+ SELECT * FROM pipes WHERE name LIKE '%powershell';
+ ```
+notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
+ Running the following command at a prompt in PowerShell lists the named pipes currently open on a Windows computer:
+
+ ```
+ PS C:\Windows\System32> get-childitem \\.\pipe\
+ ```
+
+ Links:
+ - Microsoft documentation on [named pipes](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes)
+ - Discover files linked to processes with Windows [Process Explorer](https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer)
+ - Windows [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.4)
diff --git a/schema/tables/platform_info.yml b/schema/tables/platform_info.yml
index 6075a2344d..dad1cb11d8 100644
--- a/schema/tables/platform_info.yml
+++ b/schema/tables/platform_info.yml
@@ -1,7 +1,26 @@
name: platform_info
+description: The `platform_info` osquery table collects boot platform information from a computer. The `platform_info` table works on Linux, macOS and Windows.
examples: |-
- See version information about the boot system, such as iBoot on Apple Silicon
+ Basic query:
```
- SELECT version FROM platform_info;
+ SELECT extra,firmware_type,vendor FROM platform_info;
```
+
+ This query results in a listing of the following attributes on a macOS host running a Windows 11 virtual machine in the Parallels.app:
+
+ Mac -
+ - extra = "Darwin Kernel Version 23.5.0: Wed May 1 20:14:38 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6020"
+ - firmware_type = "iboot"
+ - vendor = "Apple Inc."
+
+ Windows -
+ - extra = ""
+ - firmware_type = "uefi"
+ - vendor = "Parallels International GmbH."
+notes: |-
+ Links:
+ - EFI: https://en.wikipedia.org/wiki/EFI_system_partition
+ - iboot: https://en.wikipedia.org/wiki/IBoot
+ - UEFI: https://en.wikipedia.org/wiki/UEFI#Classes
+ - System booting: https://en.wikipedia.org/wiki/Booting
diff --git a/schema/tables/programs.yml b/schema/tables/programs.yml
new file mode 100644
index 0000000000..4bcd5aa70a
--- /dev/null
+++ b/schema/tables/programs.yml
@@ -0,0 +1,33 @@
+name: programs
+description: |- # (required) string - The description for this table. Note: this field supports Markdown
+ The `programs` table lists applications installed via Windows Installer from a package.
+examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
+ Basic query:
+
+ ```
+ SELECT * FROM programs;
+ ```
+
+ This query determines if a specific version of Google Chrome.exe is installed:
+
+ ```
+ SELECT name,version FROM programs WHERE name='Google Chrome' AND version='125.0.6422.142';
+ ```
+notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
+ This table includes references for applications:
+
+ - downloaded directly from websites and installed by an end user like Google Chrome or Notepad++
+ - installed via automation frameworks like winget or Chocolatey
+ - installed via command line in cmd or PowerShell
+
+ Links:
+
+ - [Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)
+ - [Chocolatey](https://chocolatey.org/)
+ - The Fleet `chocolatey_packages`[table](https://fleetdm.com/tables/chocolatey_packages)
+ - [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)
+ - [winget.run](https://winget.run/)
+ - Windows [cmd](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd)
+ - Windows [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/overview?view=powershell-7.4)
+ - [PowerShell primer](https://www.howtogeek.com/devops/how-to-get-started-with-learning-powershell/)
+ - [Notepad++](https://notepad-plus-plus.org/)
diff --git a/schema/tables/registry.yml b/schema/tables/registry.yml
new file mode 100644
index 0000000000..1397a5ded0
--- /dev/null
+++ b/schema/tables/registry.yml
@@ -0,0 +1,40 @@
+name: registry
+description: The Windows Registry is a database that stores Windows application data and low-level Windows settings like driver, security, service, system and user information. The `registry` osquery table expresses the data in the Windows Registry.
+examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
+ This query returns the date a Windows Host was enrolled in Fleet:
+
+ ```
+ SELECT strftime('%Y-%m-%d %H:%M:%S', mtime, 'unixepoch') AS enroll_time FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments\%%\DeviceEnroller';
+ ```
+
+ This query returns the state of the configurable profiles (i.e., domain, public, standard) in the Windows firewall settings (a value of 1 means the firewall is enabled for the profile):
+
+ ```
+ WITH profiles AS (
+ SELECT SPLIT(KEY, '\', 7) AS enabled,name,data,'profile' AS grpkey
+ FROM registry r
+ WHERE r.path IN (
+ '\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\DomainProfile\EnableFirewall',
+ '\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\PublicProfile\EnableFirewall',
+ '\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\EnableFirewall'
+ )
+ ),
+ firewall AS (
+ SELECT
+ MAX(CASE WHEN enabled='DomainProfile' THEN DATA END) AS domain_enabled,
+ MAX(CASE WHEN enabled='PublicProfile' THEN DATA END) AS public_enabled,
+ MAX(CASE WHEN enabled='StandardProfile' THEN DATA END) AS standard_enabled
+ FROM profiles
+ GROUP BY grpkey
+ )
+ SELECT *
+ FROM firewall;
+ ```
+notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
+ The `registry` table is ideal for use in Fleet policies and queries because of the critical operating system and application data stored in the Windows Registry.
+
+ Links:
+
+ - [Windows Registry](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry)
+ - [Fleet Windows MDM Setup](https://fleetdm.com/guides/windows-mdm-setup)
+ - [Windows Firewall](https://learn.microsoft.com/en-us/windows/security/operating-system-security/network-security/windows-firewall/)
diff --git a/schema/tables/safari_extensions.yml b/schema/tables/safari_extensions.yml
index 68ab38ddd7..ea0d561b39 100644
--- a/schema/tables/safari_extensions.yml
+++ b/schema/tables/safari_extensions.yml
@@ -1,11 +1,25 @@
name: safari_extensions
-description: Installed Safari browser extensions (plugins).
+description: Safari extensions add functionality to Safari.app, the native web browser in macOS. The `safari_extensions` table collects all Safari extensions installed on a Mac.
columns:
- name: uid
examples: |-
+ Collect Safari extensions for all Mac users:
+
```
SELECT * FROM users CROSS JOIN safari_extensions USING (uid);
```
notes: |-
- - Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
- - Includes installed extensions for all system users.
+ Because Safari data is intentionally isolated for each macOS user to maintain privacy, this query requires a `JOIN` operation.
+
+ Query explanation:
+
+ - The `safari_extensions` table has a row for each installed extension
+ - Each row has a column with the `uid` of the user who installed the extension
+ - Each `uid` from the `safari_extensions` table is matched in the `users` table to collect Safari extensions in the output data for all user accounts on the Mac by the `JOIN`
+
+ Links:
+
+ - Apple dcoumentaion on Safari Extensions: https://support.apple.com/en-us/102343
+ - CROSS JOIN SQLite tutorial: https://www.sqlitetutorial.net/sqlite-cross-join/
+ - [Fleet documentation on joining against the `users` table](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
+ - Fleet users table: https://fleetdm.com/tables/users
diff --git a/schema/tables/scheduled_tasks.yml b/schema/tables/scheduled_tasks.yml
new file mode 100644
index 0000000000..c20e612c09
--- /dev/null
+++ b/schema/tables/scheduled_tasks.yml
@@ -0,0 +1,13 @@
+name: scheduled_tasks
+description: |- # (required) string - The description for this table. Note: this field supports Markdown
+ The Windows Task Scheduler tracks and performs automated tasks on a Windows device. The `scheduled_tasks` table collects the data from the Windows Task Scheduler.
+examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
+ This query collects all tasks that are enabled but have not run:
+
+ ```
+ SELECT * FROM scheduled_tasks WHERE enabled='1' AND last_run_message='The task has not yet run.';
+ ```
+notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
+ Many automated tasks are added to the Task Scheduler by Windows itself, however, administrators can also customize the Task Scheduler. Scheduled tasks are analogous to Launch Daemons and Launch Agents used on Linux or macOS. Because automation is a potential vector for malicious activity, monitoring the Windows Task Scheduler may be critical in an enterprise environment.
+
+ [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler)
diff --git a/schema/tables/sip_config.yml b/schema/tables/sip_config.yml
index a783dfbce3..2fff0d92a9 100644
--- a/schema/tables/sip_config.yml
+++ b/schema/tables/sip_config.yml
@@ -1,7 +1,27 @@
name: sip_config
+description: macOS System Integrity Protection (SIP) protects the Mac by preventing the execution of unauthorized code. The `sip_config` osquery table collects the current SIP status of a Mac.
examples: |-
- View the status of System Integrity Protection.
+ Basic query:
```
- SELECT config_flag, enabled FROM sip_config WHERE config_flag='sip';
+ SELECT * FROM sip_config;
```
+
+ This query displays the current SIP status (SIP is enabled if the value=1):
+
+ ```
+ SELECT enabled FROM sip_config WHERE config_flag='sip';
+ ```
+notes: |-
+ SIP:
+ - automatically authorizes apps users download from the App Store
+ - authorizes apps developers notarize and distribute directly to users
+ - prevents launching of other apps unless users or administrators modify "Gatekeeper" settings
+
+ Organizations that develop software for Apple operating systems on the Mac may allow users to disable SIP. Because SIP is a basic and critical macOS security protection it is important to monitor SIP status on Hosts.
+
+ Links:
+
+ - [About System Integrity Protection](https://support.apple.com/en-us/102149)
+ - [Disabling and Enabling System Integrity Protection](https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection/)
+ - [Gatekeeper and runtime protection in macOS](https://support.apple.com/guide/security/gatekeeper-and-runtime-protection-sec5599b66df/web)
diff --git a/schema/tables/software_update.yml b/schema/tables/software_update.yml
index 9ac1ab9025..b853acbd27 100644
--- a/schema/tables/software_update.yml
+++ b/schema/tables/software_update.yml
@@ -1,12 +1,29 @@
name: software_update
+description: The `software_update` table displays the number of updates available from Apple's Software Update service on a Mac.
platforms:
- darwin
-description: Information about available Apple software updates.
+examples: |-
+ Basic query:
+
+ ```
+ SELECT * FROM software_update;
+ ```
columns:
- name: software_update_required
type: integer
required: false
description: |-
- If true, means one of the Apple softwares installed on this machine has a new available upgrade.
-notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+ A value of 0 means no updates are available. Any other integer represents the number of updates available.
+notes: |-
+ This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+
+ Available updates on a Mac can be displayed in the macOS Graphical User Interface (GUI) by clicking on the Apple menu and then selecting “System Settings”. In the System Settings.app, click General > Software Update.
+
+ Apple Software Updates can also be listed in Terminal with the following command:
+
+ ```
+ softwareupdate --list --verbose
+ ```
+
+ [Update Your Apple Software](https://support.apple.com/guide/personal-safety/update-your-apple-software-ips4930e3486/web)
evented: false
diff --git a/scripts/mdm/linux/linux-unlock.sh b/scripts/mdm/linux/linux-unlock.sh
index 2122fb837b..e981d4ce44 100644
--- a/scripts/mdm/linux/linux-unlock.sh
+++ b/scripts/mdm/linux/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/scripts/mdm/windows/windows-remove-fleetd.ps1 b/scripts/mdm/windows/windows-remove-fleetd.ps1
index f532843c09..77a420781d 100644
--- a/scripts/mdm/windows/windows-remove-fleetd.ps1
+++ b/scripts/mdm/windows/windows-remove-fleetd.ps1
@@ -1,5 +1,5 @@
-function Test-Administrator
-{
+function Test-Administrator
+{
[OutputType([bool])]
param()
process {
@@ -58,11 +58,11 @@ function Force-Remove-Orbit {
#Remove HKLM registry entries
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse -ErrorAction "SilentlyContinue" | Where-Object {($_.ValueCount -gt 0)} | ForEach-Object {
-
+
# Filter for osquery entries
- $properties = Get-ItemProperty $_.PSPath -ErrorAction "SilentlyContinue" | Where-Object {($_.DisplayName -eq "Fleet osquery")}
+ $properties = Get-ItemProperty $_.PSPath -ErrorAction "SilentlyContinue" | Where-Object {($_.DisplayName -eq "Fleet osquery")}
if ($properties) {
-
+
#Remove Registry Entries
$regKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + $_.PSChildName
@@ -72,12 +72,12 @@ function Force-Remove-Orbit {
}
}
}
- catch {
+ catch {
Write-Host "There was a problem running Force-Remove-Orbit"
Write-Host "$(Resolve-Error-Detailed)"
return $false
}
-
+
return $true
}
@@ -92,19 +92,41 @@ function Main {
Write-Host "About to uninstall fleetd..."
- if (Force-Remove-Orbit) {
- Write-Host "fleetd was uninstalled."
- Exit 0
+ if ($mode -eq "remove") {
+ # "remove" is received as argument to the script when called as the
+ # sub-process that will actually remove the fleet agent.
+
+ # sleep to give time to fleetd to send the script results to Fleet
+ Start-Sleep -Seconds 20
+ if (Force-Remove-Orbit) {
+ Write-Host "fleetd was uninstalled."
+ Exit 0
+ } else {
+ Write-Host "There was a problem uninstalling fleetd."
+ Exit -1
+ }
} else {
- Write-Host "There was a problem uninstalling fleetd."
- Exit -1
+ # when this script is executed from fleetd, it does not immediately
+ # remove the agent. Instead, it starts a new detached process that
+ # will do the actual removal. This is done to avoid the agent being
+ # killed by the removal process, which prevents it from sending the
+ # script execution results to Fleet, causing the script to remain
+ # "Pending" and being re-executed when/if the host reinstalls the
+ # agent.
+ #
+ # See https://github.com/fleetdm/fleet/issues/19197#issuecomment-2150020270
+ $execName = $MyInvocation.ScriptName
+ $proc = Start-Process -PassThru -FilePath "powershell" -WindowStyle Hidden -ArgumentList "-MTA", "-ExecutionPolicy", "Bypass", "-File", "$execName remove"
+ Start-Sleep -Seconds 5 # give time to process to start running
+ Write-Host "Removal process started: $($proc.Id)."
}
} catch {
- Write-Host "Errorr: Entry point"
+ Write-Host "Errorr: Entry point"
Write-Host "$(Resolve-Error-Detailed)"
Exit -1
}
}
+$mode = $args[0]
$null = Main
diff --git a/server/contexts/logging/logging.go b/server/contexts/logging/logging.go
index 91b1b2db6d..8ef8236a27 100644
--- a/server/contexts/logging/logging.go
+++ b/server/contexts/logging/logging.go
@@ -9,9 +9,9 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
kithttp "github.com/go-kit/kit/transport/http"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
type key int
diff --git a/server/contexts/logging/logging_test.go b/server/contexts/logging/logging_test.go
index b43508e218..b17767b4a8 100644
--- a/server/contexts/logging/logging_test.go
+++ b/server/contexts/logging/logging_test.go
@@ -8,7 +8,7 @@ import (
"strings"
"testing"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
)
diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go
index 90764b32a6..f7267c36ba 100644
--- a/server/datastore/mysql/activities.go
+++ b/server/datastore/mysql/activities.go
@@ -231,14 +231,31 @@ func (ds *Datastore) MarkActivitiesAsStreamed(ctx context.Context, activityIDs [
// software to install, etc.) and provides a unified view of those upcoming
// tasks.
func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {
+ // NOTE: Be sure to update both the count (here) and list statements (below)
+ // if the query condition is modified.
countStmts := []string{
- `SELECT COUNT(*) c FROM host_script_results WHERE host_id = :host_id AND exit_code IS NULL`,
- `SELECT COUNT(*) c FROM host_software_installs WHERE host_id = :host_id AND pre_install_query_output IS NULL AND install_script_exit_code IS NULL`,
+ `SELECT
+ COUNT(*) c
+ FROM host_script_results
+ WHERE host_id = :host_id AND
+ exit_code IS NULL AND
+ (sync_request = 0 OR created_at >= DATE_SUB(NOW(), INTERVAL :max_wait_time SECOND))`,
+ `SELECT
+ COUNT(*) c
+ FROM host_software_installs
+ WHERE host_id = :host_id AND
+ pre_install_query_output IS NULL AND
+ install_script_exit_code IS NULL`,
}
var count uint
countStmt := `SELECT SUM(c) FROM ( ` + strings.Join(countStmts, " UNION ALL ") + ` ) AS counts`
- countStmt, args, err := sqlx.Named(countStmt, map[string]any{"host_id": hostID})
+
+ seconds := int(scripts.MaxServerWaitTime.Seconds())
+ countStmt, args, err := sqlx.Named(countStmt, map[string]any{
+ "host_id": hostID,
+ "max_wait_time": seconds,
+ })
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "build count query from named args")
}
@@ -249,7 +266,8 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
return []*fleet.Activity{}, &fleet.PaginationMetadata{}, nil
}
- // NOTE: Be sure to update both the count and list statements if the list query is modified
+ // NOTE: Be sure to update both the count (above) and list statements (below)
+ // if the query condition is modified.
listStmts := []string{
// list pending scripts
`SELECT
@@ -318,7 +336,6 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
`, softwareInstallerHostStatusNamedQuery("hsi", "")),
}
- seconds := int(scripts.MaxServerWaitTime.Seconds())
listStmt := `
SELECT
uuid,
diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go
index aad9419485..e91a825227 100644
--- a/server/datastore/mysql/activities_test.go
+++ b/server/datastore/mysql/activities_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"time"
+ "github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
@@ -414,8 +415,17 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
sw2Meta, err := ds.GetSoftwareInstallerMetadataByID(ctx, sw2)
require.NoError(t, err)
+ // create a sync script request for h1 that has been pending for > MaxWaitTime, will not show up
+ hsr, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptContents: "sync", UserID: &u.ID, SyncRequest: true})
+ require.NoError(t, err)
+ hSyncExpired := hsr.ExecutionID
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, "UPDATE host_script_results SET created_at = ? WHERE execution_id = ?", time.Now().Add(-(scripts.MaxServerWaitTime + time.Minute)), hSyncExpired)
+ return err
+ })
+
// create some script requests for h1
- hsr, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr1.ID, ScriptContents: scr1.ScriptContents, UserID: &u.ID})
+ hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr1.ID, ScriptContents: scr1.ScriptContents, UserID: &u.ID})
require.NoError(t, err)
h1A := hsr.ExecutionID
hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptID: &scr2.ID, ScriptContents: scr2.ScriptContents, UserID: &u.ID})
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index 4d2baf7dba..142cd81294 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -1840,6 +1840,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now())
hosts = append(hosts, h)
+ nanoEnroll(t, ds, h, false)
}
// create somes config profiles for no team
@@ -2116,6 +2117,7 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now())
hosts = append(hosts, h)
+ nanoEnroll(t, ds, h, false)
}
// create somes config profiles for no team
@@ -4603,14 +4605,14 @@ func testLockUnlockWipeMacOS(t *testing.T, ds *Datastore) {
require.NoError(t, err)
checkLockWipeState(t, status, false, true, false, false, false, false)
- // request an unlock, to make it pending unlock
+ // request an unlock. This is a NOOP for Apple MDM.
err = ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC())
require.NoError(t, err)
- // it is now locked pending unlock
+ // it is still locked
status, err = ds.GetHostLockWipeStatus(ctx, host)
require.NoError(t, err)
- checkLockWipeState(t, status, false, true, false, true, false, false)
+ checkLockWipeState(t, status, false, true, false, false, false, false)
// execute CleanMacOSMDMLock to simulate successful unlock
err = ds.CleanMacOSMDMLock(ctx, host.UUID)
diff --git a/server/datastore/mysql/config.go b/server/datastore/mysql/config.go
index f198a57618..6412034cf9 100644
--- a/server/datastore/mysql/config.go
+++ b/server/datastore/mysql/config.go
@@ -4,7 +4,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server/config"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/ngrok/sqlmw"
)
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 07d80885de..2e0e841ff0 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -17,6 +17,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
@@ -500,7 +501,6 @@ var hostRefs = []string{
"host_updates",
"host_disk_encryption_keys",
"host_software_installed_paths",
- "host_script_results",
"query_results",
"host_activities",
"host_mdm_actions",
@@ -526,6 +526,13 @@ var additionalHostRefsByUUID = map[string]string{
"host_mdm_apple_declarations": "host_uuid",
}
+// additionalHostRefsSoftDelete are tables that reference a host but for which
+// the rows are not deleted when the host is deleted, only a soft delete is
+// performed by setting a timestamp column to the current time.
+var additionalHostRefsSoftDelete = map[string]string{
+ "host_script_results": "host_deleted_at",
+}
+
func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {
delHostRef := func(tx sqlx.ExtContext, table string) error {
_, err := tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE host_id=?`, table), hid)
@@ -568,6 +575,13 @@ func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {
}
}
+ // perform the soft-deletion of host-referencing tables
+ for table, col := range additionalHostRefsSoftDelete {
+ if _, err := tx.ExecContext(ctx, fmt.Sprintf("UPDATE `%s` SET `%s` = NOW() WHERE host_id=?", table, col), hid); err != nil {
+ return ctxerr.Wrapf(ctx, err, "soft-deleting %s for host id %d", table, hid)
+ }
+ }
+
return nil
})
}
@@ -750,9 +764,54 @@ func queryStatsToScheduledQueryStats(queriesStats []fleet.QueryStats, packName s
return scheduledQueriesStats
}
+func winHostConnectedToFleetCond(aliasedCols []string, lenPlaceholders int) string {
+ in := strings.Repeat("?,", lenPlaceholders)
+ in += strings.Join(aliasedCols, ",")
+ in = strings.Trim(in, ",")
+
+ return fmt.Sprintf(`
+ SELECT host_uuid
+ FROM mdm_windows_enrollments
+ WHERE host_uuid IN (%s)
+ AND device_state = '%s'`,
+ in,
+ microsoft_mdm.MDMDeviceStateEnrolled)
+}
+
+func appleHostConnectedToFleetCond(aliasedCols []string, lenPlaceholders int) string {
+ in := strings.Repeat("?,", lenPlaceholders)
+ in += strings.Join(aliasedCols, ",")
+ in = strings.Trim(in, ",")
+
+ return fmt.Sprintf(`
+ SELECT id
+ FROM nano_enrollments
+ WHERE id IN (%s)
+ AND enabled = 1
+ AND type = 'Device'`, in)
+}
+
+var caseConnectedToFleet = `
+CASE
+ WHEN h.platform = 'windows' THEN (
+ SELECT CASE WHEN EXISTS (` + winHostConnectedToFleetCond([]string{"h.uuid"}, 0) + `)
+ THEN CAST(TRUE AS JSON)
+ ELSE CAST(FALSE AS JSON)
+ END
+ )
+ WHEN h.platform IN ('ios', 'ipados', 'darwin') THEN (
+ SELECT CASE WHEN EXISTS (` + appleHostConnectedToFleetCond([]string{"h.uuid"}, 0) + `)
+ THEN CAST(TRUE AS JSON)
+ ELSE CAST(FALSE AS JSON)
+ END
+ )
+ ELSE CAST(FALSE AS JSON)
+END
+`
+
// hostMDMSelect is the SQL fragment used to construct the JSON object
// of MDM host data. It assumes that hostMDMJoin is included in the query.
-const hostMDMSelect = `,
+var hostMDMSelect = `,
JSON_OBJECT(
'enrollment_status',
CASE
@@ -775,11 +834,11 @@ const hostMDMSelect = `,
END,
'encryption_key_available',
CASE
- /* roberto: this is the only way I have found for MySQL to
- * return true and false instead of 0 and 1 in the JSON, the
- * unmarshaller was having problems converting int values to
- * booleans.
- */
+ /* roberto: this is the only way I have found for MySQL to
+ * return true and false instead of 0 and 1 in the JSON, the
+ * unmarshaller was having problems converting int values to
+ * booleans.
+ */
WHEN hdek.decryptable IS NULL OR hdek.decryptable = 0 THEN CAST(FALSE AS JSON)
ELSE CAST(TRUE AS JSON)
END,
@@ -788,6 +847,8 @@ const hostMDMSelect = `,
WHEN hdek.host_id IS NULL THEN -1
ELSE hdek.decryptable
END,
+ 'connected_to_fleet',
+ ` + caseConnectedToFleet + `,
'name', hmdm.name
) mdm_host_data
`
@@ -1069,6 +1130,18 @@ func (ds *Datastore) applyHostFilters(
whereParams = append(whereParams, *opt.LowDiskSpaceFilter)
}
+ connectedToFleetJoin := ""
+ if opt.ConnectedToFleetFilter != nil && *opt.ConnectedToFleetFilter ||
+ opt.OSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsDiskEncryptionFilter.IsValid() ||
+ opt.OSSettingsDiskEncryptionFilter.IsValid() {
+ connectedToFleetJoin = `
+ LEFT JOIN nano_enrollments ne ON ne.id = h.uuid AND ne.enabled = 1 AND ne.type = 'Device'
+ LEFT JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid AND mwe.device_state = ?`
+ whereParams = append(whereParams, microsoft_mdm.MDMDeviceStateEnrolled)
+ }
+
sqlStmt += fmt.Sprintf(
`FROM hosts h
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
@@ -1082,7 +1155,8 @@ func (ds *Datastore) applyHostFilters(
%s
%s
%s
- %s
+ %s
+ %s
WHERE TRUE AND %s AND %s AND %s AND %s
`,
@@ -1095,6 +1169,7 @@ func (ds *Datastore) applyHostFilters(
operatingSystemJoin,
munkiJoin,
displayNameJoin,
+ connectedToFleetJoin,
// Conditions
ds.whereFilterHostsByTeams(filter, "h"),
@@ -1108,6 +1183,7 @@ func (ds *Datastore) applyHostFilters(
sqlStmt, whereParams = filterHostsByTeam(sqlStmt, opt, whereParams)
sqlStmt, whereParams = filterHostsByPolicy(sqlStmt, opt, whereParams)
sqlStmt, whereParams = filterHostsByMDM(sqlStmt, opt, whereParams)
+ sqlStmt, whereParams = filterHostsByConnectedToFleet(sqlStmt, opt, whereParams)
var err error
sqlStmt, whereParams, err = filterHostsByMacOSSettingsStatus(sqlStmt, opt, whereParams)
if err != nil {
@@ -1193,6 +1269,13 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{
return sql, params
}
+func filterHostsByConnectedToFleet(sql string, opt fleet.HostListOptions, params []any) (string, []any) {
+ if opt.ConnectedToFleetFilter != nil && *opt.ConnectedToFleetFilter {
+ sql += "AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL)"
+ }
+ return sql, params
+}
+
func filterHostsByOS(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
if opt.OSIDFilter != nil {
sql += ` AND hos.os_id = ?`
@@ -1241,7 +1324,8 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par
return sql, params, nil
}
- whereStatus := ""
+ // ensure the host has MDM turned on
+ whereStatus := " AND ne.id IS NOT NULL"
// macOS settings filter is not compatible with the "all teams" option so append the "no
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
if opt.TeamFilter == nil {
@@ -1281,7 +1365,7 @@ func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOption
subquery, subqueryParams = subqueryFileVaultRemovingEnforcement()
}
- return sql + fmt.Sprintf(` AND EXISTS (%s)`, subquery), append(params, subqueryParams...)
+ return sql + fmt.Sprintf(` AND EXISTS (%s) AND ne.id IS NOT NULL`, subquery), append(params, subqueryParams...)
}
func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostListOptions, params []interface{}, isDiskEncryptionEnabled bool) (string, []interface{}, error) {
@@ -1297,7 +1381,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
// or are servers. Similar logic could be applied to macOS hosts but is not included in this
// current implementation.
- sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados')`
+ sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados') AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL) `
if opt.TeamFilter == nil {
// OS settings filter is not compatible with the "all teams" option so append the "no team"
// filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
@@ -1313,11 +1397,12 @@ OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (
return "", nil, err
}
whereMacOS += ` = ?`
+ // ensure the host has MDM turned on
paramsMacOS = append(paramsMacOS, opt.OSSettingsFilter)
// construct the WHERE for windows
- whereWindows = `hmdm.name = ? AND hmdm.enrolled = 1 AND hmdm.is_server = 0`
- paramsWindows := []interface{}{fleet.WellKnownMDMFleet}
+ whereWindows = `hmdm.is_server = 0`
+ paramsWindows := []any{}
subqueryFailed, paramsFailed, err := subqueryHostsMDMWindowsOSSettingsStatusFailed()
if err != nil {
return "", nil, err
@@ -2179,6 +2264,16 @@ type hostWithMDMInfo struct {
DEPProfileAssignStatus *string `db:"dep_profile_assign_status"`
}
+const hostWithMDMInfoSelect = `
+ hm.host_id,
+ hm.enrolled,
+ hm.server_url,
+ hm.installed_from_dep,
+ COALESCE(hm.is_server, false) AS is_server,
+ hm.mdm_id,
+ COALESCE(mdms.name, ?) AS name,
+ hdep.assign_profile_response AS dep_profile_assign_status`
+
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
// If the node key is invalid it returns a NotFoundError.
func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) (*fleet.Host, error) {
@@ -2224,19 +2319,12 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
h.policy_updated_at,
h.public_ip,
h.orbit_node_key,
- hm.host_id,
- hm.enrolled,
- hm.server_url,
- hm.installed_from_dep,
- hm.mdm_id,
- COALESCE(hm.is_server, false) AS is_server,
- COALESCE(mdms.name, ?) AS name,
COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested,
- COALESCE(hdek.decryptable, false) as encryption_key_available,
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
hd.encrypted as disk_encryption_enabled,
- t.name as team_name,
- hdep.assign_profile_response AS dep_profile_assign_status
+ COALESCE(hdek.decryptable, false) as encryption_key_available,
+ t.name as team_name,` +
+ hostWithMDMInfoSelect + `
FROM
hosts h
LEFT OUTER JOIN
@@ -2342,15 +2430,8 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
COALESCE(hd.gigs_total_disk_space, 0) as gigs_total_disk_space,
- hm.host_id,
- hm.enrolled,
- hm.server_url,
- hm.installed_from_dep,
- hm.mdm_id,
- COALESCE(hm.is_server, false) AS is_server,
- COALESCE(mdms.name, ?) AS name,
- IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
- hdep.assign_profile_response AS dep_profile_assign_status
+ IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,` +
+ hostWithMDMInfoSelect + `
FROM
host_device_auth hda
INNER JOIN
@@ -2617,13 +2698,8 @@ SELECT
h.policy_updated_at,
h.refetch_requested,
h.refetch_critical_queries_until,
- hm.host_id,
- hm.enrolled,
- hm.server_url,
- hm.installed_from_dep,
- hm.mdm_id,
- COALESCE(hm.is_server, false) AS is_server,
- COALESCE(mdms.name, ?) AS name
+ IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
+ `+hostWithMDMInfoSelect+`
FROM
hosts h
LEFT OUTER JOIN
@@ -2634,6 +2710,10 @@ LEFT OUTER JOIN
mobile_device_management_solutions mdms
ON
hm.mdm_id = mdms.id
+LEFT OUTER JOIN
+ host_dep_assignments hdep
+ON
+ hdep.host_id = h.id AND hdep.deleted_at IS NULL
WHERE h.uuid IN (?) AND %s
`, ds.whereFilterHostsByTeams(filter, "h"),
)
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index 634b50d61d..0217329d9f 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -775,6 +775,7 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...)
hosts = append(hosts, h)
+ nanoEnroll(t, ds, h, false)
}
userFilter := fleet.TeamFilter{User: test.UserAdmin}
@@ -3285,6 +3286,7 @@ func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore)
})
require.NoError(t, err)
hosts = append(hosts, h)
+ nanoEnroll(t, ds, h, false)
}
// set up data
@@ -4255,7 +4257,7 @@ func testHostsIncludesScheduledQueriesInPackStats(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage(json.RawMessage(`{"foo": "baz"}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
hostResult, err = ds.Host(context.Background(), host.ID)
@@ -6038,7 +6040,6 @@ func testHostsLoadHostByDeviceAuthToken(t *testing.T, ds *Datastore) {
require.NotNil(t, loadSimple.MDMInfo)
require.Equal(t, hSimple.ID, loadSimple.MDMInfo.HostID)
require.True(t, loadSimple.IsOsqueryEnrolled())
- require.False(t, loadSimple.MDMInfo.IsPendingDEPFleetEnrollment())
// create a host that will be pending enrollment in Fleet MDM
hFleet := createHostWithDeviceToken("fleet")
@@ -6051,7 +6052,6 @@ func testHostsLoadHostByDeviceAuthToken(t *testing.T, ds *Datastore) {
require.NotNil(t, loadFleet.MDMInfo)
require.Equal(t, hFleet.ID, loadFleet.MDMInfo.HostID)
require.True(t, loadFleet.IsOsqueryEnrolled())
- require.True(t, loadFleet.MDMInfo.IsPendingDEPFleetEnrollment())
require.False(t, loadFleet.MDMInfo.IsServer)
// force its is_server mdm field to NULL, should be same as false
@@ -6647,6 +6647,12 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
require.NoError(t, err, tbl)
require.True(t, ok, "table: %s", tbl)
}
+ for tbl, col := range additionalHostRefsSoftDelete {
+ var ok bool
+ err = ds.writer(context.Background()).Get(&ok, fmt.Sprintf("SELECT 1 FROM %s WHERE host_id = ? AND %s IS NULL", tbl, col), host.ID)
+ require.NoError(t, err, tbl)
+ require.True(t, ok, "table: %s", tbl)
+ }
err = ds.DeleteHosts(context.Background(), []uint{host.ID})
require.NoError(t, err)
@@ -6664,6 +6670,12 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
require.True(t, err == nil || errors.Is(err, sql.ErrNoRows), "table: %s", tbl)
require.False(t, ok, "table: %s", tbl)
}
+ for tbl, col := range additionalHostRefsSoftDelete {
+ var ok bool
+ err = ds.writer(context.Background()).Get(&ok, fmt.Sprintf("SELECT 1 FROM %s WHERE host_id = ? AND %s IS NULL", tbl, col), host.ID)
+ require.True(t, err == nil || errors.Is(err, sql.ErrNoRows), "table: %s", tbl)
+ require.False(t, ok, "table: %s", tbl) // the soft-delete column is not null anymore, so no row is found
+ }
}
func testHostIDsByOSVersion(t *testing.T, ds *Datastore) {
@@ -7500,8 +7512,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NotNil(t, loadSimple.MDMInfo)
require.Equal(t, hSimple.ID, loadSimple.MDMInfo.HostID)
require.True(t, loadSimple.IsOsqueryEnrolled())
- require.False(t, loadSimple.MDMInfo.IsPendingDEPFleetEnrollment())
- require.False(t, loadSimple.IsEligibleForDEPMigration())
+ require.False(t, loadSimple.IsEligibleForDEPMigration(false))
// create a host that will be pending enrollment in Fleet MDM
hFleet := createOrbitHost("fleet")
@@ -7514,10 +7525,9 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NotNil(t, loadFleet.MDMInfo)
require.Equal(t, hFleet.ID, loadFleet.MDMInfo.HostID)
require.True(t, loadFleet.IsOsqueryEnrolled())
- require.True(t, loadFleet.MDMInfo.IsPendingDEPFleetEnrollment())
require.False(t, loadFleet.MDMInfo.IsServer)
require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus)
- require.False(t, loadFleet.IsEligibleForDEPMigration())
+ require.False(t, loadFleet.IsEligibleForDEPMigration(false))
// force its is_server mdm field to NULL, should be same as false
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@@ -7528,7 +7538,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, hFleet.ID, loadFleet.ID)
require.False(t, loadFleet.MDMInfo.IsServer)
- require.False(t, loadFleet.IsEligibleForDEPMigration())
+ require.False(t, loadFleet.IsEligibleForDEPMigration(false))
// fill in disk encryption information
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true))
@@ -7542,7 +7552,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
require.True(t, *loadFleet.DiskEncryptionEnabled)
- require.False(t, loadFleet.IsEligibleForDEPMigration())
+ require.False(t, loadFleet.IsEligibleForDEPMigration(false))
require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus)
// simulate the device being assigned to Fleet in ABM
@@ -9055,7 +9065,7 @@ func testHostsAddToTeamCleansUpTeamQueryResults(t *testing.T, ds *Datastore) {
h4Global0Results,
h4Query1Results,
} {
- err = ds.OverwriteQueryResultRows(ctx, results)
+ err = ds.OverwriteQueryResultRows(ctx, results, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
}
diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go
index b9602cdabd..cf0904fc48 100644
--- a/server/datastore/mysql/labels.go
+++ b/server/datastore/mysql/labels.go
@@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/jmoiron/sqlx"
)
@@ -619,6 +620,16 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea
query += softwareStatusJoin
}
+ if opt.ConnectedToFleetFilter != nil && *opt.ConnectedToFleetFilter ||
+ opt.OSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsDiskEncryptionFilter.IsValid() {
+ query += `
+ LEFT JOIN nano_enrollments ne ON ne.id = h.uuid AND ne.enabled = 1 AND ne.type = 'Device'
+ LEFT JOIN mdm_windows_enrollments mwe ON mwe.host_uuid = h.uuid AND mwe.device_state = ?`
+ joinParams = append(joinParams, microsoft_mdm.MDMDeviceStateEnrolled)
+ }
+
query += fmt.Sprintf(` WHERE lm.label_id = ? AND %s `, ds.whereFilterHostsByTeams(filter, "h"))
whereParams = append(whereParams, lid)
diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go
index 70edb43f13..914ada4f89 100644
--- a/server/datastore/mysql/labels_test.go
+++ b/server/datastore/mysql/labels_test.go
@@ -495,6 +495,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, 2) // h1 and h2
// test team filter in combination with macos settings filter
+ nanoEnroll(t, db, h1, false)
require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: "a" + uuid.NewString(),
@@ -513,6 +514,7 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
+ nanoEnroll(t, db, h2, false)
require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: "a" + uuid.NewString(),
@@ -1065,6 +1067,7 @@ func testListHostsInLabelDiskEncryptionStatus(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
hosts = append(hosts, h)
+ nanoEnroll(t, ds, h, false)
}
// set up data
@@ -1426,6 +1429,7 @@ func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) {
// add two hosts to MDM to enforce disk encryption, fleet doesn't enforce settings on centos so h3 is not included
for _, h := range []*fleet.Host{h1, h2} {
require.NoError(t, db.SetOrUpdateMDMData(context.Background(), h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+ nanoEnroll(t, db, h, false)
}
// add disk encryption key for h1
require.NoError(t, db.SetOrUpdateHostDiskEncryptionKey(context.Background(), h1.ID, "test-key", "", ptr.Bool(true)))
diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go
index 96e5d09138..864f37bc54 100644
--- a/server/datastore/mysql/mdm.go
+++ b/server/datastore/mysql/mdm.go
@@ -11,7 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
@@ -1213,3 +1213,57 @@ func getTableAndColumnNameForHostMDMProfileUUID(profUUID string) (table, column
return "", "", fmt.Errorf("invalid profile UUID prefix %s", profUUID)
}
}
+
+func (ds *Datastore) AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
+ var (
+ appleUUIDs []any
+ winUUIDs []any
+ )
+
+ res := make(map[string]bool, len(hosts))
+ for _, h := range hosts {
+ switch h.Platform {
+ case "darwin", "ipados", "ios":
+ appleUUIDs = append(appleUUIDs, h.UUID)
+ case "windows":
+ winUUIDs = append(winUUIDs, h.UUID)
+ }
+ res[h.UUID] = false
+ }
+
+ setConnectedUUIDs := func(stmtFn func(aliasedCols []string, lenPlaceholders int) string, uuids []any, mp map[string]bool) error {
+ var res []string
+
+ if len(uuids) > 0 {
+ err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, stmtFn(nil, len(uuids)), uuids...)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "retrieving hosts connected to fleet")
+ }
+ }
+
+ for _, uuid := range res {
+ mp[uuid] = true
+ }
+
+ return nil
+ }
+
+ if err := setConnectedUUIDs(appleHostConnectedToFleetCond, appleUUIDs, res); err != nil {
+ return nil, err
+ }
+
+ if err := setConnectedUUIDs(winHostConnectedToFleetCond, winUUIDs, res); err != nil {
+ return nil, err
+ }
+
+ return res, nil
+
+}
+
+func (ds *Datastore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) {
+ mp, err := ds.AreHostsConnectedToFleetMDM(ctx, []*fleet.Host{host})
+ if err != nil {
+ return false, ctxerr.Wrap(ctx, err, "finding if host is connected to Fleet MDM")
+ }
+ return mp[host.UUID], nil
+}
diff --git a/server/datastore/mysql/mdm_test.go b/server/datastore/mysql/mdm_test.go
index ce8e89602f..944ba5ad23 100644
--- a/server/datastore/mysql/mdm_test.go
+++ b/server/datastore/mysql/mdm_test.go
@@ -14,6 +14,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/certauth"
"github.com/fleetdm/fleet/v4/server/ptr"
@@ -46,6 +47,8 @@ func TestMDMShared(t *testing.T) {
{"TestGetHostCertAssociationsToExpire", testSCEPRenewalHelpers},
{"TestSCEPRenewalHelpers", testSCEPRenewalHelpers},
{"TestMDMProfilesSummaryAndHostFilters", testMDMProfilesSummaryAndHostFilters},
+ {"TestIsHostConnectedToFleetMDM", testIsHostConnectedToFleetMDM},
+ {"TestAreHostsConnectedToFleetMDM", testAreHostsConnectedToFleetMDM},
}
for _, c := range cases {
@@ -6122,7 +6125,7 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
- CommonName: "FleetDM Identity",
+ CommonName: "Fleet Identity",
},
NotAfter: notAfter,
// use a random value, just to make sure they're
@@ -6516,9 +6519,25 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
require.NotNil(t, h)
hosts = append(hosts, h)
if p == "darwin" {
+ nanoEnroll(t, ds, h, false)
macHostsByID[h.ID] = h
} else {
winHostsByID[h.ID] = h
+ windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
+ MDMDeviceID: uuid.New().String(),
+ MDMHardwareID: uuid.New().String() + uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
+ MDMDeviceType: "CIMClient_Windows",
+ MDMDeviceName: "DESKTOP-1C3ARC1",
+ MDMEnrollType: "ProgrammaticEnrollment",
+ MDMEnrollUserID: "",
+ MDMEnrollProtoVersion: "5.0",
+ MDMEnrollClientVersion: "10.0.19045.2965",
+ MDMNotInOOBE: false,
+ HostUUID: h.UUID,
+ }
+ err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
+ require.NoError(t, err)
}
require.NoError(
@@ -6710,3 +6729,152 @@ func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
cleanupTables(t)
}
+
+func testAreHostsConnectedToFleetMDM(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ notConnectedMac, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "macos-test",
+ OsqueryHostID: ptr.String("osquery-macos-not-connected"),
+ NodeKey: ptr.String("node-key-macos-not-connected"),
+ UUID: uuid.NewString(),
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ connectedMac, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "macos-test",
+ OsqueryHostID: ptr.String("osquery-macos"),
+ NodeKey: ptr.String("node-key-macos"),
+ UUID: uuid.NewString(),
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ nanoEnroll(t, ds, connectedMac, false)
+
+ notConnectedWin, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "windows-test",
+ OsqueryHostID: ptr.String("osquery-windows-not-connected"),
+ NodeKey: ptr.String("node-key-windows-not-connected"),
+ UUID: uuid.NewString(),
+ Platform: "windows",
+ })
+ require.NoError(t, err)
+
+ connectedWin, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "windows-test",
+ OsqueryHostID: ptr.String("osquery-windows"),
+ NodeKey: ptr.String("node-key-windows"),
+ UUID: uuid.NewString(),
+ Platform: "windows",
+ })
+ require.NoError(t, err)
+
+ windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
+ MDMDeviceID: uuid.New().String(),
+ MDMHardwareID: uuid.New().String() + uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
+ MDMDeviceType: "CIMClient_Windows",
+ MDMDeviceName: "DESKTOP-1C3ARC1",
+ MDMEnrollType: "ProgrammaticEnrollment",
+ MDMEnrollUserID: "",
+ MDMEnrollProtoVersion: "5.0",
+ MDMEnrollClientVersion: "10.0.19045.2965",
+ MDMNotInOOBE: false,
+ HostUUID: connectedWin.UUID,
+ }
+ err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
+ require.NoError(t, err)
+
+ connectedMap, err := ds.AreHostsConnectedToFleetMDM(ctx, []*fleet.Host{
+ notConnectedMac,
+ connectedMac,
+ connectedWin,
+ notConnectedWin,
+ })
+ require.NoError(t, err)
+ require.Equal(t, map[string]bool{
+ notConnectedMac.UUID: false,
+ connectedMac.UUID: true,
+ connectedWin.UUID: true,
+ notConnectedWin.UUID: false,
+ }, connectedMap)
+
+ linuxHost, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "linux-test",
+ OsqueryHostID: ptr.String("osquery-linux"),
+ NodeKey: ptr.String("node-key-linux"),
+ UUID: uuid.NewString(),
+ Platform: "linux",
+ })
+ require.NoError(t, err)
+ connectedMap, err = ds.AreHostsConnectedToFleetMDM(ctx, []*fleet.Host{
+ notConnectedMac,
+ connectedMac,
+ connectedWin,
+ notConnectedWin,
+ linuxHost,
+ })
+ require.NoError(t, err)
+ require.Equal(t, map[string]bool{
+ notConnectedMac.UUID: false,
+ connectedMac.UUID: true,
+ connectedWin.UUID: true,
+ notConnectedWin.UUID: false,
+ linuxHost.UUID: false,
+ }, connectedMap)
+}
+
+func testIsHostConnectedToFleetMDM(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ macH, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "macos-test",
+ OsqueryHostID: ptr.String("osquery-macos"),
+ NodeKey: ptr.String("node-key-macos"),
+ UUID: uuid.NewString(),
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ connected, err := ds.IsHostConnectedToFleetMDM(ctx, macH)
+ require.NoError(t, err)
+ require.False(t, connected)
+
+ nanoEnroll(t, ds, macH, false)
+ connected, err = ds.IsHostConnectedToFleetMDM(ctx, macH)
+ require.NoError(t, err)
+ require.True(t, connected)
+
+ windowsH, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: "windows-test",
+ OsqueryHostID: ptr.String("osquery-windows"),
+ NodeKey: ptr.String("node-key-windows"),
+ UUID: uuid.NewString(),
+ Platform: "windows",
+ })
+ require.NoError(t, err)
+ connected, err = ds.IsHostConnectedToFleetMDM(ctx, windowsH)
+ require.NoError(t, err)
+ require.False(t, connected)
+
+ windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
+ MDMDeviceID: uuid.New().String(),
+ MDMHardwareID: uuid.New().String() + uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
+ MDMDeviceType: "CIMClient_Windows",
+ MDMDeviceName: "DESKTOP-1C3ARC1",
+ MDMEnrollType: "ProgrammaticEnrollment",
+ MDMEnrollUserID: "",
+ MDMEnrollProtoVersion: "5.0",
+ MDMEnrollClientVersion: "10.0.19045.2965",
+ MDMNotInOOBE: false,
+ HostUUID: windowsH.UUID,
+ }
+ err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
+ require.NoError(t, err)
+
+ connected, err = ds.IsHostConnectedToFleetMDM(ctx, windowsH)
+ require.NoError(t, err)
+ require.True(t, connected)
+}
diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go
index 15c2b1c142..839532d898 100644
--- a/server/datastore/mysql/microsoft_mdm.go
+++ b/server/datastore/mysql/microsoft_mdm.go
@@ -11,7 +11,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
- "github.com/go-kit/kit/log/level"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
+ "github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
@@ -359,7 +360,8 @@ ON DUPLICATE KEY UPDATE
// if we received a Wipe command result, update the host's status
if wipeCmdUUID != "" {
if err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrollment.HostUUID,
- "wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2")); err != nil {
+ "wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2"), false,
+ ); err != nil {
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
}
}
@@ -416,7 +418,7 @@ func updateMDMWindowsHostProfileStatusFromResponseDB(
WHERE host_uuid = ? AND command_uuid IN (?)`
// grab command UUIDs to find matching entries using `getMatchingHostProfilesStmt`
- commandUUIDs := make([]string, len(payloads))
+ commandUUIDs := make([]string, 0, len(payloads))
// also grab the payloads keyed by the command uuid, so we can easily
// grab the corresponding `Detail` and `Status` from the matching
// command later on.
@@ -960,12 +962,11 @@ SELECT
FROM
hosts h
JOIN host_mdm hmdm ON h.id = hmdm.host_id
- JOIN mobile_device_management_solutions mdms ON hmdm.mdm_id = mdms.id
+ JOIN mdm_windows_enrollments mwe ON h.uuid = mwe.host_uuid
WHERE
- mdms.name = '%s' AND
- hmdm.is_server = 0 AND
- hmdm.enrolled = 1 AND
+ mwe.device_state = '%s' AND
h.platform = 'windows' AND
+ hmdm.is_server = 0 AND
%s
GROUP BY
status`,
@@ -973,7 +974,7 @@ GROUP BY
subqueryPending,
subqueryVerifying,
subqueryVerified,
- fleet.WellKnownMDMFleet,
+ microsoft_mdm.MDMDeviceStateEnrolled,
teamFilter,
)
@@ -1092,13 +1093,12 @@ SELECT
FROM
hosts h
JOIN host_mdm hmdm ON h.id = hmdm.host_id
- JOIN mobile_device_management_solutions mdms ON hmdm.mdm_id = mdms.id
+ JOIN mdm_windows_enrollments mwe ON h.uuid = mwe.host_uuid
%s
WHERE
- mdms.name = '%s' AND
- hmdm.is_server = 0 AND
- hmdm.enrolled = 1 AND
+ mwe.device_state = '%s' AND
h.platform = 'windows' AND
+ hmdm.is_server = 0 AND
%s
GROUP BY
status`,
@@ -1108,7 +1108,7 @@ GROUP BY
bitlockerStatus,
bitlockerStatus,
bitlockerJoin,
- fleet.WellKnownMDMFleet,
+ microsoft_mdm.MDMDeviceStateEnrolled,
teamFilter,
)
diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go
index 6a7be91fd3..e0b5ed0d60 100644
--- a/server/datastore/mysql/microsoft_mdm_test.go
+++ b/server/datastore/mysql/microsoft_mdm_test.go
@@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
+ microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
@@ -55,7 +56,7 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
@@ -293,6 +294,12 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
hosts = append(hosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+
+ if p == "darwin" {
+ nanoEnroll(t, ds, h, false)
+ } else {
+ windowsEnroll(t, ds, h)
+ }
}
t.Run("Disk encryption disabled", func(t *testing.T) {
@@ -571,8 +578,8 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
require.NoError(t, err)
if len(expectedIDs) != len(gotHosts) {
gotIDs := make([]uint, len(gotHosts))
- for _, h := range gotHosts {
- gotIDs = append(gotIDs, h.ID)
+ for i, h := range gotHosts {
+ gotIDs[i] = h.ID
}
require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs))
@@ -640,6 +647,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
// Create some hosts
var hosts []*fleet.Host
+ uuidToDeviceID := map[string]string{}
for i := 0; i < 10; i++ {
p := "windows"
if i >= 5 {
@@ -661,6 +669,9 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
hosts = append(hosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+ if p == "windows" {
+ uuidToDeviceID[h.UUID] = windowsEnroll(t, ds, h)
+ }
}
t.Run("profiles summary accounts for bitlocker status", func(t *testing.T) {
@@ -933,6 +944,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
otherHosts = append(otherHosts, h)
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+ windowsEnroll(t, ds, h)
}
checkExpected(t, nil, expected)
@@ -1040,8 +1052,8 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
}
checkExpected(t, nil, expected)
- // report hosts[3] as not enrolled
- require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[3].ID, false, false, "https://example.com", false, fleet.WellKnownMDMFleet, ""))
+ // unenroll hosts[3]
+ require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[3].UUID]))
// hosts[3] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
@@ -1051,6 +1063,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) {
// report hosts[4] as enrolled to a different MDM
require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[4].ID, false, true, "https://some-other-mdm.example.com", false, "some-other-mdm", ""))
+ require.NoError(t, ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, uuidToDeviceID[hosts[4].UUID]))
// hosts[4] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{otherHosts[1].ID, otherHosts[2].ID, otherHosts[3].ID, otherHosts[4].ID},
@@ -1071,7 +1084,7 @@ func testMDMWindowsInsertCommandForHosts(t *testing.T, ds *Datastore) {
d1 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
@@ -1085,7 +1098,7 @@ func testMDMWindowsInsertCommandForHosts(t *testing.T, ds *Datastore) {
d2 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
@@ -1150,7 +1163,7 @@ func testMDMWindowsGetPendingCommands(t *testing.T, ds *Datastore) {
d := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
@@ -1213,7 +1226,7 @@ func testMDMWindowsCommandResults(t *testing.T, ds *Datastore) {
dev := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: "test-device-id",
MDMHardwareID: "test-hardware-id",
- MDMDeviceState: "ds",
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "dt",
MDMDeviceName: "dn",
MDMEnrollType: "et",
@@ -1276,7 +1289,7 @@ func windowsEnroll(t *testing.T, ds fleet.Datastore, h *fleet.Host) string {
d1 := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
+ MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
diff --git a/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles.go b/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles.go
new file mode 100644
index 0000000000..fd1668b7f1
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles.go
@@ -0,0 +1,53 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240607133721, Down_20240607133721)
+}
+
+func Up_20240607133721(tx *sql.Tx) error {
+
+ // For users that are not running vulnerabilities job, we need to ensure that software_titles are up-to-date
+
+ upsertTitlesStmt := `
+INSERT INTO software_titles (name, source, browser)
+SELECT DISTINCT
+ name,
+ source,
+ browser
+FROM
+ software s
+WHERE
+ NOT EXISTS (SELECT 1 FROM software_titles st WHERE (s.name, s.source, s.browser) = (st.name, st.source, st.browser))
+ON DUPLICATE KEY UPDATE software_titles.id = software_titles.id`
+
+ _, err := tx.Exec(upsertTitlesStmt)
+ if err != nil {
+ return fmt.Errorf("failed to upsert software titles: %w", err)
+ }
+
+ // update title ids for software table entries
+ updateSoftwareStmt := `
+UPDATE
+ software s,
+ software_titles st
+SET
+ s.title_id = st.id
+WHERE
+ (s.name, s.source, s.browser) = (st.name, st.source, st.browser)
+ AND (s.title_id IS NULL OR s.title_id != st.id)`
+
+ _, err = tx.Exec(updateSoftwareStmt)
+ if err != nil {
+ return fmt.Errorf("failed to update software title_id: %w", err)
+ }
+ return nil
+}
+
+func Down_20240607133721(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles_test.go b/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles_test.go
new file mode 100644
index 0000000000..802bfea836
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240607133721_ReconcileSoftwareTitles_test.go
@@ -0,0 +1,53 @@
+package tables
+
+import (
+ "context"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestUp_20240607133721(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ // Insert data into software_titles
+ title1 := execNoErrLastID(t, db, "INSERT INTO software_titles (name, source, browser) VALUES (?, ?, ?)", "sw1", "src1", "")
+
+ // Insert software
+ const insertStmt = `INSERT INTO software
+ (name, version, source, browser, checksum, title_id)
+ VALUES
+ (?, ?, ?, ?, ?, ?)`
+
+ execNoErr(t, db, insertStmt, "sw1", "1.0", "src1", "", "1", title1)
+ execNoErr(t, db, insertStmt, "sw1", "1.0.1", "src1", "", "1a", nil)
+ execNoErr(t, db, insertStmt, "sw2", "2.0", "src2", "", "2", nil)
+ execNoErr(t, db, insertStmt, "sw3", "3.0", "src3", "browser3", "3", nil)
+
+ applyNext(t, db)
+
+ var softwareTitles []fleet.SoftwareTitle
+ require.NoError(t, db.SelectContext(context.Background(), &softwareTitles, `SELECT * FROM software_titles`))
+ require.Len(t, softwareTitles, 3)
+
+ var software []fleet.Software
+ require.NoError(t, db.SelectContext(context.Background(), &software, `SELECT id, name, source, browser, title_id FROM software`))
+ require.Len(t, software, 4)
+
+ for _, sw := range software {
+ require.NotNil(t, sw.TitleID)
+ var found bool
+ for _, title := range softwareTitles {
+ if *sw.TitleID == title.ID {
+ assert.Equal(t, sw.Name, title.Name)
+ assert.Equal(t, sw.Source, title.Source)
+ assert.Equal(t, sw.Browser, title.Browser)
+ found = true
+ break
+ }
+ }
+ assert.True(t, found)
+ }
+
+}
diff --git a/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete.go b/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete.go
new file mode 100644
index 0000000000..e004fd5c12
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete.go
@@ -0,0 +1,22 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240612150059, Down_20240612150059)
+}
+
+func Up_20240612150059(tx *sql.Tx) error {
+ _, err := tx.Exec(`ALTER TABLE host_script_results ADD COLUMN host_deleted_at TIMESTAMP NULL`)
+ if err != nil {
+ return fmt.Errorf("failed to add host_deleted_at timestamp to host_script_results: %w", err)
+ }
+ return nil
+}
+
+func Down_20240612150059(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete_test.go b/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete_test.go
new file mode 100644
index 0000000000..8bbb8658f4
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240612150059_AlterHostScriptsAndInstallsSoftDelete_test.go
@@ -0,0 +1,32 @@
+package tables
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20240612150059(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ script1 := execNoErrLastID(t, db, "INSERT INTO script_contents(contents, md5_checksum) VALUES ('echo hello', 'a')")
+
+ host := insertHost(t, db, nil)
+
+ hostScript := execNoErrLastID(t, db, `
+INSERT INTO host_script_results (
+ host_id,
+ execution_id,
+ output,
+ script_content_id
+) VALUES (?, ?, '', ?)`, host, "f", script1)
+
+ // Apply current migration.
+ applyNext(t, db)
+
+ var hostDeletedAt *time.Time
+ err := db.Get(&hostDeletedAt, "SELECT host_deleted_at FROM host_script_results WHERE id = ?", hostScript)
+ require.NoError(t, err)
+ require.Nil(t, hostDeletedAt)
+}
diff --git a/server/datastore/mysql/migrations/tables/20240613162201_AddMDMWindowsHostUUIDIndex.go b/server/datastore/mysql/migrations/tables/20240613162201_AddMDMWindowsHostUUIDIndex.go
new file mode 100644
index 0000000000..3444200e15
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240613162201_AddMDMWindowsHostUUIDIndex.go
@@ -0,0 +1,25 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240613162201, Down_20240613162201)
+}
+
+func Up_20240613162201(tx *sql.Tx) error {
+ _, err := tx.Exec(`
+ ALTER TABLE mdm_windows_enrollments
+ ADD INDEX idx_mdm_windows_enrollments_host_uuid (host_uuid)`,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to add index to mdm_windows_enrollments.host_uuid: %w", err)
+ }
+ return nil
+}
+
+func Down_20240613162201(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go
index bbc46df1c2..79398aa975 100644
--- a/server/datastore/mysql/mysql.go
+++ b/server/datastore/mysql/mysql.go
@@ -28,8 +28,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/goose"
scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/go-sql-driver/mysql"
"github.com/hashicorp/go-multierror"
"github.com/jmoiron/sqlx"
diff --git a/server/datastore/mysql/mysql_test.go b/server/datastore/mysql/mysql_test.go
index 5967fdfdc2..e28ae949ee 100644
--- a/server/datastore/mysql/mysql_test.go
+++ b/server/datastore/mysql/mysql_test.go
@@ -23,7 +23,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index c08cf4ed5c..2b044ea89b 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -15,8 +15,8 @@ import (
"github.com/doug-martin/goqu/v9"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
diff --git a/server/datastore/mysql/query_results.go b/server/datastore/mysql/query_results.go
index fe0aac913b..02e222399e 100644
--- a/server/datastore/mysql/query_results.go
+++ b/server/datastore/mysql/query_results.go
@@ -13,7 +13,7 @@ import (
// OverwriteQueryResultRows overwrites the query result rows for a given query and host
// in a single transaction, ensuring that the number of rows for the given query
// does not exceed the maximum allowed
-func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) (err error) {
+func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) (err error) {
if len(rows) == 0 {
return nil
}
@@ -31,7 +31,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
return ctxerr.Wrap(ctx, err, "counting existing query results")
}
- if countExisting >= fleet.MaxQueryReportRows {
+ if countExisting >= maxQueryReportRows {
// do not delete any rows if we are already at the limit
return nil
}
@@ -53,7 +53,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
// Calculate how many new rows can be added given the maximum limit
netRowsAfterDeletion := countExisting - int(countDeleted)
- allowedNewRows := fleet.MaxQueryReportRows - netRowsAfterDeletion
+ allowedNewRows := maxQueryReportRows - netRowsAfterDeletion
if allowedNewRows == 0 {
return nil
}
diff --git a/server/datastore/mysql/query_results_test.go b/server/datastore/mysql/query_results_test.go
index 284ecc6143..ce1f3c8a4c 100644
--- a/server/datastore/mysql/query_results_test.go
+++ b/server/datastore/mysql/query_results_test.go
@@ -62,7 +62,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
}`)),
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), query1Rows)
+ err := ds.OverwriteQueryResultRows(context.Background(), query1Rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert Result Row for different Scheduled Query
@@ -76,7 +76,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), query2Rows)
+ err = ds.OverwriteQueryResultRows(context.Background(), query2Rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
results, err := ds.QueryResultRows(context.Background(), query.ID, fleet.TeamFilter{User: test.UserAdmin})
@@ -125,7 +125,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
+ err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert 1 Result Row for Query1 Host2
@@ -137,7 +137,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that Query1 returns 2 results for Host1
@@ -215,7 +215,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), globalRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), globalRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
teamRow := []*fleet.ScheduledQueryResultRow{
@@ -229,7 +229,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), teamRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), teamRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
observerTeamRow := []*fleet.ScheduledQueryResultRow{
@@ -243,7 +243,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
filter := fleet.TeamFilter{
@@ -286,7 +286,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
}`)),
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow)
+ err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert Nil Result Row for Query1, nil data rows are not counted
@@ -298,7 +298,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
Data: nil,
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert 5 Result Rows for Query2
@@ -317,7 +317,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
resultRows = append(resultRows, resultRow2)
}
- err = ds.OverwriteQueryResultRows(context.Background(), resultRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), resultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that ResultCountForQuery returns 1
@@ -366,7 +366,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
+ err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host1Query2 := []*fleet.ScheduledQueryResultRow{
@@ -380,7 +380,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), host1Query2)
+ err = ds.OverwriteQueryResultRows(context.Background(), host1Query2, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host2ResultRow := []*fleet.ScheduledQueryResultRow{
@@ -394,7 +394,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host3ResultRow := []*fleet.ScheduledQueryResultRow{
@@ -405,7 +405,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
Data: nil,
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow)
+ err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that Query1 returns 2
@@ -451,7 +451,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), initialRow)
+ err := ds.OverwriteQueryResultRows(context.Background(), initialRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Overwrite Result Rows with new data
@@ -465,7 +465,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that we get the overwritten data (1 result with USB Mouse data)
@@ -486,7 +486,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that the data has not changed
@@ -511,7 +511,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
mockTime := time.Now().UTC().Truncate(time.Second)
// Generate max rows -1
- maxRows := fleet.MaxQueryReportRows - 1
+ maxRows := fleet.DefaultMaxQueryReportRows - 1
maxMinusOneRows := make([]*fleet.ScheduledQueryResultRow, maxRows)
for i := 0; i < maxRows; i++ {
maxMinusOneRows[i] = &fleet.ScheduledQueryResultRow{
@@ -521,7 +521,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
}
}
- err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows)
+ err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Add an empty data rows which do not count towards the max
@@ -532,7 +532,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: nil,
},
- })
+ }, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Confirm that we can still add a row
@@ -543,13 +543,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
- })
+ }, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that we now have max rows
count, err := ds.ResultCountForQuery(context.Background(), query.ID)
require.NoError(t, err)
- require.Equal(t, fleet.MaxQueryReportRows, count)
+ require.Equal(t, fleet.DefaultMaxQueryReportRows, count)
// Attempt to add another row
err = ds.OverwriteQueryResultRows(context.Background(), []*fleet.ScheduledQueryResultRow{
@@ -559,7 +559,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
- })
+ }, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that the last row was not added
@@ -568,7 +568,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
require.Len(t, host4result, 0)
// Generate more than max rows in Query 2
- rows := fleet.MaxQueryReportRows + 50
+ rows := fleet.DefaultMaxQueryReportRows + 50
largeBatchRows := make([]*fleet.ScheduledQueryResultRow, rows)
for i := 0; i < rows; i++ {
largeBatchRows[i] = &fleet.ScheduledQueryResultRow{
@@ -578,13 +578,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
}
}
- err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Confirm only max rows are stored for the queryID
allResults, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host1.ID)
require.NoError(t, err)
- require.Len(t, allResults, fleet.MaxQueryReportRows)
+ require.Len(t, allResults, fleet.DefaultMaxQueryReportRows)
// Confirm that new rows are not added when the max is reached
newMockTime := mockTime.Add(2 * time.Minute)
@@ -597,7 +597,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host2Results, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host2.ID)
@@ -619,7 +619,7 @@ func testQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
- err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
+ err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
filter := fleet.TeamFilter{User: user, IncludeObserver: true}
@@ -655,7 +655,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "Keyboard", "vendor": "Microsoft"}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), rows)
+ err = ds.OverwriteQueryResultRows(context.Background(), rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Call OverwriteQueryResultRows again with different rows
@@ -673,7 +673,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "Speakers", "vendor": "Bose"}`)),
},
}
- err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
+ err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Cleanup query result rows
diff --git a/server/datastore/mysql/scep_test.go b/server/datastore/mysql/scep_test.go
index 32347df118..d420d2af18 100644
--- a/server/datastore/mysql/scep_test.go
+++ b/server/datastore/mysql/scep_test.go
@@ -39,7 +39,7 @@ func TestAppleMDMSCEPSerial(t *testing.T) {
func TestAppleMDMPutAndHasCN(t *testing.T) {
depot := setup(t)
- name := "FleetDM Identity"
+ name := "Fleet Identity"
serial, err := depot.Serial()
require.NoError(t, err)
cert := x509.Certificate{
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 0d76506734..abac9f4d0a 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
+INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `calendar_events` (
@@ -442,6 +442,7 @@ CREATE TABLE `host_script_results` (
`user_id` int(10) unsigned DEFAULT NULL,
`sync_request` tinyint(1) NOT NULL DEFAULT '0',
`script_content_id` int(10) unsigned DEFAULT NULL,
+ `host_deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_host_script_results_execution_id` (`execution_id`),
KEY `idx_host_script_results_host_exit_created` (`host_id`,`exit_code`,`created_at`),
@@ -913,7 +914,8 @@ CREATE TABLE `mdm_windows_enrollments` (
`host_uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_type` (`mdm_hardware_id`),
- KEY `idx_mdm_windows_enrollments_mdm_device_id` (`mdm_device_id`)
+ KEY `idx_mdm_windows_enrollments_mdm_device_id` (`mdm_device_id`),
+ KEY `idx_mdm_windows_enrollments_host_uuid` (`host_uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
@@ -925,9 +927,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=268 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240607133721,1,'2020-01-01 01:01:01'),(269,20240612150059,1,'2020-01-01 01:01:01'),(270,20240613162201,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_device_management_solutions` (
diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go
index 59afeb94af..67c223f01f 100644
--- a/server/datastore/mysql/scripts.go
+++ b/server/datastore/mysql/scripts.go
@@ -238,7 +238,8 @@ func (ds *Datastore) getHostScriptExecutionResultDB(ctx context.Context, q sqlx.
hsr.exit_code,
hsr.created_at,
hsr.user_id,
- hsr.sync_request
+ hsr.sync_request,
+ hsr.host_deleted_at
FROM
host_script_results hsr
JOIN
@@ -1022,7 +1023,7 @@ func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, hostFl
return ctxerr.Wrap(ctx, err, "record manual unlock host request")
}
-func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string) string {
+func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string, setUnlockRef bool) string {
var alias string
stmt := `UPDATE host_mdm_actions `
@@ -1038,7 +1039,14 @@ func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart s
// Note that this must not clear the unlock_pin, because recording the
// lock request does generate the PIN and store it there to be used by an
// eventual unlock.
- stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
+ if !setUnlockRef {
+ stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
+ } else {
+ // Currently only used for Apple MDM devices.
+ // We set the unlock_ref to current time since the device can be unlocked any time after the lock.
+ // Apple MDM does not have a concept of unlock pending.
+ stmt += fmt.Sprintf("%sunlock_ref = '%s', %[1]swipe_ref = NULL", alias, time.Now().Format(time.DateTime))
+ }
case "unlock_ref":
// a successful unlock clears itself as well as the lock ref, because
// unlock is the default state so we don't need to keep its unlock_ref
@@ -1060,26 +1068,30 @@ func (ds *Datastore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Cont
// a bit of MDM protocol leaking in the mysql layer, but it's either that or
// the other way around (MDM protocol would translate to database column)
var refCol string
+ var setUnlockRef bool
switch requestType {
case "EraseDevice":
refCol = "wipe_ref"
case "DeviceLock":
refCol = "lock_ref"
+ setUnlockRef = true
default:
return nil
}
- return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded)
+ return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded, setUnlockRef)
}
-func updateHostLockWipeStatusFromResultAndHostUUID(ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool) error {
- stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`)
+func updateHostLockWipeStatusFromResultAndHostUUID(
+ ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool, setUnlockRef bool,
+) error {
+ stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`, setUnlockRef)
stmt += ` WHERE h.uuid = ? AND hma.` + refCol + ` = ?`
_, err := tx.ExecContext(ctx, stmt, hostUUID, cmdUUID)
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result via host uuid")
}
func updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, refCol string, succeeded bool) error {
- stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "")
+ stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "", false)
stmt += ` WHERE host_id = ?`
_, err := tx.ExecContext(ctx, stmt, hostID)
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")
diff --git a/server/datastore/mysql/scripts_test.go b/server/datastore/mysql/scripts_test.go
index 09736a0dd7..0dbb22e1f4 100644
--- a/server/datastore/mysql/scripts_test.go
+++ b/server/datastore/mysql/scripts_test.go
@@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -764,6 +765,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, "windows", status.HostFleetPlatform)
require.NotNil(t, status.LockScript)
+ assert.Nil(t, status.UnlockScript)
s := status.LockScript
require.Equal(t, script, s.ScriptContents)
diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go
index 4856e1b831..9af2eee09e 100644
--- a/server/datastore/mysql/software.go
+++ b/server/datastore/mysql/software.go
@@ -15,7 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
@@ -347,6 +347,11 @@ func (ds *Datastore) applyChangesForNewSoftwareDB(
return r, nil
}
+ existingSoftware, incomingByChecksum, existingTitlesForNewSoftware, err := ds.getExistingSoftware(ctx, current, incoming)
+ if err != nil {
+ return r, err
+ }
+
err = ds.withRetryTxx(
ctx, func(tx sqlx.ExtContext) error {
deleted, err := deleteUninstalledHostSoftwareDB(ctx, tx, hostID, current, incoming)
@@ -355,7 +360,9 @@ func (ds *Datastore) applyChangesForNewSoftwareDB(
}
r.Deleted = deleted
- inserted, err := ds.insertNewInstalledHostSoftwareDB(ctx, tx, hostID, current, incoming)
+ inserted, err := ds.insertNewInstalledHostSoftwareDB(
+ ctx, tx, hostID, existingSoftware, incomingByChecksum, existingTitlesForNewSoftware,
+ )
if err != nil {
return err
}
@@ -377,6 +384,91 @@ func (ds *Datastore) applyChangesForNewSoftwareDB(
return r, err
}
+func (ds *Datastore) getExistingSoftware(
+ ctx context.Context, current map[string]fleet.Software, incoming map[string]fleet.Software,
+) (
+ currentSoftware []softwareIDChecksum, incomingChecksumToSoftware map[string]fleet.Software,
+ incomingChecksumToTitle map[string]fleet.SoftwareTitle, err error,
+) {
+ // Compute checksums for all incoming software, which we will use for faster retrieval, since checksum is a unique index
+ incomingChecksumToSoftware = make(map[string]fleet.Software, len(current))
+ newSoftware := make(map[string]struct{})
+ for uniqueName, s := range incoming {
+ if _, ok := current[uniqueName]; !ok {
+ checksum, err := computeRawChecksum(s)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ incomingChecksumToSoftware[string(checksum)] = s
+ newSoftware[string(checksum)] = struct{}{}
+ }
+ }
+
+ if len(incomingChecksumToSoftware) > 0 {
+ keys := make([]string, 0, len(incomingChecksumToSoftware))
+ for checksum := range incomingChecksumToSoftware {
+ keys = append(keys, checksum)
+ }
+ // We use the replica DB for retrieval to minimize the traffic to the master DB.
+ // It is OK if the software is not found in the replica DB, because we will then attempt to insert it in the master DB.
+ currentSoftware, err = getSoftwareIDsByChecksums(ctx, ds.reader(ctx), keys)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ for _, s := range currentSoftware {
+ _, ok := incomingChecksumToSoftware[s.Checksum]
+ if !ok {
+ // This should never happen. If it does, we have a bug.
+ return nil, nil, nil, ctxerr.New(
+ ctx, fmt.Sprintf("software not found for checksum %s", hex.EncodeToString([]byte(s.Checksum))),
+ )
+ }
+ delete(newSoftware, s.Checksum)
+ }
+ }
+
+ // Get software titles for new software, if any
+ incomingChecksumToTitle = make(map[string]fleet.SoftwareTitle, len(newSoftware))
+ if len(newSoftware) > 0 {
+ totalToProcess := len(newSoftware)
+ const numberOfArgsPerSoftwareTitle = 3 // number of ? in each WHERE clause
+ whereClause := strings.TrimSuffix(
+ strings.Repeat("(name = ? AND source = ? AND browser = ?) OR", totalToProcess), " OR",
+ )
+ stmt := fmt.Sprintf(
+ "SELECT id, name, source, browser FROM software_titles WHERE %s",
+ whereClause,
+ )
+ args := make([]interface{}, 0, totalToProcess*numberOfArgsPerSoftwareTitle)
+ uniqueTitleStrToChecksum := make(map[string]string, totalToProcess)
+ for checksum := range newSoftware {
+ sw := incomingChecksumToSoftware[checksum]
+ args = append(args, sw.Name, sw.Source, sw.Browser)
+ // Map software title identifier to software checksums so that we can map checksums to actual titles later.
+ uniqueTitleStrToChecksum[UniqueSoftwareTitleStr(sw.Name, sw.Source, sw.Browser)] = checksum
+ }
+ var existingSoftwareTitlesForNewSoftware []fleet.SoftwareTitle
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &existingSoftwareTitlesForNewSoftware, stmt, args...); err != nil {
+ return nil, nil, nil, ctxerr.Wrap(ctx, err, "get existing titles")
+ }
+
+ // Map software titles to software checksums.
+ for _, title := range existingSoftwareTitlesForNewSoftware {
+ checksum, ok := uniqueTitleStrToChecksum[UniqueSoftwareTitleStr(title.Name, title.Source, title.Browser)]
+ if ok {
+ incomingChecksumToTitle[checksum] = title
+ }
+ }
+ }
+
+ return currentSoftware, incomingChecksumToSoftware, incomingChecksumToTitle, nil
+}
+
+// UniqueSoftwareTitleStr creates a unique string representation of the software title
+func UniqueSoftwareTitleStr(values ...string) string {
+ return strings.Join(values, fleet.SoftwareFieldSeparator)
+}
+
// delete host_software that is in current map, but not in incoming map.
// returns the deleted software on the host
func deleteUninstalledHostSoftwareDB(
@@ -423,59 +515,22 @@ func computeRawChecksum(sw fleet.Software) ([]byte, error) {
return h.Sum(nil), nil
}
-// insert host_software that is in incoming map, but not in current map.
+// Insert host_software that is in softwareChecksums map, but not in existingSoftware.
+// Also insert any new software titles that are needed.
// returns the inserted software on the host
-//
-//nolint:gocritic // This function uses a read from DB replica inside a transaction. This is an intentional optimization to reduce master DB load.
func (ds *Datastore) insertNewInstalledHostSoftwareDB(
ctx context.Context,
tx sqlx.ExtContext,
hostID uint,
- currentMap map[string]fleet.Software,
- incomingMap map[string]fleet.Software,
+ existingSoftware []softwareIDChecksum,
+ softwareChecksums map[string]fleet.Software,
+ existingTitlesForNewSoftware map[string]fleet.SoftwareTitle,
) ([]fleet.Software, error) {
var insertsHostSoftware []interface{}
var insertedSoftware []fleet.Software
- type softwareWithUniqueName struct {
- uniqueName string
- software fleet.Software
- }
- incomingOrdered := make([]softwareWithUniqueName, 0, len(incomingMap))
- for uniqueName, software := range incomingMap {
- incomingOrdered = append(incomingOrdered, softwareWithUniqueName{
- uniqueName: uniqueName,
- software: software,
- })
- }
- sort.Slice(incomingOrdered, func(i, j int) bool {
- return incomingOrdered[i].uniqueName < incomingOrdered[j].uniqueName
- })
-
- // Compute checksums for all incoming software, which we will use for faster retrieval, since checksum is a unique index
- softwareChecksums := make(map[string]*fleet.Software, 0)
- for i, s := range incomingOrdered {
- if _, ok := currentMap[s.uniqueName]; !ok {
- checksum, err := computeRawChecksum(s.software)
- if err != nil {
- return nil, err
- }
- softwareChecksums[string(checksum)] = &incomingOrdered[i].software // we can't use `s` here because it is a pointer to a loop variable
- }
- }
-
- // First, we check if host's new software already exists in the software table.
+ // First, we remove incoming software that already exists in the software table.
if len(softwareChecksums) > 0 {
- keys := make([]string, 0, len(softwareChecksums))
- for checksum := range softwareChecksums {
- keys = append(keys, checksum)
- }
- // We use the replica DB for retrieval to minimize the traffic to the master DB.
- // It is OK if the software is not found in the replica DB, because we will then attempt to insert it in the master DB.
- existingSoftware, err := getSoftwareIDsByChecksums(ctx, ds.reader(ctx), keys)
- if err != nil {
- return nil, err
- }
for _, s := range existingSoftware {
software, ok := softwareChecksums[s.Checksum]
if !ok {
@@ -483,7 +538,7 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
}
software.ID = s.ID
insertsHostSoftware = append(insertsHostSoftware, hostID, software.ID, software.LastOpenedAt)
- insertedSoftware = append(insertedSoftware, *software)
+ insertedSoftware = append(insertedSoftware, software)
delete(softwareChecksums, s.Checksum)
}
}
@@ -501,42 +556,92 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
end = len(keys)
}
totalToProcess := end - start
- const numberOfArgsPerSoftware = 10 // number of ? in each VALUES clause
+
+ // Insert into software
+ const numberOfArgsPerSoftware = 11 // number of ? in each VALUES clause
values := strings.TrimSuffix(
- strings.Repeat("(?,?,?,?,?,?,?,?,?,?),", totalToProcess), ",",
+ strings.Repeat("(?,?,?,?,?,?,?,?,?,?,?),", totalToProcess), ",",
)
// INSERT IGNORE is used to avoid duplicate key errors, which may occur since our previous read came from the replica.
stmt := fmt.Sprintf(
- "INSERT IGNORE INTO software (name, version, source, `release`, vendor, arch, bundle_identifier, extension_id, browser, checksum) VALUES %s",
+ "INSERT IGNORE INTO software (name, version, source, `release`, vendor, arch, bundle_identifier, extension_id, browser, title_id, checksum) VALUES %s",
values,
)
args := make([]interface{}, 0, totalToProcess*numberOfArgsPerSoftware)
+ newTitlesNeeded := make(map[string]fleet.SoftwareTitle)
for j := start; j < end; j++ {
checksum := keys[j]
sw := softwareChecksums[checksum]
+ var titleID *uint
+ title, ok := existingTitlesForNewSoftware[checksum]
+ if ok {
+ titleID = &title.ID
+ } else if _, ok := newTitlesNeeded[checksum]; !ok {
+ newTitlesNeeded[checksum] = fleet.SoftwareTitle{
+ Name: sw.Name,
+ Source: sw.Source,
+ Browser: sw.Browser,
+ }
+ }
args = append(
args, sw.Name, sw.Version, sw.Source, sw.Release, sw.Vendor, sw.Arch, sw.BundleIdentifier, sw.ExtensionID, sw.Browser,
- checksum,
+ titleID, checksum,
)
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "insert software")
}
+
+ // Insert into software_titles
+ totalTitlesToProcess := len(newTitlesNeeded)
+ if totalTitlesToProcess > 0 {
+ const numberOfArgsPerSoftwareTitles = 3 // number of ? in each VALUES clause
+ titlesValues := strings.TrimSuffix(strings.Repeat("(?,?,?),", totalTitlesToProcess), ",")
+ // INSERT IGNORE is used to avoid duplicate key errors, which may occur since our previous read came from the replica.
+ titlesStmt := fmt.Sprintf("INSERT IGNORE INTO software_titles (name, source, browser) VALUES %s", titlesValues)
+ titlesArgs := make([]interface{}, 0, totalTitlesToProcess*numberOfArgsPerSoftwareTitles)
+ titleChecksums := make([]string, totalTitlesToProcess)
+ for checksum, title := range newTitlesNeeded {
+ titlesArgs = append(titlesArgs, title.Name, title.Source, title.Browser)
+ titleChecksums = append(titleChecksums, checksum)
+ }
+ if _, err := tx.ExecContext(ctx, titlesStmt, titlesArgs...); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "insert software_titles")
+ }
+
+ // update new title ids for new software table entries
+ updateSoftwareStmt := `
+ UPDATE
+ software s,
+ software_titles st
+ SET
+ s.title_id = st.id
+ WHERE
+ (s.name, s.source, s.browser) = (st.name, st.source, st.browser)
+ AND s.checksum IN (?)`
+ updateSoftwareStmt, updateArgs, err := sqlx.In(updateSoftwareStmt, titleChecksums)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "build update software title_id")
+ }
+ if _, err = tx.ExecContext(ctx, updateSoftwareStmt, updateArgs...); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "update software title_id")
+ }
+ }
}
// Here, we use the transaction (tx) for retrieval because we must retrieve the software IDs that we just inserted.
- existingSoftware, err := getSoftwareIDsByChecksums(ctx, tx, keys)
+ updatedExistingSoftware, err := getSoftwareIDsByChecksums(ctx, tx, keys)
if err != nil {
return nil, err
}
- for _, s := range existingSoftware {
+ for _, s := range updatedExistingSoftware {
software, ok := softwareChecksums[s.Checksum]
if !ok {
return nil, ctxerr.New(ctx, fmt.Sprintf("software not found for checksum %s", hex.EncodeToString([]byte(s.Checksum))))
}
software.ID = s.ID
insertsHostSoftware = append(insertsHostSoftware, hostID, software.ID, software.LastOpenedAt)
- insertedSoftware = append(insertedSoftware, *software)
+ insertedSoftware = append(insertedSoftware, software)
delete(softwareChecksums, s.Checksum)
}
}
diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go
index 01327ea037..329117cb74 100644
--- a/server/datastore/mysql/software_test.go
+++ b/server/datastore/mysql/software_test.go
@@ -224,9 +224,15 @@ func testSoftwareHostDuplicates(t *testing.T, ds *Datastore) {
soft2Key := sw.ToUniqueStr()
incoming[soft2Key] = *sw
+ incomingByChecksum, existingSoftware, existingTitlesForNewSoftware, err := ds.getExistingSoftware(
+ context.Background(), make(map[string]fleet.Software), incoming,
+ )
+ require.NoError(t, err)
tx, err := ds.writer(context.Background()).Beginx()
require.NoError(t, err)
- _, err = ds.insertNewInstalledHostSoftwareDB(context.Background(), tx, host1.ID, make(map[string]fleet.Software), incoming)
+ _, err = ds.insertNewInstalledHostSoftwareDB(
+ context.Background(), tx, host1.ID, incomingByChecksum, existingSoftware, existingTitlesForNewSoftware,
+ )
require.NoError(t, err)
require.NoError(t, tx.Commit())
@@ -247,9 +253,15 @@ func testSoftwareHostDuplicates(t *testing.T, ds *Datastore) {
soft3Key := sw.ToUniqueStr()
incoming[soft3Key] = *sw
+ incomingByChecksum, existingSoftware, existingTitlesForNewSoftware, err = ds.getExistingSoftware(
+ context.Background(), make(map[string]fleet.Software), incoming,
+ )
+ require.NoError(t, err)
tx, err = ds.writer(context.Background()).Beginx()
require.NoError(t, err)
- _, err = ds.insertNewInstalledHostSoftwareDB(context.Background(), tx, host1.ID, make(map[string]fleet.Software), incoming)
+ _, err = ds.insertNewInstalledHostSoftwareDB(
+ context.Background(), tx, host1.ID, incomingByChecksum, existingSoftware, existingTitlesForNewSoftware,
+ )
require.NoError(t, err)
require.NoError(t, tx.Commit())
@@ -1732,6 +1744,16 @@ func testUpdateHostSoftware(t *testing.T, ds *Datastore) {
want := expect[i]
require.Equal(t, want.name, sw.Name)
+ var titleID uint
+ require.NoError(
+ t, ds.writer(ctx).GetContext(
+ ctx, &titleID,
+ `SELECT s.title_id FROM software s INNER JOIN software_titles st ON (s.name = st.name AND s.source = st.source AND s.browser = st.browser) WHERE st.id = ?`,
+ sw.ID,
+ ),
+ )
+ assert.NotZero(t, titleID)
+
if want.ts.IsZero() {
require.Nil(t, sw.LastOpenedAt)
} else {
@@ -1742,7 +1764,7 @@ func testUpdateHostSoftware(t *testing.T, ds *Datastore) {
// set the initial software list
sw := []fleet.Software{
- {Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
+ {Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo", Browser: "chrome"},
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar", LastOpenedAt: &lastYear},
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz", LastOpenedAt: &now},
}
@@ -2761,6 +2783,14 @@ func TestReconcileSoftwareTitles(t *testing.T) {
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
{Name: "baz", Version: "0.0.1", Source: "deb_packages"},
}
+ expectedTitlesByNSB := map[string]fleet.SoftwareTitle{}
+ for _, s := range expectedSoftware {
+ expectedTitlesByNSB[s.Name+s.Source+s.Browser] = fleet.SoftwareTitle{
+ Name: s.Name,
+ Source: s.Source,
+ Browser: s.Browser,
+ }
+ }
software1 := []fleet.Software{expectedSoftware[0], expectedSoftware[2]}
software2 := []fleet.Software{expectedSoftware[1], expectedSoftware[2], expectedSoftware[3]}
@@ -2793,8 +2823,7 @@ func TestReconcileSoftwareTitles(t *testing.T) {
return swt, nil
}
- expectedTitlesByNSB := map[string]fleet.SoftwareTitle{}
- assertSoftware := func(t *testing.T, wantSoftware []fleet.Software, wantNilTitleID []fleet.Software) {
+ assertSoftware := func(t *testing.T, wantSoftware []fleet.Software) {
gotSoftware, err := getSoftware()
require.NoError(t, err)
require.Len(t, gotSoftware, len(wantSoftware))
@@ -2808,25 +2837,13 @@ func TestReconcileSoftwareTitles(t *testing.T) {
_, ok := byNSBV[r.Name+r.Source+r.Browser+r.Version]
require.True(t, ok)
- if r.TitleID == nil {
- var found bool
- for _, s := range wantNilTitleID {
- if s.Name == r.Name && s.Source == r.Source && s.Browser == r.Browser && s.Version == r.Version {
- found = true
- break
- }
- }
- require.True(t, found)
- } else {
- require.NotNil(t, r.TitleID)
- swt, ok := expectedTitlesByNSB[r.Name+r.Source+r.Browser]
- require.True(t, ok)
- require.NotNil(t, r.TitleID)
- require.Equal(t, swt.ID, *r.TitleID)
- require.Equal(t, swt.Name, r.Name)
- require.Equal(t, swt.Source, r.Source)
- require.Equal(t, swt.Browser, r.Browser)
- }
+ assert.NotNil(t, r.TitleID)
+ swt, ok := expectedTitlesByNSB[r.Name+r.Source+r.Browser]
+ require.True(t, ok)
+ assert.Equal(t, swt.ID, *r.TitleID)
+ assert.Equal(t, swt.Name, r.Name)
+ assert.Equal(t, swt.Source, r.Source)
+ assert.Equal(t, swt.Browser, r.Browser)
}
}
@@ -2844,8 +2861,15 @@ func TestReconcileSoftwareTitles(t *testing.T) {
}
}
- // title_id is initially nil for all software entries
- assertSoftware(t, expectedSoftware, expectedSoftware)
+ swTitles, err := getTitles()
+ require.NoError(t, err)
+ for _, swt := range swTitles {
+ if _, ok := expectedTitlesByNSB[swt.Name+swt.Source+swt.Browser]; ok {
+ expectedTitlesByNSB[swt.Name+swt.Source+swt.Browser] = swt
+ }
+ }
+
+ assertSoftware(t, expectedSoftware)
// reconcile software titles
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
@@ -2873,15 +2897,15 @@ func TestReconcileSoftwareTitles(t *testing.T) {
require.Equal(t, swt[3].Browser, "chrome")
expectedTitlesByNSB[swt[3].Name+swt[3].Source+swt[3].Browser] = swt[3]
- // title_id is now populated for all software entries
- assertSoftware(t, expectedSoftware, nil)
+ // Double check software and titles
+ assertSoftware(t, expectedSoftware)
// remove the bar software title from host 2
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2[:2])
require.NoError(t, err)
// SyncHostsSoftware will remove the above software item from the software table
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
- assertSoftware(t, []fleet.Software{expectedSoftware[0], expectedSoftware[1], expectedSoftware[2], expectedSoftware[4]}, nil)
+ assertSoftware(t, []fleet.Software{expectedSoftware[0], expectedSoftware[1], expectedSoftware[2], expectedSoftware[4]})
// bar is no longer associated with any host so the title should be deleted
require.NoError(t, ds.ReconcileSoftwareTitles(context.Background()))
@@ -2894,18 +2918,6 @@ func TestReconcileSoftwareTitles(t *testing.T) {
_, err = ds.UpdateHostSoftware(context.Background(), host3.ID, []fleet.Software{expectedSoftware[3], expectedSoftware[4]})
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
-
- // title_id is initially nil for new software entries
- assertSoftware(t, expectedSoftware, []fleet.Software{expectedSoftware[3]})
-
- // bar isn't added back to software titles until we reconcile software titles
- gotTitles, err = getTitles()
- require.NoError(t, err)
- require.Len(t, gotTitles, 3)
- assertTitles(t, gotTitles, []string{"bar"})
-
- // reconcile software titles
- require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
gotTitles, err = getTitles()
require.NoError(t, err)
require.Len(t, gotTitles, 4)
@@ -2916,38 +2928,24 @@ func TestReconcileSoftwareTitles(t *testing.T) {
require.NotEqual(t, expectedTitlesByNSB[gotTitles[0].Name+gotTitles[0].Source], gotTitles[0].ID)
expectedTitlesByNSB[gotTitles[0].Name+gotTitles[0].Source] = gotTitles[0]
assertTitles(t, gotTitles, nil)
-
- // title_id is now populated for bar
- assertSoftware(t, expectedSoftware, nil)
+ assertSoftware(t, expectedSoftware)
// add a new version of foo to host 3
expectedSoftware = append(expectedSoftware, fleet.Software{Name: "foo", Version: "0.0.4", Source: "chrome_extensions"})
_, err = ds.UpdateHostSoftware(ctx, host3.ID, expectedSoftware[3:])
require.NoError(t, err)
-
- // title_id is initially nil for new software entries
- assertSoftware(t, expectedSoftware, []fleet.Software{expectedSoftware[5]})
-
- // new version of foo doesn't result in a new software title entry
- require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
gotTitles, err = getTitles()
require.NoError(t, err)
require.Len(t, gotTitles, 4)
assertTitles(t, gotTitles, nil)
-
- // title_id is now populated for new version of foo
- assertSoftware(t, expectedSoftware, nil)
+ assertSoftware(t, expectedSoftware)
// add a new source of foo to host 3
expectedSoftware = append(expectedSoftware, fleet.Software{Name: "foo", Version: "0.0.4", Source: "rpm_packages"})
_, err = ds.UpdateHostSoftware(ctx, host3.ID, expectedSoftware[3:])
require.NoError(t, err)
- // title_id is initially nil for new software entries
- assertSoftware(t, expectedSoftware, []fleet.Software{expectedSoftware[6]})
-
// new source of foo results in a new software title entry
- require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
gotTitles, err = getTitles()
require.NoError(t, err)
require.Len(t, gotTitles, 5)
@@ -2956,9 +2954,7 @@ func TestReconcileSoftwareTitles(t *testing.T) {
require.Equal(t, "", gotTitles[4].Browser)
expectedTitlesByNSB[gotTitles[4].Name+gotTitles[4].Source+gotTitles[4].Browser] = gotTitles[4]
assertTitles(t, gotTitles, nil)
-
- // title_id is now populated for new source of foo
- assertSoftware(t, expectedSoftware, nil)
+ assertSoftware(t, expectedSoftware)
}
func testUpdateHostSoftwareDeadlock(t *testing.T, ds *Datastore) {
diff --git a/server/datastore/mysql/statistics.go b/server/datastore/mysql/statistics.go
index 4dc5e5e0d4..55e9b9e4ee 100644
--- a/server/datastore/mysql/statistics.go
+++ b/server/datastore/mysql/statistics.go
@@ -11,7 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/version"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
@@ -24,47 +24,47 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
lic, _ := license.FromContext(ctx)
computeStats := func(stats *fleet.StatisticsPayload, since time.Time) error {
- enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.writer(ctx))
+ enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount enrolled hosts by os")
}
- amountUsers, err := tableRowsCount(ctx, ds.writer(ctx), "users")
+ amountUsers, err := tableRowsCount(ctx, ds.reader(ctx), "users")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount users")
}
- amountSoftwaresVersions, err := tableRowsCount(ctx, ds.writer(ctx), "software")
+ amountSoftwaresVersions, err := tableRowsCount(ctx, ds.reader(ctx), "software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software")
}
- amountHostSoftwares, err := tableRowsCount(ctx, ds.writer(ctx), "host_software")
+ amountHostSoftwares, err := tableRowsCount(ctx, ds.reader(ctx), "host_software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software")
}
- amountSoftwareTitles, err := tableRowsCount(ctx, ds.writer(ctx), "software_titles")
+ amountSoftwareTitles, err := tableRowsCount(ctx, ds.reader(ctx), "software_titles")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_titles")
}
- amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.writer(ctx), "host_software_installed_paths")
+ amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.reader(ctx), "host_software_installed_paths")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software_installed_paths")
}
- amountSoftwareCpes, err := tableRowsCount(ctx, ds.writer(ctx), "software_cpe")
+ amountSoftwareCpes, err := tableRowsCount(ctx, ds.reader(ctx), "software_cpe")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cpe")
}
- amountSoftwareCves, err := tableRowsCount(ctx, ds.writer(ctx), "software_cve")
+ amountSoftwareCves, err := tableRowsCount(ctx, ds.reader(ctx), "software_cve")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cve")
}
- amountTeams, err := amountTeamsDB(ctx, ds.writer(ctx))
+ amountTeams, err := amountTeamsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount teams")
}
- amountPolicies, err := amountPoliciesDB(ctx, ds.writer(ctx))
+ amountPolicies, err := amountPoliciesDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount policies")
}
- amountLabels, err := amountLabelsDB(ctx, ds.writer(ctx))
+ amountLabels, err := amountLabelsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount labels")
}
@@ -72,11 +72,11 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics app config")
}
- amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.writer(ctx), since)
+ amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.reader(ctx), since)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount active users")
}
- amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.writer(ctx))
+ amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.reader(ctx))
if err == sql.ErrNoRows {
level.Debug(ds.logger).Log("msg", "amount policy violation days", "err", err) //nolint:errcheck
} else if err != nil {
@@ -86,15 +86,15 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics error store")
}
- amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.writer(ctx), ds.logger, config)
+ amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.reader(ctx), ds.logger, config)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts not responding")
}
- amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.writer(ctx))
+ amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts by orbit version")
}
- amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.writer(ctx))
+ amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts by osquery version")
}
@@ -134,7 +134,7 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
}
dest := statistics{}
- err := sqlx.GetContext(ctx, ds.writer(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
+ err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
if err != nil {
if err == sql.ErrNoRows {
anonIdentifier, err := server.GenerateRandomText(64)
diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go
index 6e2efacef2..eb1e626281 100644
--- a/server/datastore/mysql/testing_utils.go
+++ b/server/datastore/mysql/testing_utils.go
@@ -29,7 +29,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
diff --git a/server/errorstore/errors.go b/server/errorstore/errors.go
index 7290e444fa..5bddf32bd8 100644
--- a/server/errorstore/errors.go
+++ b/server/errorstore/errors.go
@@ -22,8 +22,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/datastore/redis"
"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"
redigo "github.com/gomodule/redigo/redis"
)
diff --git a/server/errorstore/errors_test.go b/server/errorstore/errors_test.go
index e2eec946d3..cc5ccbbd41 100644
--- a/server/errorstore/errors_test.go
+++ b/server/errorstore/errors_test.go
@@ -18,7 +18,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
pkgErrors "github.com/pkg/errors" //nolint:depguard
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/fleet/app.go b/server/fleet/app.go
index 4c10781c60..fcd299da74 100644
--- a/server/fleet/app.go
+++ b/server/fleet/app.go
@@ -191,12 +191,10 @@ type MacOSUpdates struct {
Deadline optjson.String `json:"deadline"`
}
-// EnabledForHost returns a boolean indicating if updates are enabled for the host
-func (m MacOSUpdates) EnabledForHost(h *Host) bool {
+// Configured returns a boolean indicating if updates are configured
+func (m MacOSUpdates) Configured() bool {
return m.Deadline.Value != "" &&
- m.MinimumVersion.Value != "" &&
- h.IsOsqueryEnrolled() &&
- h.MDMInfo.IsFleetEnrolled()
+ m.MinimumVersion.Value != ""
}
func (m MacOSUpdates) Validate() error {
@@ -237,16 +235,6 @@ type WindowsUpdates struct {
GracePeriodDays optjson.Int `json:"grace_period_days"`
}
-// EnabledForHost returns a boolean indicating if enforced Windows OS updates
-// are enabled for the host. Note that the provided Host needs to be loaded
-// with full MDMInfo data for the check to be valid.
-func (w WindowsUpdates) EnabledForHost(h *Host) bool {
- return w.DeadlineDays.Valid &&
- w.GracePeriodDays.Valid &&
- h.IsOsqueryEnrolled() &&
- h.MDMInfo.IsFleetEnrolled()
-}
-
// Equal returns true if the values of the fields of w and other are equal. It
// returns false otherwise. If e.g. w.DeadlineDays.Value == 0 but its .Valid
// field == false (i.e. it is null), it is not equal to
@@ -888,6 +876,16 @@ type ServerSettings struct {
QueryReportsDisabled bool `json:"query_reports_disabled"`
ScriptsDisabled bool `json:"scripts_disabled"`
AIFeaturesDisabled bool `json:"ai_features_disabled"`
+ QueryReportCap int `json:"query_report_cap"`
+}
+
+const DefaultMaxQueryReportRows int = 1000
+
+func (f *ServerSettings) GetQueryReportCap() int {
+ if f.QueryReportCap <= 0 {
+ return DefaultMaxQueryReportRows
+ }
+ return f.QueryReportCap
}
// HostExpirySettings contains settings pertaining to automatic host expiry.
diff --git a/server/fleet/app_test.go b/server/fleet/app_test.go
index bca9dd6dba..46c93c0982 100644
--- a/server/fleet/app_test.go
+++ b/server/fleet/app_test.go
@@ -171,55 +171,17 @@ func TestWindowsUpdatesEqual(t *testing.T) {
}
}
-func TestWIndowsUpdatesEnabledForHost(t *testing.T) {
- hostWithRequirements := &Host{
- OsqueryHostID: ptr.String("notempty"),
- Platform: "windows",
- MDMInfo: &HostMDM{
- IsServer: false,
- Enrolled: true,
- Name: WellKnownMDMFleet,
- },
- }
- cases := []struct {
- w WindowsUpdates
- host *Host
- want bool
- }{
- {WindowsUpdates{}, &Host{}, false},
- {WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: false}, GracePeriodDays: optjson.Int{Set: true, Valid: false}}, hostWithRequirements, false},
- {WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: true}, GracePeriodDays: optjson.Int{Set: true, Valid: false}}, hostWithRequirements, false},
- {WindowsUpdates{DeadlineDays: optjson.Int{Set: true, Valid: true}, GracePeriodDays: optjson.Int{Set: true, Valid: true}}, hostWithRequirements, true},
- {WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, &Host{}, false},
- {WindowsUpdates{DeadlineDays: optjson.SetInt(1), GracePeriodDays: optjson.SetInt(2)}, hostWithRequirements, true},
- }
-
- for _, tc := range cases {
- require.Equal(t, tc.want, tc.w.EnabledForHost(tc.host))
- }
-}
-
-func TestMacOSUpdatesEnabledForHost(t *testing.T) {
- hostWithRequirements := &Host{
- OsqueryHostID: ptr.String("notempty"),
- MDMInfo: &HostMDM{
- IsServer: false,
- Enrolled: true,
- Name: WellKnownMDMFleet,
- },
- }
+func TestMacOSUpdatesConfigured(t *testing.T) {
cases := []struct {
version string
deadline string
- host *Host
out bool
}{
- {"", "", &Host{}, false},
- {"", "", hostWithRequirements, false},
- {"12.3", "", hostWithRequirements, false},
- {"", "12-03-2022", hostWithRequirements, false},
- {"12.3", "12-03-2022", &Host{}, false},
- {"12.3", "12-03-2022", hostWithRequirements, true},
+ {"", "", false},
+ {"", "", false},
+ {"12.3", "", false},
+ {"", "12-03-2022", false},
+ {"12.3", "12-03-2022", true},
}
for _, tc := range cases {
@@ -227,7 +189,7 @@ func TestMacOSUpdatesEnabledForHost(t *testing.T) {
MinimumVersion: optjson.SetString(tc.version),
Deadline: optjson.SetString(tc.deadline),
}
- require.Equal(t, tc.out, m.EnabledForHost(tc.host))
+ require.Equal(t, tc.out, m.Configured())
}
}
diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go
index 5691650265..96b0e89a18 100644
--- a/server/fleet/apple_mdm.go
+++ b/server/fleet/apple_mdm.go
@@ -18,7 +18,7 @@ import (
type MDMAppleCommandIssuer interface {
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
- DeviceLock(ctx context.Context, host *Host, uuid string) error
+ DeviceLock(ctx context.Context, host *Host, uuid string) (unlockPIN string, err error)
EraseDevice(ctx context.Context, host *Host, uuid string) error
InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error
}
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 89d129a439..958db9865c 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -329,6 +329,17 @@ type Datastore interface {
// updated in the given `interval`).
ListIOSAndIPadOSToRefetch(ctx context.Context, refetchInterval time.Duration) (uuids []string, err error)
+ // IsHostConnectedToFleetMDM verifies if the host has an active Fleet MDM enrollment with this server
+ IsHostConnectedToFleetMDM(ctx context.Context, host *Host) (bool, error)
+
+ // AreHostsConnectedToFleetMDM checks each host MDM enrollment with
+ // this server and returns a map indexed by the host uuid and a boolean
+ // indicating if the enrollment is active.
+ //
+ // This function exists to prevent n+1 queries when we need to check
+ // the MDM status of a list of hosts.
+ AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*Host) (map[string]bool, error)
+
AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, time.Time, error)
AggregatedMunkiIssues(ctx context.Context, teamID *uint) ([]AggregatedMunkiIssue, time.Time, error)
AggregatedMDMStatus(ctx context.Context, teamID *uint, platform string) (AggregatedMDMStatus, time.Time, error)
@@ -457,7 +468,7 @@ type Datastore interface {
QueryResultRowsForHost(ctx context.Context, queryID, hostID uint) ([]*ScheduledQueryResultRow, error)
ResultCountForQuery(ctx context.Context, queryID uint) (int, error)
ResultCountForQueryAndHost(ctx context.Context, queryID, hostID uint) (int, error)
- OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow) error
+ OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow, maxQueryReportRows int) error
// CleanupDiscardedQueryResults deletes all query results for queries with DiscardData enabled.
// Used in cleanups_then_aggregation cron to cleanup rows that were inserted immediately
// after DiscardData was set to true due to query caching.
diff --git a/server/fleet/errors.go b/server/fleet/errors.go
index ed6bf27187..c14da53e11 100644
--- a/server/fleet/errors.go
+++ b/server/fleet/errors.go
@@ -538,11 +538,11 @@ func (e OrbitError) Error() string {
const (
// Scripts
RunScriptInvalidTypeErrMsg = "File type not supported. Only .sh (Bash) and .ps1 (PowerShell) file types are allowed."
- RunScriptHostOfflineErrMsg = "Script can’t run on offline host."
- RunScriptHostNotFoundErrMsg = "Host doesn’t exist. Make sure you provide a valid hostname, UUID, osquery host ID, or node key."
- RunScriptForbiddenErrMsg = "You don’t have the right permissions in Fleet to run the script."
+ RunScriptHostOfflineErrMsg = "Script can't run on offline host."
+ RunScriptHostNotFoundErrMsg = "Host doesn't exist. Make sure you provide a valid hostname, UUID, osquery host ID, or node key."
+ RunScriptForbiddenErrMsg = "You don't have the right permissions in Fleet to run the script."
RunScriptAlreadyRunningErrMsg = "A script is already running on this host. Please wait about 5 minutes to let it finish."
- RunScriptHostTimeoutErrMsg = "Fleet didn’t hear back from the host in under 5 minutes (timeout for live scripts). Fleet doesn’t know if the script ran because it didn’t receive the result. Please try again."
+ RunScriptHostTimeoutErrMsg = "Fleet didn't hear back from the host in under 5 minutes (timeout for live scripts). Fleet doesn't know if the script ran because it didn't receive the result. Please try again."
RunScriptScriptsDisabledGloballyErrMsg = "Running scripts is disabled in organization settings."
RunScriptDisabledErrMsg = "Scripts are disabled for this host. To run scripts, deploy the fleetd agent with scripts enabled."
RunScriptsOrbitDisabledErrMsg = "Couldn't run script. To run a script, deploy the fleetd agent with --enable-scripts."
@@ -550,6 +550,7 @@ const (
RunScriptAsyncScriptEnqueuedErrMsg = "Script is running or will run when the host comes online."
RunScripSavedMaxLenErrMsg = "Script is too large. It's limited to 500,000 characters (approximately 10,000 lines)."
RunScripUnsavedMaxLenErrMsg = "Script is too large. It's limited to 10,000 characters (approximately 125 lines)."
+ RunScriptGatewayTimeoutErrMsg = "Gateway timeout. Fleet didn't hear back from the host and doesn't know if the script ran. Please make sure your load balancer timeout isn't shorter than the Fleet server timeout."
// End user authentication
EndUserAuthDEPWebURLConfiguredErrMsg = `End user authentication can't be configured when the configured automatic enrollment (DEP) profile specifies a configuration_web_url.` // #nosec G101
diff --git a/server/fleet/geoip.go b/server/fleet/geoip.go
index 597af53944..d2c73be2a7 100644
--- a/server/fleet/geoip.go
+++ b/server/fleet/geoip.go
@@ -5,8 +5,8 @@ import (
"errors"
"net"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/oschwald/geoip2-golang"
)
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index df445951b6..61f456a066 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -199,6 +199,10 @@ type HostListOptions struct {
// VulnerabilityFilter filters the hosts by the presence of a vulnerability (CVE)
VulnerabilityFilter *string
+
+ // ConnectedToFleetFilter filters hosts that have an active MDM
+ // connection with this Fleet instance.
+ ConnectedToFleetFilter *bool
}
// TODO(Sarah): Are we missing any filters here? Should all MDM filters be included?
@@ -464,6 +468,11 @@ type MDMHostData struct {
// host-returning methods.
DeviceStatus *string `json:"device_status,omitempty" db:"-" csv:"-"`
PendingAction *string `json:"pending_action,omitempty" db:"-" csv:"-"`
+
+ // ConnectedToFleet indicates if the host has an active MDM connection
+ // with this Fleet instance. This boolean is not filled by all
+ // host-returning methods.
+ ConnectedToFleet *bool `json:"connected_to_fleet" csv:"-" db:"connected_to_fleet"`
}
type HostMDMOSSettings struct {
@@ -678,19 +687,20 @@ func (h *Host) IsDEPAssignedToFleet() bool {
// IsEligibleForDEPMigration returns true if the host fulfills all requirements
// for DEP migration from a third-party provider into Fleet.
-func (h *Host) IsEligibleForDEPMigration() bool {
+func (h *Host) IsEligibleForDEPMigration(isConnectedToFleetMDM bool) bool {
return h.IsOsqueryEnrolled() &&
h.IsDEPAssignedToFleet() &&
h.MDMInfo.HasJSONProfileAssigned() &&
- h.MDMInfo.IsEnrolledInThirdPartyMDM()
+ h.MDMInfo.Enrolled &&
+ !isConnectedToFleetMDM
}
// NeedsDEPEnrollment returns true if the host should be DEP enrolled into
// fleet but it's currently unenrolled.
-func (h *Host) NeedsDEPEnrollment() bool {
- return h.MDMInfo != nil && !h.MDMInfo.IsDEPFleetEnrolled() &&
- !h.MDMInfo.IsManualFleetEnrolled() &&
- !h.MDMInfo.IsEnrolledInThirdPartyMDM() &&
+func (h *Host) NeedsDEPEnrollment(isConnectedToFleetMDM bool) bool {
+ return h.MDMInfo != nil &&
+ !h.MDMInfo.Enrolled &&
+ !isConnectedToFleetMDM &&
h.IsDEPAssignedToFleet()
}
@@ -699,18 +709,19 @@ func (h *Host) NeedsDEPEnrollment() bool {
func (h *Host) IsEligibleForWindowsMDMEnrollment() bool {
return h.FleetPlatform() == "windows" &&
h.IsOsqueryEnrolled() &&
- !h.MDMInfo.IsEnrolledInThirdPartyMDM() &&
- !h.MDMInfo.IsFleetEnrolled() &&
- (h.MDMInfo == nil || !h.MDMInfo.IsServer)
+ // NOTE(roberto): during a refactor I found this `h.MDMInfo ==
+ // nil` check, but seems wrong to me, we're assuming that if
+ // there's no data in `host_mdm` it's fine to enroll a host.
+ // I'm leaving it as-is just in case I'm missing something.
+ (h.MDMInfo == nil || (!h.MDMInfo.IsServer && !h.MDMInfo.Enrolled))
}
// IsEligibleForWindowsMDMUnenrollment returns true if the host must be
// unenrolled from Fleet's Windows MDM (if it MDM was disabled).
-func (h *Host) IsEligibleForWindowsMDMUnenrollment() bool {
+func (h *Host) IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM bool) bool {
return h.FleetPlatform() == "windows" &&
h.IsOsqueryEnrolled() &&
- h.MDMInfo.IsFleetEnrolled() &&
- (h.MDMInfo == nil || !h.MDMInfo.IsServer)
+ isConnectedToFleetMDM
}
// IsEligibleForBitLockerEncryption checks if the host needs to enforce disk
@@ -718,7 +729,7 @@ func (h *Host) IsEligibleForWindowsMDMUnenrollment() bool {
//
// Note: the *Host structs needs disk encryption data and MDM data filled in to
// perform the check.
-func (h *Host) IsEligibleForBitLockerEncryption() bool {
+func (h *Host) IsEligibleForBitLockerEncryption(isConnectedToFleetMDM bool) bool {
isServer := h.MDMInfo != nil && h.MDMInfo.IsServer
isWindows := h.FleetPlatform() == "windows"
needsEncryption := h.DiskEncryptionEnabled != nil && !*h.DiskEncryptionEnabled
@@ -726,8 +737,9 @@ func (h *Host) IsEligibleForBitLockerEncryption() bool {
return isWindows &&
h.IsOsqueryEnrolled() &&
- h.MDMInfo.IsFleetEnrolled() &&
+ isConnectedToFleetMDM &&
!isServer &&
+ h.MDMInfo != nil &&
(needsEncryption || encryptedWithoutKey)
}
@@ -948,27 +960,6 @@ type HostMDM struct {
DEPProfileAssignStatus *string `db:"dep_profile_assign_status" json:"-" csv:"-"`
}
-// IsPendingDEPFleetEnrollment returns true if the host's MDM information
-// indicates that it is in pending state for Fleet MDM DEP (automatic)
-// enrollment.
-func (h *HostMDM) IsPendingDEPFleetEnrollment() bool {
- if h == nil {
- return false
- }
- return (!h.IsServer) && (!h.Enrolled) && h.InstalledFromDep &&
- h.Name == WellKnownMDMFleet
-}
-
-// IsEnrolledInThirdPartyMDM returns true if and only if the host's MDM
-// information indicates that the device is currently enrolled into a
-// third-party MDM (an MDM that's not Fleet)
-func (h *HostMDM) IsEnrolledInThirdPartyMDM() bool {
- if h == nil {
- return false
- }
- return h.Enrolled && h.Name != WellKnownMDMFleet
-}
-
// HasJSONProfileAssigned returns true if Fleet has assigned an ADE/DEP JSON
// profile to the host, and it'll be enrolled into Fleet the next time the host
// performs automatic enrollment.
@@ -980,48 +971,6 @@ func (h *HostMDM) HasJSONProfileAssigned() bool {
*h.DEPProfileAssignStatus == string(DEPAssignProfileResponseSuccess)
}
-// IsDEPCapable returns true if and only if the host's MDM information
-// indicates that the device is capable of doing DEP/AEP enrollments.
-func (h *HostMDM) IsDEPCapable() bool {
- if h == nil {
- return false
- }
- // TODO: InstalledFromDep doesn't necessarily mean DEP capable, we need
- // to improve our internal state. See the differences at
- // https://fleetdm.com/tables/mdm
- return !h.IsServer && h.InstalledFromDep
-}
-
-// IsDEPFleetEnrolled returns true if the host's MDM information indicates that
-// it is in enrolled state for Fleet MDM DEP (automatic) enrollment.
-func (h *HostMDM) IsDEPFleetEnrolled() bool {
- if h == nil {
- return false
- }
- return (!h.IsServer) && (h.Enrolled) && h.InstalledFromDep &&
- h.Name == WellKnownMDMFleet
-}
-
-// IsManualFleetEnrolled returns true if the host's MDM information indicates that
-// it is in enrolled state for Fleet MDM manual enrollment.
-func (h *HostMDM) IsManualFleetEnrolled() bool {
- if h == nil {
- return false
- }
- return (!h.IsServer) && (h.Enrolled) && !h.InstalledFromDep &&
- h.Name == WellKnownMDMFleet
-}
-
-// IsFleetEnrolled returns true if the host's MDM information indicates that
-// it is in enrolled state for Fleet MDM, regardless of automatic or manual
-// enrollment method.
-func (h *HostMDM) IsFleetEnrolled() bool {
- if h == nil {
- return false
- }
- return h.IsDEPFleetEnrolled() || h.IsManualFleetEnrolled()
-}
-
// HostMunkiIssue represents a single munki issue for a host.
type HostMunkiIssue struct {
MunkiIssueID uint `db:"munki_issue_id" json:"id"`
@@ -1235,6 +1184,7 @@ type OSVersion struct {
type HostDetailOptions struct {
IncludeCVEScores bool
IncludePolicies bool
+ ExcludeSoftware bool
}
// EnrollHostLimiter defines the methods to support enforcement of enrolled
diff --git a/server/fleet/hosts_test.go b/server/fleet/hosts_test.go
index a5d0def83f..104c941dd0 100644
--- a/server/fleet/hosts_test.go
+++ b/server/fleet/hosts_test.go
@@ -214,60 +214,8 @@ func TestMDMEnrollmentStatus(t *testing.T) {
}
}
-func TestIsEnrolledInThirdPartyMDM(t *testing.T) {
- for _, tc := range []struct {
- hostMDM HostMDM
- expected bool
- }{
- {
- hostMDM: HostMDM{Enrolled: true, Name: WellKnownMDMSimpleMDM},
- expected: true,
- },
- {
- hostMDM: HostMDM{Enrolled: false, Name: WellKnownMDMSimpleMDM},
- expected: false,
- },
- {
- hostMDM: HostMDM{Enrolled: true, Name: WellKnownMDMFleet},
- expected: false,
- },
- {
- hostMDM: HostMDM{Enrolled: false, Name: WellKnownMDMFleet},
- expected: false,
- },
- } {
- require.Equal(t, tc.expected, tc.hostMDM.IsEnrolledInThirdPartyMDM())
- }
-}
-
-func TestIsDEPCapable(t *testing.T) {
- for _, tc := range []struct {
- hostMDM HostMDM
- expected bool
- }{
- {
- hostMDM: HostMDM{IsServer: false, InstalledFromDep: true},
- expected: true,
- },
- {
- hostMDM: HostMDM{IsServer: true, InstalledFromDep: true},
- expected: false,
- },
- {
- hostMDM: HostMDM{IsServer: true, InstalledFromDep: false},
- expected: false,
- },
- {
- hostMDM: HostMDM{IsServer: false, InstalledFromDep: false},
- expected: false,
- },
- } {
- require.Equal(t, tc.expected, tc.hostMDM.IsDEPCapable())
- }
-}
-
func TestIsEligibleForBitLockerEncryption(t *testing.T) {
- require.False(t, (&Host{}).IsEligibleForBitLockerEncryption())
+ require.False(t, (&Host{}).IsEligibleForBitLockerEncryption(false))
hostThatNeedsEnforcement := Host{
Platform: "windows",
@@ -283,37 +231,34 @@ func TestIsEligibleForBitLockerEncryption(t *testing.T) {
},
DiskEncryptionEnabled: ptr.Bool(false),
}
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
// macOS hosts are not elegible
hostThatNeedsEnforcement.Platform = "darwin"
- require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
hostThatNeedsEnforcement.Platform = "windows"
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
// hosts with disk encryption already enabled are elegible only if we
// can't decrypt the key
hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(true)
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = true
- require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(false)
hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = false
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
// hosts without MDMinfo are not elegible
oldMDMInfo := hostThatNeedsEnforcement.MDMInfo
hostThatNeedsEnforcement.MDMInfo = nil
- require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
hostThatNeedsEnforcement.MDMInfo = oldMDMInfo
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(true))
// hosts that are not enrolled in MDM are not elegible
- hostThatNeedsEnforcement.MDMInfo.Enrolled = false
- require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
- hostThatNeedsEnforcement.MDMInfo.Enrolled = true
- require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
+ require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption(false))
}
func TestIsEligibleForDEPMigration(t *testing.T) {
@@ -403,7 +348,7 @@ func TestIsEligibleForDEPMigration(t *testing.T) {
},
}
- require.Equal(t, tc.expected, host.IsEligibleForDEPMigration())
+ require.Equal(t, tc.expected, host.IsEligibleForDEPMigration(false))
})
}
}
diff --git a/server/fleet/osquery.go b/server/fleet/osquery.go
index 23722ccddb..9e11d2bc15 100644
--- a/server/fleet/osquery.go
+++ b/server/fleet/osquery.go
@@ -18,8 +18,7 @@ type Stats struct {
const (
// StatusOK is the success code returned by osquery
- StatusOK OsqueryStatus = 0
- MaxQueryReportRows int = 1000
+ StatusOK OsqueryStatus = 0
)
// QueryContent is the format of a query stanza in an osquery configuration.
diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go
index 6f803f28c1..bd86ce7532 100644
--- a/server/fleet/scripts.go
+++ b/server/fleet/scripts.go
@@ -249,6 +249,12 @@ type HostScriptResult struct {
// execution. It is otherwise not part of the host_script_results table and
// not returned as part of the resulting JSON.
Hostname string `json:"-" db:"-"`
+
+ // HostDeletedAt indicates if the results are associated with a deleted host.
+ // This supports the soft-delete feature for script results so that the
+ // results can still be returned to see activity details after the host got
+ // deleted.
+ HostDeletedAt *time.Time `json:"-" db:"host_deleted_at"`
}
func (hsr HostScriptResult) AuthzType() string {
@@ -297,8 +303,10 @@ const (
)
// anchored, so that it matches to the end of the line
-var scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/z?sh(?:\s*|\s+.*)$`)
-var ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh."`)
+var (
+ scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/z?sh(?:\s*|\s+.*)$`)
+ ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh."`)
+)
// ValidateShebang validates if we support a script, and whether we
// can execute it directly, or need to pass it to a shell interpreter.
@@ -396,7 +404,7 @@ type HostLockWipeStatus struct {
}
func (s *HostLockWipeStatus) IsPendingLock() bool {
- if s.HostFleetPlatform == "darwin" {
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// pending lock if an MDM command is queued but no result received yet
return s.LockMDMCommand != nil && s.LockMDMCommandResult == nil
}
@@ -405,9 +413,9 @@ func (s *HostLockWipeStatus) IsPendingLock() bool {
}
func (s HostLockWipeStatus) IsPendingUnlock() bool {
- if s.HostFleetPlatform == "darwin" {
- // pending unlock if an unlock was requested
- return !s.UnlockRequestedAt.IsZero()
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
+ // Apple MDM does not have a concept of pending unlock.
+ return false
}
// pending unlock if script execution request is queued but no result yet
return s.UnlockScript != nil && s.UnlockScript.ExitCode == nil
@@ -426,7 +434,7 @@ func (s HostLockWipeStatus) IsLocked() bool {
// this state is regardless of pending unlock/wipe (it reports whether the
// host is locked *now*).
- if s.HostFleetPlatform == "darwin" {
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// locked if an MDM command was sent and succeeded
return s.LockMDMCommand != nil && s.LockMDMCommandResult != nil &&
s.LockMDMCommandResult.Status == MDMAppleStatusAcknowledged
@@ -452,7 +460,7 @@ func (s HostLockWipeStatus) IsWiped() bool {
// wiped if an MDM command was sent and succeeded
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
strings.HasPrefix(s.WipeMDMCommandResult.Status, "2")
- case "darwin":
+ case "darwin", "ios", "ipados":
// wiped if an MDM command was sent and succeeded
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
s.WipeMDMCommandResult.Status == MDMAppleStatusAcknowledged
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 732202b139..b493b1bff0 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -106,7 +106,8 @@ type Service interface {
CreateUserFromInvite(ctx context.Context, p UserPayload) (user *User, err error)
// CreateUser allows an admin to create a new user without first creating and validating invite tokens.
- CreateUser(ctx context.Context, p UserPayload) (user *User, err error)
+ // The sessionKey is only returned (not-nil) when creating API-only (non-SSO) users.
+ CreateUser(ctx context.Context, p UserPayload) (user *User, sessionKey *string, err error)
// CreateInitialUser creates the first user, skipping authorization checks. If a user already exists this method
// should fail.
@@ -274,12 +275,13 @@ type Service interface {
// included in the results.
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error)
GetQuery(ctx context.Context, id uint) (*Query, error)
- // GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to
- GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error)
+ // GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to.
+ // Returns a boolean indicating whether the report is clipped.
+ GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, bool, error)
// GetHostQueryReportResults returns all stored results of a query for a specific host
GetHostQueryReportResults(ctx context.Context, hid uint, queryID uint) (rows []HostQueryReportResult, lastFetched *time.Time, err error)
// QueryReportIsClipped returns true if the number of query report rows exceeds the maximum
- QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error)
+ QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error)
NewQuery(ctx context.Context, p QueryPayload) (*Query, error)
ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error)
DeleteQuery(ctx context.Context, teamID *uint, name string) error
@@ -1040,7 +1042,7 @@ type Service interface {
BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeTmName *string, payloads []ScriptPayload, dryRun bool) error
// Script-based methods (at least for some platforms, MDM-based for others)
- LockHost(ctx context.Context, hostID uint) error
+ LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
WipeHost(ctx context.Context, hostID uint) error
diff --git a/server/fleet/teams.go b/server/fleet/teams.go
index 984d295343..ebdf8a7eb1 100644
--- a/server/fleet/teams.go
+++ b/server/fleet/teams.go
@@ -419,7 +419,7 @@ type TeamSpec struct {
// set to the agent options JSON object.
AgentOptions json.RawMessage `json:"agent_options,omitempty"` // marshals as "null" if omitempty is not set
HostExpirySettings *HostExpirySettings `json:"host_expiry_settings,omitempty"`
- Secrets []EnrollSecret `json:"secrets,omitempty"`
+ Secrets *[]EnrollSecret `json:"secrets,omitempty"`
Features *json.RawMessage `json:"features"`
MDM TeamSpecMDM `json:"mdm"`
Scripts optjson.Slice[string] `json:"scripts"`
@@ -486,7 +486,7 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
Name: t.Name,
AgentOptions: agentOptions,
Features: &featuresJSON,
- Secrets: secrets,
+ Secrets: &secrets,
MDM: mdmSpec,
HostExpirySettings: &t.Config.HostExpirySettings,
WebhookSettings: webhookSettings,
diff --git a/server/health/health.go b/server/health/health.go
index 9f710e297a..b027a8e14e 100644
--- a/server/health/health.go
+++ b/server/health/health.go
@@ -4,7 +4,7 @@ package health
import (
"net/http"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
// Checker returns an error indicating if a service is in an unhealthy state.
diff --git a/server/health/health_test.go b/server/health/health_test.go
index 17deee69b8..6889f5ef57 100644
--- a/server/health/health_test.go
+++ b/server/health/health_test.go
@@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/server/launcher/launcher.go b/server/launcher/launcher.go
index bd0b780943..007ac423c9 100644
--- a/server/launcher/launcher.go
+++ b/server/launcher/launcher.go
@@ -17,7 +17,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/health"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/kolide/launcher/pkg/service"
"github.com/osquery/osquery-go/plugin/distributed"
"github.com/osquery/osquery-go/plugin/logger"
diff --git a/server/launcher/launcher_test.go b/server/launcher/launcher_test.go
index 2ade7727f2..6837bd6eb1 100644
--- a/server/launcher/launcher_test.go
+++ b/server/launcher/launcher_test.go
@@ -8,7 +8,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/health"
"github.com/fleetdm/fleet/v4/server/service/mock"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/kolide/launcher/pkg/service"
"github.com/osquery/osquery-go/plugin/distributed"
"github.com/stretchr/testify/assert"
diff --git a/server/launcher/server.go b/server/launcher/server.go
index ae8b9f1dbf..fb54360dcf 100644
--- a/server/launcher/server.go
+++ b/server/launcher/server.go
@@ -5,8 +5,8 @@ import (
"net/http"
"strings"
- "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
+ "github.com/go-kit/log"
launcher "github.com/kolide/launcher/pkg/service"
grpc "google.golang.org/grpc"
diff --git a/server/logging/filesystem.go b/server/logging/filesystem.go
index 0c65f3b2ca..8d87a28100 100644
--- a/server/logging/filesystem.go
+++ b/server/logging/filesystem.go
@@ -14,7 +14,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/secure"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
diff --git a/server/logging/filesystem_test.go b/server/logging/filesystem_test.go
index 9146d9af5e..aaccbb557b 100644
--- a/server/logging/filesystem_test.go
+++ b/server/logging/filesystem_test.go
@@ -10,7 +10,7 @@ import (
"path/filepath"
"testing"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/server/logging/firehose.go b/server/logging/firehose.go
index 313e4d5cad..783f38f842 100644
--- a/server/logging/firehose.go
+++ b/server/logging/firehose.go
@@ -16,8 +16,8 @@ import (
"github.com/aws/aws-sdk-go/service/firehose"
"github.com/aws/aws-sdk-go/service/firehose/firehoseiface"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
const (
diff --git a/server/logging/firehose_test.go b/server/logging/firehose_test.go
index 531acc332c..bf1e97df70 100644
--- a/server/logging/firehose_test.go
+++ b/server/logging/firehose_test.go
@@ -11,7 +11,7 @@ import (
"github.com/aws/aws-sdk-go/service/firehose"
"github.com/aws/aws-sdk-go/service/firehose/firehoseiface"
"github.com/fleetdm/fleet/v4/server/logging/mock"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
)
diff --git a/server/logging/kinesis.go b/server/logging/kinesis.go
index 1faa98eefa..3b304fbb52 100644
--- a/server/logging/kinesis.go
+++ b/server/logging/kinesis.go
@@ -17,8 +17,8 @@ import (
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/aws/aws-sdk-go/service/kinesis/kinesisiface"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
const (
diff --git a/server/logging/kinesis_test.go b/server/logging/kinesis_test.go
index 3e230aa8c0..f5e32cfc35 100644
--- a/server/logging/kinesis_test.go
+++ b/server/logging/kinesis_test.go
@@ -14,7 +14,7 @@ import (
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/aws/aws-sdk-go/service/kinesis/kinesisiface"
"github.com/fleetdm/fleet/v4/server/logging/mock"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
)
diff --git a/server/logging/lambda.go b/server/logging/lambda.go
index afeb645a8e..61ec67b836 100644
--- a/server/logging/lambda.go
+++ b/server/logging/lambda.go
@@ -11,8 +11,8 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
const (
diff --git a/server/logging/lambda_test.go b/server/logging/lambda_test.go
index fbf2563d4f..64ab9ff7c2 100644
--- a/server/logging/lambda_test.go
+++ b/server/logging/lambda_test.go
@@ -10,7 +10,7 @@ import (
"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
"github.com/fleetdm/fleet/v4/server/logging/mock"
"github.com/fleetdm/fleet/v4/server/test"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
tmock "github.com/stretchr/testify/mock"
)
diff --git a/server/logging/logging.go b/server/logging/logging.go
index 712525db90..95512b6e08 100644
--- a/server/logging/logging.go
+++ b/server/logging/logging.go
@@ -5,8 +5,8 @@ import (
"fmt"
"github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
type FilesystemConfig struct {
diff --git a/server/logging/nanodep.go b/server/logging/nanodep.go
index e7361da3f2..7e35c11094 100644
--- a/server/logging/nanodep.go
+++ b/server/logging/nanodep.go
@@ -2,7 +2,7 @@ package logging
import (
nanodep_log "github.com/fleetdm/fleet/v4/server/mdm/nanodep/log"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
diff --git a/server/logging/pubsub.go b/server/logging/pubsub.go
index dedd23af35..3405f3b176 100644
--- a/server/logging/pubsub.go
+++ b/server/logging/pubsub.go
@@ -8,8 +8,8 @@ import (
"cloud.google.com/go/pubsub"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
type pubSubLogWriter struct {
diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go
index 0e9f79982d..69280f82f4 100644
--- a/server/mdm/apple/apple_mdm.go
+++ b/server/mdm/apple/apple_mdm.go
@@ -91,7 +91,7 @@ type DEPService struct {
// getDefaultProfile returns a godep.Profile with default values set.
func (d *DEPService) getDefaultProfile() *godep.Profile {
return &godep.Profile{
- ProfileName: "FleetDM default enrollment profile",
+ ProfileName: "Fleet default enrollment profile",
AllowPairing: true,
AutoAdvanceSetup: false,
IsSupervised: false,
@@ -688,8 +688,8 @@ var enrollmentProfileMobileconfigTemplate = template.Must(template.New("").Parse
{{ .SCEPURL }}
Subject
- O FleetDM
- CN FleetDM Identity
+ O Fleet
+ CN Fleet Identity
PayloadIdentifier
diff --git a/server/mdm/apple/cert.go b/server/mdm/apple/cert.go
index ec47d0d438..a0393122d2 100644
--- a/server/mdm/apple/cert.go
+++ b/server/mdm/apple/cert.go
@@ -20,7 +20,7 @@ import (
const (
defaultFleetDMAPIURL = "https://fleetdm.com"
getSignedAPNSCSRPath = "/api/v1/deliver-apple-csr"
- depCertificateCommonName = "FleetDM"
+ depCertificateCommonName = "Fleet"
depCertificateExpiryDays = 30
)
@@ -208,7 +208,7 @@ func NewSCEPCACertKey() (*x509.Certificate, *rsa.PrivateKey, error) {
caCert := depot.NewCACert(
depot.WithYears(10),
- depot.WithCommonName("FleetDM"),
+ depot.WithCommonName("Fleet"),
)
crtBytes, err := caCert.SelfSign(rand.Reader, key.Public(), key)
diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go
index 0afb0666b2..fbacedfe4a 100644
--- a/server/mdm/apple/commander.go
+++ b/server/mdm/apple/commander.go
@@ -90,8 +90,8 @@ func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []str
return ctxerr.Wrap(ctx, err, "commander remove profile")
}
-func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) error {
- pin := GenerateRandomPin(6)
+func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) (unlockPIN string, err error) {
+ unlockPIN = GenerateRandomPin(6)
raw := fmt.Sprintf(`
@@ -106,22 +106,23 @@ func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host,
%s
- `, uuid, pin)
+`, uuid, unlockPIN,
+ )
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
- return ctxerr.Wrap(ctx, err, "decoding command")
+ return "", ctxerr.Wrap(ctx, err, "decoding command")
}
- if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, pin); err != nil {
- return ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
+ if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, unlockPIN); err != nil {
+ return "", ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
}
if err := svc.sendNotifications(ctx, []string{host.UUID}); err != nil {
- return ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
+ return "", ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
}
- return nil
+ return unlockPIN, nil
}
func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error {
diff --git a/server/mdm/apple/commander_test.go b/server/mdm/apple/commander_test.go
index 361aece383..5978944b52 100644
--- a/server/mdm/apple/commander_test.go
+++ b/server/mdm/apple/commander_test.go
@@ -132,8 +132,9 @@ func TestMDMAppleCommander(t *testing.T) {
require.Len(t, pin, 6)
return nil
}
- err = cmdr.DeviceLock(ctx, host, cmdUUID)
+ pin, err := cmdr.DeviceLock(ctx, host, cmdUUID)
require.NoError(t, err)
+ require.Len(t, pin, 6)
require.True(t, mdmStorage.EnqueueDeviceLockCommandFuncInvoked)
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
diff --git a/server/mdm/nanomdm/http/api/api.go b/server/mdm/nanomdm/http/api/api.go
index 5649470f85..6cdcda6893 100644
--- a/server/mdm/nanomdm/http/api/api.go
+++ b/server/mdm/nanomdm/http/api/api.go
@@ -145,6 +145,11 @@ func RawCommandEnqueueHandler(enqueuer storage.CommandEnqueuer, pusher push.Push
b, err := mdmhttp.ReadAllAndReplaceBody(r)
if err != nil {
logger.Info("msg", "reading body", "err", err)
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout)
+ return
+ }
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@@ -299,6 +304,11 @@ func StorePushCertHandler(storage storage.PushCertStore, logger log.Logger) http
b, err := mdmhttp.ReadAllAndReplaceBody(r)
if err != nil {
logger.Info("msg", "reading body", "err", err)
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout)
+ return
+ }
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
diff --git a/server/mdm/nanomdm/http/http_test.go b/server/mdm/nanomdm/http/http_test.go
new file mode 100644
index 0000000000..26fe47dc4f
--- /dev/null
+++ b/server/mdm/nanomdm/http/http_test.go
@@ -0,0 +1,54 @@
+package http
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestHTTPServerTimeoutError(t *testing.T) {
+ // ensure that a read timeout error is properly detected
+ srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ code := http.StatusOK
+ if _, err := io.ReadAll(r.Body); err != nil {
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ code = http.StatusRequestTimeout
+ } else {
+ code = http.StatusInternalServerError
+ }
+ }
+ w.WriteHeader(code)
+ }))
+
+ srv.Config.ReadTimeout = time.Second
+ srv.Start()
+ defer srv.Close()
+
+ req, err := http.NewRequest("POST", srv.URL, slowReader{b: []byte("slowly send this")})
+ require.NoError(t, err)
+ res, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ code := res.StatusCode
+ require.Equal(t, http.StatusRequestTimeout, code)
+}
+
+type slowReader struct {
+ b []byte
+}
+
+func (s slowReader) Read(p []byte) (n int, err error) {
+ if len(s.b) == 0 {
+ return 0, io.EOF
+ }
+
+ time.Sleep(200 * time.Millisecond)
+ n = copy(p, s.b[:len(s.b)/2])
+ s.b = s.b[n:]
+ return n, nil
+}
diff --git a/server/mdm/nanomdm/http/mdm/mdm.go b/server/mdm/nanomdm/http/mdm/mdm.go
index a3bd21796a..bad75bce2b 100644
--- a/server/mdm/nanomdm/http/mdm/mdm.go
+++ b/server/mdm/nanomdm/http/mdm/mdm.go
@@ -32,6 +32,11 @@ func CheckinHandler(svc service.Checkin, logger log.Logger) http.HandlerFunc {
bodyBytes, err := mdmhttp.ReadAllAndReplaceBody(r)
if err != nil {
logger.Info("msg", "reading body", "err", err)
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout)
+ return
+ }
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@@ -56,6 +61,11 @@ func CommandAndReportResultsHandler(svc service.CommandAndReportResults, logger
bodyBytes, err := mdmhttp.ReadAllAndReplaceBody(r)
if err != nil {
logger.Info("msg", "reading body", "err", err)
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout)
+ return
+ }
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
diff --git a/server/mdm/nanomdm/http/mdm/mdm_cert.go b/server/mdm/nanomdm/http/mdm/mdm_cert.go
index 69ef246ef6..93308ec841 100644
--- a/server/mdm/nanomdm/http/mdm/mdm_cert.go
+++ b/server/mdm/nanomdm/http/mdm/mdm_cert.go
@@ -3,6 +3,7 @@ package mdm
import (
"context"
"crypto/x509"
+ "errors"
"net/http"
"net/url"
@@ -84,6 +85,11 @@ func CertExtractMdmSignatureMiddleware(next http.Handler, logger log.Logger) htt
b, err := mdmhttp.ReadAllAndReplaceBody(r)
if err != nil {
logger.Info("msg", "reading body", "err", err)
+ var toErr interface{ Timeout() bool }
+ if errors.As(err, &toErr) && toErr.Timeout() {
+ http.Error(w, http.StatusText(http.StatusRequestTimeout), http.StatusRequestTimeout)
+ return
+ }
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
diff --git a/server/mdm/scep/client/client.go b/server/mdm/scep/client/client.go
index 1c0254d7b1..3664301def 100644
--- a/server/mdm/scep/client/client.go
+++ b/server/mdm/scep/client/client.go
@@ -3,8 +3,8 @@ package scepclient
import (
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// Client is a SCEP Client
diff --git a/server/mdm/scep/cmd/scepclient/scepclient.go b/server/mdm/scep/cmd/scepclient/scepclient.go
index c45d1decc0..91175ffb81 100644
--- a/server/mdm/scep/cmd/scepclient/scepclient.go
+++ b/server/mdm/scep/cmd/scepclient/scepclient.go
@@ -20,8 +20,8 @@ import (
scepclient "github.com/fleetdm/fleet/v4/server/mdm/scep/client"
"github.com/fleetdm/fleet/v4/server/mdm/scep/scep"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// version info
diff --git a/server/mdm/scep/cmd/scepserver/scepserver.go b/server/mdm/scep/cmd/scepserver/scepserver.go
index 85385bc107..8bdb5225ff 100644
--- a/server/mdm/scep/cmd/scepserver/scepserver.go
+++ b/server/mdm/scep/cmd/scepserver/scepserver.go
@@ -21,8 +21,8 @@ import (
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
"github.com/gorilla/mux"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// version info
diff --git a/server/mdm/scep/csrverifier/executable/csrverifier.go b/server/mdm/scep/csrverifier/executable/csrverifier.go
index 8cda685d47..73f3d31111 100644
--- a/server/mdm/scep/csrverifier/executable/csrverifier.go
+++ b/server/mdm/scep/csrverifier/executable/csrverifier.go
@@ -6,7 +6,7 @@ import (
"os"
"os/exec"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
const (
diff --git a/server/mdm/scep/scep/scep.go b/server/mdm/scep/scep/scep.go
index e28927d3f0..25fa1de349 100644
--- a/server/mdm/scep/scep/scep.go
+++ b/server/mdm/scep/scep/scep.go
@@ -17,8 +17,8 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil"
"github.com/fleetdm/fleet/v4/server/mdm/scep/cryptoutil/x509util"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"go.mozilla.org/pkcs7"
)
diff --git a/server/mdm/scep/server/endpoint.go b/server/mdm/scep/server/endpoint.go
index 205d834b07..799d410865 100644
--- a/server/mdm/scep/server/endpoint.go
+++ b/server/mdm/scep/server/endpoint.go
@@ -9,8 +9,8 @@ import (
"time"
"github.com/go-kit/kit/endpoint"
- "github.com/go-kit/kit/log"
httptransport "github.com/go-kit/kit/transport/http"
+ "github.com/go-kit/log"
)
// possible SCEP operations
diff --git a/server/mdm/scep/server/service.go b/server/mdm/scep/server/service.go
index 9b56e57a34..85d01910d3 100644
--- a/server/mdm/scep/server/service.go
+++ b/server/mdm/scep/server/service.go
@@ -8,7 +8,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/scep/scep"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
// Service is the interface for all supported SCEP server operations.
diff --git a/server/mdm/scep/server/service_logging.go b/server/mdm/scep/server/service_logging.go
index c67cd24fa0..364901e3f4 100644
--- a/server/mdm/scep/server/service_logging.go
+++ b/server/mdm/scep/server/service_logging.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
type loggingService struct {
diff --git a/server/mdm/scep/server/transport.go b/server/mdm/scep/server/transport.go
index b3cb928575..69f704e3c8 100644
--- a/server/mdm/scep/server/transport.go
+++ b/server/mdm/scep/server/transport.go
@@ -11,8 +11,8 @@ import (
"net/http"
"net/url"
- kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
+ kitlog "github.com/go-kit/log"
"github.com/gorilla/mux"
"github.com/groob/finalizer/logutil"
)
diff --git a/server/mdm/scep/server/transport_test.go b/server/mdm/scep/server/transport_test.go
index ff8d51b786..05bdc7f9ee 100644
--- a/server/mdm/scep/server/transport_test.go
+++ b/server/mdm/scep/server/transport_test.go
@@ -17,7 +17,7 @@ import (
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
"github.com/gorilla/mux"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
)
func TestCACaps(t *testing.T) {
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index 32b3a98910..0d43ec085b 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -241,6 +241,10 @@ type GetHostMDMCheckinInfoFunc func(ctx context.Context, hostUUID string) (*flee
type ListIOSAndIPadOSToRefetchFunc func(ctx context.Context, refetchInterval time.Duration) (uuids []string, err error)
+type IsHostConnectedToFleetMDMFunc func(ctx context.Context, host *fleet.Host) (bool, error)
+
+type AreHostsConnectedToFleetMDMFunc func(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error)
+
type AggregatedMunkiVersionFunc func(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error)
type AggregatedMunkiIssuesFunc func(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiIssue, time.Time, error)
@@ -339,7 +343,7 @@ type ResultCountForQueryFunc func(ctx context.Context, queryID uint) (int, error
type ResultCountForQueryAndHostFunc func(ctx context.Context, queryID uint, hostID uint) (int, error)
-type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error
+type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error
type CleanupDiscardedQueryResultsFunc func(ctx context.Context) error
@@ -1299,6 +1303,12 @@ type DataStore struct {
ListIOSAndIPadOSToRefetchFunc ListIOSAndIPadOSToRefetchFunc
ListIOSAndIPadOSToRefetchFuncInvoked bool
+ IsHostConnectedToFleetMDMFunc IsHostConnectedToFleetMDMFunc
+ IsHostConnectedToFleetMDMFuncInvoked bool
+
+ AreHostsConnectedToFleetMDMFunc AreHostsConnectedToFleetMDMFunc
+ AreHostsConnectedToFleetMDMFuncInvoked bool
+
AggregatedMunkiVersionFunc AggregatedMunkiVersionFunc
AggregatedMunkiVersionFuncInvoked bool
@@ -3165,6 +3175,20 @@ func (s *DataStore) ListIOSAndIPadOSToRefetch(ctx context.Context, refetchInterv
return s.ListIOSAndIPadOSToRefetchFunc(ctx, refetchInterval)
}
+func (s *DataStore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) {
+ s.mu.Lock()
+ s.IsHostConnectedToFleetMDMFuncInvoked = true
+ s.mu.Unlock()
+ return s.IsHostConnectedToFleetMDMFunc(ctx, host)
+}
+
+func (s *DataStore) AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*fleet.Host) (map[string]bool, error) {
+ s.mu.Lock()
+ s.AreHostsConnectedToFleetMDMFuncInvoked = true
+ s.mu.Unlock()
+ return s.AreHostsConnectedToFleetMDMFunc(ctx, hosts)
+}
+
func (s *DataStore) AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]fleet.AggregatedMunkiVersion, time.Time, error) {
s.mu.Lock()
s.AggregatedMunkiVersionFuncInvoked = true
@@ -3508,11 +3532,11 @@ func (s *DataStore) ResultCountForQueryAndHost(ctx context.Context, queryID uint
return s.ResultCountForQueryAndHostFunc(ctx, queryID, hostID)
}
-func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
s.mu.Lock()
s.OverwriteQueryResultRowsFuncInvoked = true
s.mu.Unlock()
- return s.OverwriteQueryResultRowsFunc(ctx, rows)
+ return s.OverwriteQueryResultRowsFunc(ctx, rows, maxQueryReportRows)
}
func (s *DataStore) CleanupDiscardedQueryResults(ctx context.Context) error {
diff --git a/server/policies/failing_policies.go b/server/policies/failing_policies.go
index b92201548e..ef3c7ce063 100644
--- a/server/policies/failing_policies.go
+++ b/server/policies/failing_policies.go
@@ -9,8 +9,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// FailingPolicyAutomationType is the type of automations supported for
diff --git a/server/policies/failing_policies_test.go b/server/policies/failing_policies_test.go
index 8a4573da8e..bd68bf7334 100644
--- a/server/policies/failing_policies_test.go
+++ b/server/policies/failing_policies_test.go
@@ -11,7 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
diff --git a/server/service/activities.go b/server/service/activities.go
index 2e4aeac78f..cdab1837f1 100644
--- a/server/service/activities.go
+++ b/server/service/activities.go
@@ -5,13 +5,14 @@ import (
"encoding/json"
"errors"
"fmt"
+ "net/http"
+ "time"
+
"github.com/cenkalti/backoff/v4"
"github.com/fleetdm/fleet/v4/server"
kithttp "github.com/go-kit/kit/transport/http"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
- "net/http"
- "time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index 4c90801bc1..458d9920ff 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -24,7 +24,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/version"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
)
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 9799f6e7e0..23f8d5d673 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -1423,6 +1423,12 @@ func (svc *Service) EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co
return ctxerr.Wrap(ctx, err, "getting host info for mdm apple remove profile command")
}
+ if h.Platform == "ios" || h.Platform == "ipados" {
+ return &fleet.BadRequestError{
+ Message: "Can't turn off MDM for iOS or iPadOS hosts. Use wipe instead.",
+ }
+ }
+
info, err := svc.ds.GetHostMDMCheckinInfo(ctx, h.UUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting mdm checkin info for mdm apple remove profile command")
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index d553fbb182..5bcca2234b 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -44,7 +44,7 @@ import (
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/groob/plist"
micromdm "github.com/micromdm/micromdm/mdm/mdm"
diff --git a/server/service/async/async.go b/server/service/async/async.go
index 5f341b09d9..d75162dc87 100644
--- a/server/service/async/async.go
+++ b/server/service/async/async.go
@@ -10,8 +10,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/datastore/redis"
"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"
redigo "github.com/gomodule/redigo/redis"
)
diff --git a/server/service/client.go b/server/service/client.go
index 69a1537f0d..54c5defdde 100644
--- a/server/service/client.go
+++ b/server/service/client.go
@@ -303,45 +303,84 @@ func (c *Client) runAppConfigChecks(fn func(ac *fleet.EnrichedAppConfig) error)
// getProfilesContents takes file paths and creates a slice of profile payloads
// ready to batch-apply.
-func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec, expandEnv bool) ([]fleet.MDMProfileBatchPayload, error) {
- // map to check for duplicate names
- extByName := make(map[string]string, len(profiles))
- result := make([]fleet.MDMProfileBatchPayload, 0, len(profiles))
+func getProfilesContents(baseDir string, macProfiles []fleet.MDMProfileSpec, windowsProfiles []fleet.MDMProfileSpec, expandEnv bool) ([]fleet.MDMProfileBatchPayload, error) {
+ // map to check for duplicate names across all profiles
+ extByName := make(map[string]string, len(macProfiles))
+ result := make([]fleet.MDMProfileBatchPayload, 0, len(macProfiles))
- for _, profile := range profiles {
- filePath := resolveApplyRelativePath(baseDir, profile.Path)
- fileContents, err := os.ReadFile(filePath)
- if err != nil {
- return nil, fmt.Errorf("applying fleet config: %w", err)
- }
-
- if expandEnv {
- fileContents, err = spec.ExpandEnvBytes(fileContents)
+ // iterate over the profiles for each platform
+ for platform, profiles := range map[string][]fleet.MDMProfileSpec{
+ "macos": macProfiles,
+ "windows": windowsProfiles,
+ } {
+ for _, profile := range profiles {
+ filePath := resolveApplyRelativePath(baseDir, profile.Path)
+ fileContents, err := os.ReadFile(filePath)
if err != nil {
- return nil, fmt.Errorf("expanding environment on file %q: %w", profile.Path, err)
+ return nil, fmt.Errorf("applying custom settings: %w", err)
}
- }
- // by default, use the file name. macOS profiles use their PayloadDisplayName
- ext := filepath.Ext(filePath)
- name := strings.TrimSuffix(filepath.Base(filePath), ext)
- if mdm.GetRawProfilePlatform(fileContents) == "darwin" && ext == ".mobileconfig" {
- mc, err := fleet.NewMDMAppleConfigProfile(fileContents, nil)
- if err != nil {
- return nil, fmt.Errorf("applying fleet config: %w", err)
+ if expandEnv {
+ fileContents, err = spec.ExpandEnvBytes(fileContents)
+ if err != nil {
+ return nil, fmt.Errorf("expanding environment on file %q: %w", profile.Path, err)
+ }
}
- name = strings.TrimSpace(mc.Name)
- }
- if e, isDuplicate := extByName[name]; isDuplicate {
- return nil, errors.New(fmtDuplicateNameErrMsg(name, e, ext))
- }
- extByName[name] = ext
- result = append(result, fleet.MDMProfileBatchPayload{
- Name: name,
- Contents: fileContents,
- Labels: profile.Labels,
- })
+ ext := filepath.Ext(filePath)
+ // by default, use the file name (for macOS mobileconfig profiles, we'll switch to
+ // their PayloadDisplayName when we parse the profile below)
+ name := strings.TrimSuffix(filepath.Base(filePath), ext)
+ // for validation errors, we want to include the platform and file name in the error message
+ prefixErrMsg := fmt.Sprintf("Couldn't edit %s_settings.custom_settings (%s%s)", platform, name, ext)
+
+ // validate macOS profiles
+ if platform == "macos" {
+ switch ext {
+ case ".mobileconfig", ".xml": // allowing .xml for backwards compatibility
+ mc, err := fleet.NewMDMAppleConfigProfile(fileContents, nil)
+ if err != nil {
+ errForMsg := errors.Unwrap(err)
+ if errForMsg == nil {
+ errForMsg = err
+ }
+ return nil, fmt.Errorf("%s: %w", prefixErrMsg, errForMsg)
+ }
+ name = strings.TrimSpace(mc.Name)
+ case ".json":
+ if mdm.GetRawProfilePlatform(fileContents) != "darwin" {
+ return nil, fmt.Errorf("%s: %s", prefixErrMsg, "Declaration profiles should include valid JSON.")
+ }
+ default:
+ return nil, fmt.Errorf("%s: %s", prefixErrMsg, "macOS configuration profiles must be .mobileconfig or .json files.")
+ }
+ }
+
+ // validate windows profiles
+ if platform == "windows" {
+ switch ext {
+ case ".xml":
+ if mdm.GetRawProfilePlatform(fileContents) != "windows" {
+ return nil, fmt.Errorf("%s: %s", prefixErrMsg, "Windows configuration profiles can only have or top level elements")
+ }
+ default:
+ return nil, fmt.Errorf("%s: %s", prefixErrMsg, "Windows configuration profiles must be .xml files.")
+ }
+ }
+
+ // check for duplicate names across all profiles
+ if e, isDuplicate := extByName[name]; isDuplicate {
+ return nil, errors.New(fmtDuplicateNameErrMsg(name, e, ext))
+ }
+ extByName[name] = ext
+
+ result = append(result, fleet.MDMProfileBatchPayload{
+ Name: name,
+ Contents: fileContents,
+ Labels: profile.Labels,
+ })
+
+ }
}
return result, nil
}
@@ -421,7 +460,6 @@ func (c *Client) ApplyGroup(
if specs.AppConfig != nil {
windowsCustomSettings := extractAppCfgWindowsCustomSettings(specs.AppConfig)
macosCustomSettings := extractAppCfgMacOSCustomSettings(specs.AppConfig)
- allCustomSettings := append(macosCustomSettings, windowsCustomSettings...)
// if there is no custom setting but the windows and mac settings are
// non-nil, this means that we want to clear the existing custom settings,
@@ -430,8 +468,8 @@ func (c *Client) ApplyGroup(
// TODO(mna): shouldn't that be an || instead of && ? I.e. if there are no
// custom settings but windows is present and empty (but mac is absent),
// shouldn't that clear the windows ones?
- if (windowsCustomSettings != nil && macosCustomSettings != nil) || len(allCustomSettings) > 0 {
- fileContents, err := getProfilesContents(baseDir, allCustomSettings, opts.ExpandEnvConfigProfiles)
+ if (windowsCustomSettings != nil && macosCustomSettings != nil) || len(windowsCustomSettings)+len(macosCustomSettings) > 0 {
+ fileContents, err := getProfilesContents(baseDir, macosCustomSettings, windowsCustomSettings, opts.ExpandEnvConfigProfiles)
if err != nil {
return nil, err
}
@@ -520,10 +558,10 @@ func (c *Client) ApplyGroup(
tmMDMSettings := extractTmSpecsMDMCustomSettings(specs.Teams)
tmFileContents := make(map[string][]fleet.MDMProfileBatchPayload, len(tmMDMSettings))
- for k, paths := range tmMDMSettings {
- fileContents, err := getProfilesContents(baseDir, paths, opts.ExpandEnvConfigProfiles)
+ for k, profileSpecs := range tmMDMSettings {
+ fileContents, err := getProfilesContents(baseDir, profileSpecs.macos, profileSpecs.windows, opts.ExpandEnvConfigProfiles)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("Team %s: %w", k, err) // TODO: consider adding team name to improve error messages generally for other parts of the config because multiple team configs can be processed at once
}
tmFileContents[k] = fileContents
}
@@ -837,9 +875,14 @@ func extractAppCfgScripts(appCfg interface{}) []string {
return scriptsStrings
}
+type profileSpecsByPlatform struct {
+ macos []fleet.MDMProfileSpec
+ windows []fleet.MDMProfileSpec
+}
+
// returns the custom macOS and Windows settings keyed by team name.
-func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]fleet.MDMProfileSpec {
- var m map[string][]fleet.MDMProfileSpec
+func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string]profileSpecsByPlatform {
+ var m map[string]profileSpecsByPlatform
for _, tm := range tmSpecs {
var spec struct {
Name string `json:"name"`
@@ -866,7 +909,7 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]fle
if len(spec.MDM.MacOSSettings.CustomSettings) > 0 ||
len(spec.MDM.WindowsSettings.CustomSettings) > 0 {
if m == nil {
- m = make(map[string][]fleet.MDMProfileSpec)
+ m = make(map[string]profileSpecsByPlatform)
}
}
@@ -894,8 +937,16 @@ func extractTmSpecsMDMCustomSettings(tmSpecs []json.RawMessage) map[string][]fle
}
// TODO: validate equal names here and API?
+ var result profileSpecsByPlatform
+ if macOSSettings != nil {
+ result.macos = macOSSettings
+ }
+ if windowsSettings != nil {
+ result.windows = windowsSettings
+ }
+
if macOSSettings != nil || windowsSettings != nil {
- m[spec.Name] = append(macOSSettings, windowsSettings...)
+ m[spec.Name] = result
}
}
}
diff --git a/server/service/client_scripts.go b/server/service/client_scripts.go
index 18285013fd..373b3b1ebd 100644
--- a/server/service/client_scripts.go
+++ b/server/service/client_scripts.go
@@ -57,6 +57,11 @@ func (c *Client) runHostScript(verb, path string, hostID uint, scriptContents []
return nil, errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
}
return nil, errors.New(fleet.RunScriptForbiddenErrMsg)
+ // It's possible we get a GatewayTimeout error message from nginx or another
+ // proxy server, so we want to return a more helpful error message in that
+ // case.
+ case http.StatusGatewayTimeout:
+ return nil, errors.New(fleet.RunScriptGatewayTimeoutErrMsg)
case http.StatusPaymentRequired:
if teamID > 0 {
return nil, errors.New("Team id parameter requires Fleet Premium license.")
diff --git a/server/service/client_test.go b/server/service/client_test.go
index f84baca68e..7eb7314da1 100644
--- a/server/service/client_test.go
+++ b/server/service/client_test.go
@@ -282,7 +282,7 @@ func TestExtractTeamSpecsMDMCustomSettings(t *testing.T) {
cases := []struct {
desc string
yaml string
- want map[string][]fleet.MDMProfileSpec
+ want map[string]profileSpecsByPlatform
}{
{
"no settings",
@@ -342,7 +342,7 @@ spec:
windows_settings:
custom_settings:
`,
- map[string][]fleet.MDMProfileSpec{"Fleet": {}, "Fleet2": {}},
+ map[string]profileSpecsByPlatform{"Fleet": {windows: []fleet.MDMProfileSpec{}, macos: []fleet.MDMProfileSpec{}}, "Fleet2": {windows: []fleet.MDMProfileSpec{}, macos: []fleet.MDMProfileSpec{}}},
},
{
"custom settings specified",
@@ -368,11 +368,15 @@ spec:
- "foo"
- baz
`,
- map[string][]fleet.MDMProfileSpec{"Fleet": {
- {Path: "a", Labels: []string{"foo", "bar"}},
- {Path: "b"},
- {Path: "c"},
- {Path: "d", Labels: []string{"foo", "baz"}},
+ map[string]profileSpecsByPlatform{"Fleet": {
+ macos: []fleet.MDMProfileSpec{
+ {Path: "a", Labels: []string{"foo", "bar"}},
+ {Path: "b"},
+ },
+ windows: []fleet.MDMProfileSpec{
+ {Path: "c"},
+ {Path: "d", Labels: []string{"foo", "baz"}},
+ },
}},
},
{
@@ -393,7 +397,13 @@ spec:
- "c"
- "d"
`,
- map[string][]fleet.MDMProfileSpec{"Fleet": {{Path: "a"}, {Path: "b"}, {Path: "c"}, {Path: "d"}}},
+ map[string]profileSpecsByPlatform{"Fleet": {
+ macos: []fleet.MDMProfileSpec{{Path: "a"}, {Path: "b"}},
+ windows: []fleet.MDMProfileSpec{
+ {Path: "c"},
+ {Path: "d"},
+ },
+ }},
},
{
"invalid custom settings",
@@ -423,7 +433,7 @@ spec:
- path: 24
- path: "y"
`,
- map[string][]fleet.MDMProfileSpec{},
+ map[string]profileSpecsByPlatform{},
},
{
"old invalid custom settings",
@@ -447,7 +457,7 @@ spec:
- 24
- "y"
`,
- map[string][]fleet.MDMProfileSpec{},
+ map[string]profileSpecsByPlatform{},
},
}
for _, c := range cases {
@@ -455,8 +465,13 @@ spec:
specs, err := spec.GroupFromBytes([]byte(c.yaml))
require.NoError(t, err)
if len(specs.Teams) > 0 {
- got := extractTmSpecsMDMCustomSettings(specs.Teams)
- require.Equal(t, c.want, got)
+ gotSpecs := extractTmSpecsMDMCustomSettings(specs.Teams)
+ for k, wantProfs := range c.want {
+ gotProfs, ok := gotSpecs[k]
+ require.True(t, ok)
+ require.Equal(t, wantProfs.macos, gotProfs.macos)
+ require.Equal(t, wantProfs.windows, gotProfs.windows)
+ }
}
})
}
@@ -502,19 +517,21 @@ func TestGetProfilesContents(t *testing.T) {
`
tests := []struct {
- name string
- baseDir string
- setupFiles [][2]string
- labels []string
- environment map[string]string
- expandEnv bool
- expectError bool
- want []fleet.MDMProfileBatchPayload
+ name string
+ baseDir string
+ macSetupFiles [][2]string
+ winSetupFiles [][2]string
+ labels []string
+ environment map[string]string
+ expandEnv bool
+ expectError bool
+ want []fleet.MDMProfileBatchPayload
+ wantErr string
}{
{
name: "invalid darwin xml",
baseDir: tempDir,
- setupFiles: [][2]string{
+ macSetupFiles: [][2]string{
{"foo.mobileconfig", ``},
},
expectError: true,
@@ -523,10 +540,12 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "windows and darwin files",
baseDir: tempDir,
- setupFiles: [][2]string{
- {"foo.xml", string(windowsProfile)},
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", string(darwinProfile)},
},
+ winSetupFiles: [][2]string{
+ {"foo.xml", string(windowsProfile)},
+ },
expectError: false,
want: []fleet.MDMProfileBatchPayload{
{Name: "foo", Contents: windowsProfile},
@@ -536,10 +555,12 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "windows and darwin files with labels",
baseDir: tempDir,
- setupFiles: [][2]string{
- {"foo.xml", string(windowsProfile)},
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", string(darwinProfile)},
},
+ winSetupFiles: [][2]string{
+ {"foo.xml", string(windowsProfile)},
+ },
labels: []string{"foo", "bar"},
expectError: false,
want: []fleet.MDMProfileBatchPayload{
@@ -550,10 +571,12 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "darwin files with file name != PayloadDisplayName",
baseDir: tempDir,
- setupFiles: [][2]string{
- {"foo.xml", string(windowsProfile)},
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", string(darwinProfile)},
},
+ winSetupFiles: [][2]string{
+ {"foo.xml", string(windowsProfile)},
+ },
expectError: false,
want: []fleet.MDMProfileBatchPayload{
{Name: "foo", Contents: windowsProfile},
@@ -563,16 +586,18 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "duplicate names across windows and darwin",
baseDir: tempDir,
- setupFiles: [][2]string{
- {"baz.xml", string(windowsProfile)},
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", string(mobileconfigForTest("baz", "I"))},
},
+ winSetupFiles: [][2]string{
+ {"baz.xml", string(windowsProfile)},
+ },
expectError: true,
},
{
name: "duplicate file names",
baseDir: tempDir,
- setupFiles: [][2]string{
+ winSetupFiles: [][2]string{
{"baz.xml", string(windowsProfile)},
{"baz.xml", string(windowsProfile)},
},
@@ -581,8 +606,10 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "with environment variables",
baseDir: tempDir,
- setupFiles: [][2]string{
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", darwinProfileWithFooEnv},
+ },
+ winSetupFiles: [][2]string{
{"foo.xml", windowsProfileWithBarEnv},
},
environment: map[string]string{"FOO": "42", "BAR": "24"},
@@ -634,14 +661,43 @@ func TestGetProfilesContents(t *testing.T) {
{
name: "with environment variables but not set",
baseDir: tempDir,
- setupFiles: [][2]string{
+ macSetupFiles: [][2]string{
{"bar.mobileconfig", darwinProfileWithFooEnv},
+ },
+ winSetupFiles: [][2]string{
{"foo.xml", windowsProfileWithBarEnv},
},
environment: map[string]string{},
expandEnv: true,
expectError: true,
},
+ {
+ name: "with unprocessable json",
+ baseDir: tempDir,
+ macSetupFiles: [][2]string{
+ {"bar.json", string(windowsProfile)},
+ },
+ expectError: true,
+ wantErr: "Couldn't edit macos_settings.custom_settings (bar.json): Declaration profiles should include valid JSON",
+ },
+ {
+ name: "with unprocessable xml",
+ baseDir: tempDir,
+ winSetupFiles: [][2]string{
+ {"bar.xml", string(darwinProfile)},
+ },
+ expectError: true,
+ wantErr: "Couldn't edit windows_settings.custom_settings (bar.xml): Windows configuration profiles can only have or top level elements",
+ },
+ {
+ name: "with unsupported extension",
+ baseDir: tempDir,
+ macSetupFiles: [][2]string{
+ {"bar.cfg", string(darwinProfile)},
+ },
+ expectError: true,
+ wantErr: "Couldn't edit macos_settings.custom_settings (bar.cfg): macOS configuration profiles must be .mobileconfig or .json files",
+ },
}
for _, tt := range tests {
@@ -658,17 +714,27 @@ func TestGetProfilesContents(t *testing.T) {
})
}
}
- paths := []fleet.MDMProfileSpec{}
- for _, fileSpec := range tt.setupFiles {
+ macPaths := []fleet.MDMProfileSpec{}
+ for _, fileSpec := range tt.macSetupFiles {
filePath := filepath.Join(tempDir, fileSpec[0])
require.NoError(t, os.WriteFile(filePath, []byte(fileSpec[1]), 0o644))
- paths = append(paths, fleet.MDMProfileSpec{Path: filePath, Labels: tt.labels})
+ macPaths = append(macPaths, fleet.MDMProfileSpec{Path: filePath, Labels: tt.labels})
}
- profileContents, err := getProfilesContents(tt.baseDir, paths, tt.expandEnv)
+ winPaths := []fleet.MDMProfileSpec{}
+ for _, fileSpec := range tt.winSetupFiles {
+ filePath := filepath.Join(tempDir, fileSpec[0])
+ require.NoError(t, os.WriteFile(filePath, []byte(fileSpec[1]), 0o644))
+ winPaths = append(winPaths, fleet.MDMProfileSpec{Path: filePath, Labels: tt.labels})
+ }
+
+ profileContents, err := getProfilesContents(tt.baseDir, macPaths, winPaths, tt.expandEnv)
if tt.expectError {
require.Error(t, err)
+ if tt.wantErr != "" {
+ require.Contains(t, err.Error(), tt.wantErr)
+ }
} else {
require.NoError(t, err)
require.NotNil(t, profileContents)
diff --git a/server/service/client_users.go b/server/service/client_users.go
index aedf807f77..6845997515 100644
--- a/server/service/client_users.go
+++ b/server/service/client_users.go
@@ -8,11 +8,16 @@ import (
)
// CreateUser creates a new user, skipping the invitation process.
-func (c *Client) CreateUser(p fleet.UserPayload) error {
+//
+// The session key (aka API token) is returned only when creating
+// API only users.
+func (c *Client) CreateUser(p fleet.UserPayload) (*string, error) {
verb, path := "POST", "/api/latest/fleet/users/admin"
var responseBody createUserResponse
-
- return c.authenticatedRequest(p, verb, path, &responseBody)
+ if err := c.authenticatedRequest(p, verb, path, &responseBody); err != nil {
+ return nil, err
+ }
+ return responseBody.Token, nil
}
// ListUsers retrieves the list of users.
diff --git a/server/service/debug_handler.go b/server/service/debug_handler.go
index 6ee4644dda..15800ff9e4 100644
--- a/server/service/debug_handler.go
+++ b/server/service/debug_handler.go
@@ -13,9 +13,9 @@ import (
"github.com/fleetdm/fleet/v4/server/errorstore"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
kithttp "github.com/go-kit/kit/transport/http"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/gorilla/mux"
)
diff --git a/server/service/devices_test.go b/server/service/devices_test.go
index 3fc3ba127c..0aa0ae34a6 100644
--- a/server/service/devices_test.go
+++ b/server/service/devices_test.go
@@ -110,6 +110,10 @@ func TestGetFleetDesktopSummary(t *testing.T) {
return &appCfg, nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return false, nil
+ }
+
ctx := test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
DEPAssignedToFleet: &c.depAssigned,
@@ -202,6 +206,9 @@ func TestGetFleetDesktopSummary(t *testing.T) {
appCfg.MDM = c.mdm
return &appCfg, nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return false, nil
+ }
ctx = test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
@@ -243,6 +250,10 @@ func TestGetFleetDesktopSummary(t *testing.T) {
return &appCfg, nil
}
+ 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
+ }
+
cases := []struct {
name string
host *fleet.Host
diff --git a/server/service/endpoint_campaigns.go b/server/service/endpoint_campaigns.go
index c961677e74..bad6d9e270 100644
--- a/server/service/endpoint_campaigns.go
+++ b/server/service/endpoint_campaigns.go
@@ -12,7 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/websocket"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
gws "github.com/gorilla/websocket"
"github.com/igm/sockjs-go/v3/sockjs"
)
diff --git a/server/service/endpoint_middleware.go b/server/service/endpoint_middleware.go
index a69e28bf31..1de0c6ec38 100644
--- a/server/service/endpoint_middleware.go
+++ b/server/service/endpoint_middleware.go
@@ -8,9 +8,9 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
kithttp "github.com/go-kit/kit/transport/http"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
diff --git a/server/service/endpoint_middleware_test.go b/server/service/endpoint_middleware_test.go
index a722c3e293..c4d7891b1c 100644
--- a/server/service/endpoint_middleware_test.go
+++ b/server/service/endpoint_middleware_test.go
@@ -9,7 +9,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
- 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/server/service/endpoint_setup.go b/server/service/endpoint_setup.go
index ca9a6434cf..9e54050ffa 100644
--- a/server/service/endpoint_setup.go
+++ b/server/service/endpoint_setup.go
@@ -7,8 +7,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/kit/endpoint"
- 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"
)
type setupRequest struct {
diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go
index 97ca541b87..13d4f00568 100644
--- a/server/service/endpoint_utils.go
+++ b/server/service/endpoint_utils.go
@@ -20,8 +20,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/kit/endpoint"
- "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
+ "github.com/go-kit/log"
"github.com/gorilla/mux"
)
diff --git a/server/service/endpoint_utils_test.go b/server/service/endpoint_utils_test.go
index 82cd016c62..95307747bc 100644
--- a/server/service/endpoint_utils_test.go
+++ b/server/service/endpoint_utils_test.go
@@ -15,8 +15,8 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/kit/endpoint"
- kitlog "github.com/go-kit/kit/log"
kithttp "github.com/go-kit/kit/transport/http"
+ kitlog "github.com/go-kit/log"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/service/frontend.go b/server/service/frontend.go
index 0bbdcaed2b..f5d884ec1f 100644
--- a/server/service/frontend.go
+++ b/server/service/frontend.go
@@ -7,7 +7,7 @@ import (
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/fleetdm/fleet/v4/server/bindata"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
func newBinaryFileSystem(root string) *assetfs.AssetFS {
diff --git a/server/service/handler_test.go b/server/service/handler_test.go
index 69d0525759..faaf9d0ae5 100644
--- a/server/service/handler_test.go
+++ b/server/service/handler_test.go
@@ -12,7 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/mock"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 530c7fd829..d9c6ab9edb 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -479,7 +479,8 @@ func (svc *Service) SearchHosts(ctx context.Context, matchQuery string, queryID
/////////////////////////////////////////////////////////////////////////////////
type getHostRequest struct {
- ID uint `url:"id"`
+ ID uint `url:"id"`
+ ExcludeSoftware bool `query:"exclude_software,optional"`
}
type getHostResponse struct {
@@ -493,7 +494,8 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
req := request.(*getHostRequest)
opts := fleet.HostDetailOptions{
IncludeCVEScores: false,
- IncludePolicies: true, // intentionally true to preserve existing behavior
+ IncludePolicies: true, // intentionally true to preserve existing behavior,
+ ExcludeSoftware: req.ExcludeSoftware,
}
host, err := svc.GetHost(ctx, req.ID, opts)
if err != nil {
@@ -1009,8 +1011,10 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
}
func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
- if err := svc.ds.LoadHostSoftware(ctx, host, opts.IncludeCVEScores); err != nil {
- return nil, ctxerr.Wrap(ctx, err, "load host software")
+ if !opts.ExcludeSoftware {
+ if err := svc.ds.LoadHostSoftware(ctx, host, opts.IncludeCVEScores); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "load host software")
+ }
}
labels, err := svc.ds.ListLabelsForHost(ctx, host.ID)
@@ -1227,7 +1231,12 @@ func getHostQueryReportEndpoint(ctx context.Context, request interface{}, svc fl
return getHostQueryReportResponse{Err: err}, nil
}
- isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID)
+ appConfig, err := svc.AppConfigObfuscated(ctx)
+ if err != nil {
+ return getHostQueryReportResponse{Err: err}, nil
+ }
+
+ isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID, appConfig.ServerSettings.GetQueryReportCap())
if err != nil {
return getHostQueryReportResponse{Err: err}, nil
}
diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go
index bc07f5d7d2..6be6d51ce6 100644
--- a/server/service/hosts_test.go
+++ b/server/service/hosts_test.go
@@ -1560,6 +1560,9 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
ds.UnlockHostManuallyFunc = func(ctx context.Context, hostID uint, platform string, ts time.Time) error {
return nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return true, nil
+ }
cases := []struct {
name string
@@ -1648,9 +1651,9 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
}
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
- err := svc.LockHost(ctx, globalHostID)
+ _, err := svc.LockHost(ctx, globalHostID)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
- err = svc.LockHost(ctx, teamHostID)
+ _, err = svc.LockHost(ctx, teamHostID)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
// Pretend we locked the host
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 1f7b1feca3..a7f4ee4caa 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -33,7 +33,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/ghodss/yaml"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
@@ -281,6 +281,44 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
require.True(t, found)
}
+func (s *integrationTestSuite) TestCreatingAPIOnlyUserReturnsAPIToken() {
+ t := s.T()
+
+ defer func() {
+ s.token = s.getTestAdminToken()
+ }()
+
+ var createResp createUserResponse
+ params := fleet.UserPayload{
+ Name: ptr.String("someadmin"),
+ Email: ptr.String("someadmin@example.com"),
+ Password: ptr.String(test.GoodPassword),
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ APIOnly: ptr.Bool(false),
+ }
+ s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
+ assert.NotZero(t, createResp.User.ID)
+ assert.Nil(t, createResp.Token)
+
+ params = fleet.UserPayload{
+ Name: ptr.String("apionly"),
+ Email: ptr.String("apionly@example.com"),
+ Password: ptr.String(test.GoodPassword),
+ GlobalRole: ptr.String(fleet.RoleObserver),
+ APIOnly: ptr.Bool(true),
+ // AdminForcedPasswordReset is set to false when creating api-only users via `fleetctl user create --api-only`.
+ AdminForcedPasswordReset: ptr.Bool(false),
+ }
+ s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
+ assert.NotZero(t, createResp.User.ID)
+ assert.NotNil(t, createResp.Token)
+
+ s.token = *createResp.Token
+ var chr countHostsResponse
+ s.DoJSON("GET", "/api/latest/fleet/hosts/count", countHostsRequest{}, http.StatusOK, &chr)
+ assert.Equal(t, 0, chr.Count)
+}
+
func (s *integrationTestSuite) TestActivityUserEmailPersistsAfterDeletion() {
t := s.T()
@@ -6012,7 +6050,7 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
// apply team specs
var specResp applyTeamSpecsResponse
- teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: []fleet.EnrollSecret{{Secret: "ABC"}}}}}
+ teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: &[]fleet.EnrollSecret{{Secret: "ABC"}}}}}
s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusPaymentRequired, &specResp)
// modify team agent options
@@ -7911,6 +7949,12 @@ func (s *integrationTestSuite) TestGetHostSoftwareUpdatedAt() {
require.Equal(t, host.ID, getHostResp.Host.ID)
require.Len(t, getHostResp.Host.Software, len(software))
require.Greater(t, getHostResp.Host.SoftwareUpdatedAt, getHostResp.Host.CreatedAt)
+
+ getHostResp = getHostResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp, "exclude_software", "true")
+ require.Equal(t, host.ID, getHostResp.Host.ID)
+ require.Empty(t, getHostResp.Host.Software)
+ require.Greater(t, getHostResp.Host.SoftwareUpdatedAt, getHostResp.Host.CreatedAt)
}
func (s *integrationTestSuite) TestHostsReportDownload() {
@@ -10793,12 +10837,14 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
+ require.False(t, gqrr.ReportClipped)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
+ require.False(t, gqrr.ReportClipped)
// don't change platform or min_osquery_version and results should not be deleted
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
@@ -10806,6 +10852,7 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
+ require.False(t, gqrr.ReportClipped)
// now update the platform and results should be deleted.
osqueryInfoQuerySpec.Platform = "darwin"
@@ -10814,30 +10861,35 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
+ require.False(t, gqrr.ReportClipped)
// Update logging type, which should cause results deletion
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
+ require.False(t, gqrr.ReportClipped)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
+ require.False(t, gqrr.ReportClipped)
discardData := true
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{DiscardData: &discardData}}, http.StatusOK, &modifyQueryResp)
require.True(t, modifyQueryResp.Query.DiscardData)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
+ require.False(t, gqrr.ReportClipped)
// check that now that discardData is set, we don't add new results
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
+ require.False(t, gqrr.ReportClipped)
// Verify that we can't have more than 1k results
@@ -10849,7 +10901,7 @@ func (s *integrationTestSuite) TestQueryReports() {
NodeKey: *host1Global.NodeKey,
LogType: "result",
Data: json.RawMessage(`[{
- "snapshot": [` + results(1000, host1Global.UUID) + `
+ "snapshot": [` + results(fleet.DefaultMaxQueryReportRows, host1Global.UUID) + `
],
"action": "snapshot",
"name": "pack/Global/` + osqueryInfoQuery.Name + `",
@@ -10872,13 +10924,14 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
- require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
+ require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
+ require.True(t, gqrr.ReportClipped)
ghqrr = getHostQueryReportResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/queries/%d", host1Global.ID, osqueryInfoQuery.ID), getHostQueryReportRequest{}, http.StatusOK, &ghqrr)
require.NoError(t, ghqrr.Err)
+ require.Len(t, ghqrr.Results, fleet.DefaultMaxQueryReportRows)
require.True(t, ghqrr.ReportClipped)
- require.Len(t, ghqrr.Results, fleet.MaxQueryReportRows)
slreq.Data = json.RawMessage(`[{
"snapshot": [` + results(1, host1Global.UUID) + `
@@ -10900,7 +10953,41 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
- require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
+ require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
+ require.True(t, gqrr.ReportClipped)
+
+ appConfigSpec := map[string]map[string]int{
+ "server_settings": {"query_report_cap": fleet.DefaultMaxQueryReportRows + 1},
+ }
+ s.Do("PATCH", "/api/latest/fleet/config", appConfigSpec, http.StatusOK)
+
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
+ require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
+ require.False(t, gqrr.ReportClipped)
+
+ slreq.Data = json.RawMessage(`[{
+ "snapshot": [` + results(1002, host1Global.UUID) + `
+ ],
+ "action": "snapshot",
+ "name": "pack/Global/` + osqueryInfoQuery.Name + `",
+ "hostIdentifier": "` + *host1Global.OsqueryHostID + `",
+ "calendarTime": "Fri Oct 6 18:13:04 2023 UTC",
+ "unixTime": 1696615984,
+ "epoch": 0,
+ "counter": 0,
+ "numerics": false,
+ "decorations": {
+ "host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd",
+ "hostname": "` + host1Global.Hostname + `"
+ }
+}]`)
+
+ s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
+ require.NoError(t, slres.Err)
+
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
+ require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows+1)
+ require.True(t, gqrr.ReportClipped)
// TODO: Set global discard flag and verify that all data is gone.
}
@@ -11265,8 +11352,9 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
endTime = mysql.SetOrderedCreatedAtTimestamps(t, s.ds, endTime, "host_software_installs", "execution_id", h1Foo)
mysql.SetOrderedCreatedAtTimestamps(t, s.ds, endTime, "host_script_results", "execution_id", h1C, h1D, h1E)
- // modify the timestamp h1A and h1B to simulate an script that has
- // been pending for a long time
+ // modify the timestamp h1A and h1B to simulate an script that has been
+ // pending for a long time (h1A is a sync request, so it will be ignored for
+ // upcoming activities)
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "UPDATE host_script_results SET created_at = ? WHERE execution_id IN (?, ?)", time.Now().Add(-24*time.Hour), h1A, h1B)
return err
@@ -11323,7 +11411,7 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
queryArgs := c.queries
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", host1.ID), nil, http.StatusOK, &listResp, queryArgs...)
- require.Equal(t, uint(6), listResp.Count)
+ require.Equal(t, uint(5), listResp.Count)
require.Equal(t, len(c.wantExecs), len(listResp.Activities))
require.Equal(t, c.wantMeta, listResp.Meta)
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 4009752a0f..dff33643a0 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -9423,6 +9423,9 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
+
+ // download the installer, not found anymore
+ s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusNotFound, "team_id", fmt.Sprintf("%d", *payload.TeamID))
})
}
@@ -10142,6 +10145,109 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), lastActID)
}
+func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
+ t := s.T()
+ ctx := context.Background()
+
+ // create a host and request a script execution
+ tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "host_soft_delete_team"})
+ require.NoError(t, err)
+ host := createOrbitEnrolledHost(t, "linux", "", s.ds)
+ err = s.ds.AddHostsToTeam(ctx, &tm.ID, []uint{host.ID})
+ require.NoError(t, err)
+
+ // create an anonymous script execution request
+ var runResp runScriptResponse
+ s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusAccepted, &runResp)
+ scriptExecID := runResp.ExecutionID
+
+ // post a script result so that the (past) activity is created
+ s.Do("POST", "/api/fleet/orbit/scripts/result",
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *host.OrbitNodeKey, scriptExecID)),
+ http.StatusOK)
+ s.lastActivityOfTypeMatches(
+ fleet.ActivityTypeRanScript{}.ActivityName(),
+ fmt.Sprintf(
+ `{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true}`,
+ host.ID, host.DisplayName(), scriptExecID), 0)
+
+ // create a saved script execution request
+ var newScriptResp createScriptResponse
+ body, headers := generateNewScriptMultipartRequest(t,
+ "script1.sh", []byte(`echo "hello"`), s.token, map[string][]string{"team_id": {strconv.Itoa(int(tm.ID))}})
+ res := s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusOK, headers)
+ err = json.NewDecoder(res.Body).Decode(&newScriptResp)
+ require.NoError(t, err)
+ require.NotZero(t, newScriptResp.ScriptID)
+ savedScriptID := newScriptResp.ScriptID
+
+ s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptID: &savedScriptID}, http.StatusAccepted, &runResp)
+ savedScriptExecID := runResp.ExecutionID
+
+ // post a script result so that the (past) activity is created
+ s.Do("POST", "/api/fleet/orbit/scripts/result",
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "saved"}`, *host.OrbitNodeKey, savedScriptExecID)),
+ http.StatusOK)
+ s.lastActivityOfTypeMatches(
+ fleet.ActivityTypeRanScript{}.ActivityName(),
+ fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true}`,
+ host.ID, host.DisplayName(), savedScriptExecID), 0)
+
+ // get the anoymous script result details
+ var scriptRes getScriptResultResponse
+ s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+scriptExecID, nil, http.StatusOK, &scriptRes)
+ require.Equal(t, scriptExecID, scriptRes.ExecutionID)
+ require.Equal(t, host.ID, scriptRes.HostID)
+ require.Equal(t, "ok", scriptRes.Output)
+ require.NotNil(t, scriptRes.ExitCode)
+ require.EqualValues(t, 0, *scriptRes.ExitCode)
+
+ // get the saved script result details
+ scriptRes = getScriptResultResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+savedScriptExecID, nil, http.StatusOK, &scriptRes)
+ require.Equal(t, savedScriptExecID, scriptRes.ExecutionID)
+ require.Equal(t, host.ID, scriptRes.HostID)
+ require.Equal(t, "saved", scriptRes.Output)
+ require.NotNil(t, scriptRes.ExitCode)
+ require.EqualValues(t, 0, *scriptRes.ExitCode)
+
+ // delete the host
+ var deleteResp deleteHostResponse
+ s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &deleteResp)
+
+ // get the anonymous script result details, still works
+ scriptRes = getScriptResultResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+scriptExecID, nil, http.StatusOK, &scriptRes)
+ require.Equal(t, scriptExecID, scriptRes.ExecutionID)
+ require.Equal(t, host.ID, scriptRes.HostID)
+ require.Equal(t, "ok", scriptRes.Output)
+ require.NotNil(t, scriptRes.ExitCode)
+ require.EqualValues(t, 0, *scriptRes.ExitCode)
+
+ // get the saved script result details, still works
+ scriptRes = getScriptResultResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+savedScriptExecID, nil, http.StatusOK, &scriptRes)
+ require.Equal(t, savedScriptExecID, scriptRes.ExecutionID)
+ require.Equal(t, host.ID, scriptRes.HostID)
+ require.Equal(t, "saved", scriptRes.Output)
+ require.NotNil(t, scriptRes.ExitCode)
+ require.EqualValues(t, 0, *scriptRes.ExitCode)
+
+ // delete the named script
+ s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/scripts/%d", savedScriptID), nil, http.StatusNoContent)
+
+ // get the saved script result details, still works because the saved script
+ // is a "soft-reference", when deleted the results become essentially results
+ // for an anonymous script (i.e. the script_id FK is "ON DELETE SET NULL").
+ scriptRes = getScriptResultResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+savedScriptExecID, nil, http.StatusOK, &scriptRes)
+ require.Equal(t, savedScriptExecID, scriptRes.ExecutionID)
+ require.Equal(t, host.ID, scriptRes.HostID)
+ require.Equal(t, "saved", scriptRes.Output)
+ require.NotNil(t, scriptRes.ExitCode)
+ require.EqualValues(t, 0, *scriptRes.ExitCode)
+}
+
func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(payload *fleet.UploadSoftwareInstallerPayload, expectedStatus int, expectedError string) {
t := s.T()
openFile := func(name string) *os.File {
diff --git a/server/service/integration_live_queries_test.go b/server/service/integration_live_queries_test.go
index 037487f9ec..944ea650bf 100644
--- a/server/service/integration_live_queries_test.go
+++ b/server/service/integration_live_queries_test.go
@@ -22,7 +22,7 @@ import (
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
diff --git a/server/service/integration_logger_test.go b/server/service/integration_logger_test.go
index 783fe7aa88..8831d2e304 100644
--- a/server/service/integration_logger_test.go
+++ b/server/service/integration_logger_test.go
@@ -15,8 +15,8 @@ import (
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
diff --git a/server/service/integration_mdm_lifecycle_test.go b/server/service/integration_mdm_lifecycle_test.go
index 75fad197f0..72bcb19bf0 100644
--- a/server/service/integration_mdm_lifecycle_test.go
+++ b/server/service/integration_mdm_lifecycle_test.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
+ "github.com/stretchr/testify/assert"
"net/http"
"os"
"path/filepath"
@@ -73,12 +74,15 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() {
{
"locked host turns on MDM",
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
- s.Do(
+ var resp lockHostResponse
+ s.DoJSON(
"POST",
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
nil,
- http.StatusNoContent,
+ http.StatusOK,
+ &resp,
)
+ assert.Len(t, resp.UnlockPIN, 6)
cmd, err := device.Idle()
require.NoError(t, err)
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 4f95a5395d..c25604d5e3 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -71,7 +71,7 @@ import (
func TestIntegrationsMDM(t *testing.T) {
testingSuite := new(integrationMDMTestSuite)
- testingSuite.s = &testingSuite.Suite
+ testingSuite.withServer.s = &testingSuite.Suite
suite.Run(t, testingSuite)
}
@@ -531,9 +531,9 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
var mdmResp getAppleMDMResponse
s.DoJSON("GET", "/api/latest/fleet/apns", nil, http.StatusOK, &mdmResp)
// returned values are dummy, this is a test certificate
- require.Equal(t, "FleetDM", mdmResp.Issuer)
+ require.Equal(t, "Fleet", mdmResp.Issuer)
require.NotZero(t, mdmResp.SerialNumber)
- require.Equal(t, "FleetDM", mdmResp.CommonName)
+ require.Equal(t, "Fleet", mdmResp.CommonName)
require.NotZero(t, mdmResp.RenewDate)
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -2316,7 +2316,7 @@ func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
// create an enroll secret for the team
teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
Name: tm.Name,
- Secrets: []fleet.EnrollSecret{{Secret: t.Name() + "team-secret"}},
+ Secrets: &[]fleet.EnrollSecret{{Secret: t.Name() + "team-secret"}},
}}}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
@@ -5223,6 +5223,9 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() {
}
}
+ // turn on MDM for a host
+ orbitHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
+
// disable Microsoft MDM
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
"mdm": { "windows_enabled_and_configured": false }
@@ -5230,16 +5233,11 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() {
assert.False(t, acResp.MDM.WindowsEnabledAndConfigured)
s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledWindowsMDM{}.ActivityName(), `{}`, 0)
- // set the win-no-team host as enrolled in Windows MDM
- noTeamHost := hostsBySuffix["win-no-team"]
- err = s.ds.SetOrUpdateMDMData(ctx, noTeamHost.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "")
- require.NoError(t, err)
-
// get the orbit config for win-no-team should return true for the
// unenrollment notification
var resp orbitGetConfigResponse
s.DoJSON("POST", "/api/fleet/orbit/config",
- json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *noTeamHost.OrbitNodeKey)),
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *orbitHost.OrbitNodeKey)),
http.StatusOK, &resp)
require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
@@ -6314,30 +6312,12 @@ func (s *integrationMDMTestSuite) TestValidManagementUnenrollRequest() {
func (s *integrationMDMTestSuite) TestRunMDMCommands() {
t := s.T()
- ctx := context.Background()
// create a Windows host enrolled in MDM
enrolledWindows := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
- deviceID := "DB257C3A08778F4FB61E2749066C1F27"
- enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
- MDMDeviceID: deviceID,
- MDMHardwareID: uuid.New().String() + uuid.New().String(),
- MDMDeviceState: uuid.New().String(),
- MDMDeviceType: "CIMClient_Windows",
- MDMDeviceName: "DESKTOP-1C3ARC1",
- MDMEnrollType: "ProgrammaticEnrollment",
- MDMEnrollUserID: "",
- MDMEnrollProtoVersion: "5.0",
- MDMEnrollClientVersion: "10.0.19045.2965",
- MDMNotInOOBE: false,
- HostUUID: enrolledWindows.UUID,
- }
- err := s.ds.SetOrUpdateMDMData(ctx, enrolledWindows.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
- require.NoError(t, err)
-
- err = s.ds.MDMWindowsInsertEnrolledDevice(context.Background(), enrolledDevice)
- require.NoError(t, err)
- err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(context.Background(), enrolledDevice.HostUUID, enrolledDevice.MDMDeviceID)
+ //deviceID := "DB257C3A08778F4FB61E2749066C1F27"
+ mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *enrolledWindows.OrbitNodeKey)
+ err := mdmDevice.Enroll()
require.NoError(t, err)
// create an unenrolled Windows host
@@ -6624,8 +6604,9 @@ func (s *integrationMDMTestSuite) TestHostDiskEncryptionKey() {
msg := extractServerErrorText(res.Body)
require.Contains(t, msg, "host is not enrolled with fleet")
- // mark it as enrolled in Fleet
- err := s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
+ // enroll it in fleet
+ mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *host.OrbitNodeKey)
+ err := mdmDevice.Enroll()
require.NoError(t, err)
// set its encryption key
@@ -6639,6 +6620,10 @@ func (s *integrationMDMTestSuite) TestHostDiskEncryptionKey() {
require.NotNil(t, hdek.Decryptable)
require.True(t, *hdek.Decryptable)
+ // mark it as non-server
+ err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "")
+ require.NoError(t, err)
+
var hostResp getHostResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
require.Nil(t, hostResp.Host.DiskEncryptionEnabled) // the disk encryption status of the host is not set by the orbit request
@@ -8038,7 +8023,9 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusConflict, &unlockResp)
// lock the host
- s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusNoContent)
+ var lockResp lockHostResponse
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusOK, &lockResp)
+ assert.Len(t, lockResp.UnlockPIN, 6)
// refresh the host's status, it is now pending lock
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
@@ -8084,12 +8071,12 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
unlockActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeUnlockedHost{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "host_platform": %q}`, host.ID, host.DisplayName(), host.FleetPlatform()), 0)
- // refresh the host's status, it is locked pending unlock
+ // refresh the host's status, it is still locked
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
- require.Equal(t, "unlock", *getHostResp.Host.MDM.PendingAction)
+ assert.Empty(t, *getHostResp.Host.MDM.PendingAction)
// try unlocking the host again simply returns the PIN again
unlockResp = unlockHostResponse{}
@@ -8834,7 +8821,7 @@ func (s *integrationMDMTestSuite) appleCoreCertsSetup() {
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
- CommonName: "FleetDM",
+ CommonName: "Fleet",
ExtraNames: []pkix.AttributeTypeAndValue{
{
Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1},
diff --git a/server/service/integration_sandbox_test.go b/server/service/integration_sandbox_test.go
index 98f4d9c68f..9016da8880 100644
--- a/server/service/integration_sandbox_test.go
+++ b/server/service/integration_sandbox_test.go
@@ -12,7 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/s3"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
diff --git a/server/service/integration_smtp_test.go b/server/service/integration_smtp_test.go
index 92374cdbd8..a35969e22a 100644
--- a/server/service/integration_smtp_test.go
+++ b/server/service/integration_smtp_test.go
@@ -6,7 +6,7 @@ import (
"os"
"testing"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
diff --git a/server/service/integration_sso_test.go b/server/service/integration_sso_test.go
index 817e1afc98..240072c110 100644
--- a/server/service/integration_sso_test.go
+++ b/server/service/integration_sso_test.go
@@ -20,7 +20,7 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/service/mdm.go b/server/service/mdm.go
index 069a5ecf1d..c190788ca8 100644
--- a/server/service/mdm.go
+++ b/server/service/mdm.go
@@ -33,7 +33,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/assets"
nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
"github.com/go-sql-driver/mysql"
)
@@ -478,9 +478,14 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host
return nil, ctxerr.Wrap(ctx, err, "no host received")
}
+ connectedMap, err := svc.ds.AreHostsConnectedToFleetMDM(ctx, hosts)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "checking if hosts are connected to Fleet")
+ }
+
platforms := make(map[string]bool)
for _, h := range hosts {
- if !h.MDMInfo.IsFleetEnrolled() {
+ if !connectedMap[h.UUID] {
err := fleet.NewInvalidArgumentError("host_uuids", "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.").WithStatus(http.StatusPreconditionFailed)
return nil, ctxerr.Wrap(ctx, err, "check host mdm enrollment")
}
@@ -1869,8 +1874,17 @@ func validateProfiles(profiles []fleet.MDMProfileBatchPayload) error {
for _, profile := range profiles {
platform := mdm.GetRawProfilePlatform(profile.Contents)
if platform != "darwin" && platform != "windows" {
- // TODO(roberto): there's ongoing feedback with Marko about improving this message, as it's too windows specific
- return fleet.NewInvalidArgumentError("mdm", "Windows configuration profiles can only have or top level elements.")
+ // We can only display a generic error message here because at this point
+ // we don't know the file extension or whether the profile is intended
+ // for macos_settings or windows_settings. We should expecte never see this
+ // in practice because the client should be validating the profiles
+ // before sending them to the server so the client can surface more helpful
+ // error messages to the user. However, we're validating again here just
+ // in case the client is not working as expected.
+ return fleet.NewInvalidArgumentError("mdm", fmt.Sprintf(
+ "%s is not a valid macOS or Windows configuration profile. ", profile.Name)+
+ "macOS profiles must be valid .mobileconfig or .json files. "+
+ "Windows configuration profiles can only have or top level elements.")
}
}
diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go
index a3ef9ecef8..0d86234981 100644
--- a/server/service/mdm_test.go
+++ b/server/service/mdm_test.go
@@ -373,6 +373,14 @@ func TestRunMDMCommandAuthz(t *testing.T) {
team1And2UnenrolledHosts := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a"}, {ID: 2, TeamID: ptr.Uint(2), UUID: "b"}}
team2And3UnenrolledHosts := []*fleet.Host{{ID: 2, TeamID: ptr.Uint(2), UUID: "b"}, {ID: 3, TeamID: ptr.Uint(3), UUID: "c"}}
+ 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] = true
+ }
+ return res, nil
+ }
+
userTeamMaintainerTeam1And2 := &fleet.User{
ID: 100,
Teams: []fleet.UserTeam{
@@ -490,6 +498,14 @@ func TestRunMDMCommandValidations(t *testing.T) {
windowsSingleHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", MDMInfo: enrolledMDMInfo, Platform: "windows"}}
macosSingleHost := []*fleet.Host{{ID: 1, TeamID: ptr.Uint(1), UUID: "a", MDMInfo: enrolledMDMInfo, Platform: "darwin"}}
+ 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.MDMInfo != nil && h.MDMInfo.Enrolled && h.MDMInfo.Name == fleet.WellKnownMDMFleet
+ }
+ return res, nil
+ }
+
cases := []struct {
desc string
hosts []*fleet.Host
@@ -544,6 +560,13 @@ func TestMDMCommonAuthorization(t *testing.T) {
ds.GetMDMWindowsProfilesSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
return &fleet.MDMProfilesSummary{}, 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] = true
+ }
+ return res, nil
+ }
mockTeamFuncWithUser := func(u *fleet.User) mock.TeamFunc {
return func(ctx context.Context, teamID uint) (*fleet.Team, error) {
@@ -661,6 +684,13 @@ func TestEnqueueWindowsMDMCommand(t *testing.T) {
ds.MDMWindowsInsertCommandForHostsFunc = func(ctx context.Context, deviceIDs []string, cmd *fleet.MDMWindowsCommand) error {
return 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] = true
+ }
+ return res, nil
+ }
cases := []struct {
desc string
@@ -790,6 +820,13 @@ func TestGetMDMDiskEncryptionSummary(t *testing.T) {
// Use default zeros verifying, action_required, or removing_enforcement
return &fleet.MDMWindowsBitLockerSummary{Verified: 7, Failed: 8, Enforcing: 9}, 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] = true
+ }
+ return res, nil
+ }
// Test that the summary properly combines the results of the two methods
des, err := svc.GetMDMDiskEncryptionSummary(ctx, nil)
diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go
index 5b8ebe67fd..c87806c798 100644
--- a/server/service/microsoft_mdm.go
+++ b/server/service/microsoft_mdm.go
@@ -26,7 +26,7 @@ import (
mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
diff --git a/server/service/orbit.go b/server/service/orbit.go
index e5b4c4f474..50bf1d8d28 100644
--- a/server/service/orbit.go
+++ b/server/service/orbit.go
@@ -16,7 +16,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
)
type setOrbitNodeKeyer interface {
@@ -182,19 +182,20 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
return fleet.OrbitConfig{}, err
}
+ isConnectedToFleetMDM, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
+ if err != nil {
+ return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
+ }
+
// set the host's orbit notifications for macOS MDM
var notifs fleet.OrbitConfigNotifications
- if appConfig.MDM.EnabledAndConfigured && host.IsOsqueryEnrolled() {
- // TODO(mna): all those notifications implied a macos hosts, but none of
- // the checks enforce that (only indirectly in some cases, like
- // IsDEPAssignedToFleet), should we add such a platform check?
-
- if host.NeedsDEPEnrollment() {
+ if appConfig.MDM.EnabledAndConfigured && host.IsOsqueryEnrolled() && host.Platform == "darwin" {
+ if host.NeedsDEPEnrollment(isConnectedToFleetMDM) {
notifs.RenewEnrollmentProfile = true
}
if appConfig.MDM.MacOSMigration.Enable &&
- host.IsEligibleForDEPMigration() {
+ host.IsEligibleForDEPMigration(isConnectedToFleetMDM) {
notifs.NeedsMDMMigration = true
}
@@ -221,7 +222,7 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}
}
if !appConfig.MDM.WindowsEnabledAndConfigured {
- if host.IsEligibleForWindowsMDMUnenrollment() {
+ if host.IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM) {
notifs.NeedsProgrammaticWindowsMDMUnenrollment = true
}
}
@@ -276,7 +277,10 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
var nudgeConfig *fleet.NudgeConfig
if appConfig.MDM.EnabledAndConfigured &&
mdmConfig != nil &&
- mdmConfig.MacOSUpdates.EnabledForHost(host) {
+ host.IsOsqueryEnrolled() &&
+ isConnectedToFleetMDM &&
+ mdmConfig.MacOSUpdates.Configured() {
+
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
if errors.Is(err, sql.ErrNoRows) {
// host os has not been collected yet (no details query)
@@ -298,7 +302,7 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}
if mdmConfig.EnableDiskEncryption &&
- host.IsEligibleForBitLockerEncryption() {
+ host.IsEligibleForBitLockerEncryption(isConnectedToFleetMDM) {
notifs.EnforceBitLockerEncryption = true
}
@@ -335,7 +339,9 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
var nudgeConfig *fleet.NudgeConfig
if appConfig.MDM.EnabledAndConfigured &&
- appConfig.MDM.MacOSUpdates.EnabledForHost(host) {
+ isConnectedToFleetMDM &&
+ host.IsOsqueryEnrolled() &&
+ appConfig.MDM.MacOSUpdates.Configured() {
hostOS, err := svc.ds.GetHostOperatingSystem(ctx, host.ID)
if errors.Is(err, sql.ErrNoRows) {
// host os has not been collected yet (no details query)
@@ -358,7 +364,7 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
if appConfig.MDM.WindowsEnabledAndConfigured &&
appConfig.MDM.EnableDiskEncryption.Value &&
- host.IsEligibleForBitLockerEncryption() {
+ host.IsEligibleForBitLockerEncryption(isConnectedToFleetMDM) {
notifs.EnforceBitLockerEncryption = true
}
@@ -723,7 +729,13 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption
if !ok {
return newOsqueryError("internal error: missing host from request context")
}
- if !host.MDMInfo.IsFleetEnrolled() {
+
+ connected, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "checking if host is connected to Fleet")
+ }
+
+ if !connected {
return badRequest("host is not enrolled with fleet")
}
diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go
index e04ac966b9..dd937c2eee 100644
--- a/server/service/orbit_test.go
+++ b/server/service/orbit_test.go
@@ -35,6 +35,10 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) {
return nil, nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return true, nil
+ }
+
ctx = test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
ID: 1,
@@ -98,6 +102,9 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) {
return nil, nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return true, nil
+ }
ctx = test.HostContext(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test"),
@@ -172,6 +179,10 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) {
return nil, nil
}
+ var isHostConnectedToFleet bool
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, h *fleet.Host) (bool, error) {
+ return isHostConnectedToFleet, nil
+ }
checkEmptyNudgeConfig := func(h *fleet.Host) {
ctx := test.HostContext(ctx, h)
cfg, err := svc.GetOrbitConfig(ctx)
@@ -182,45 +193,28 @@ func TestGetOrbitConfigNudge(t *testing.T) {
}
checkHostVariations := func(h *fleet.Host) {
- // host uses another MDM
- h.MDMInfo.Name = fleet.WellKnownMDMIntune
- checkEmptyNudgeConfig(h)
-
- // host has MDM turned off
- h.MDMInfo.Name = fleet.WellKnownMDMFleet
- h.MDMInfo.Enrolled = false
+ // host is not connected to fleet
+ isHostConnectedToFleet = false
checkEmptyNudgeConfig(h)
// host has MDM turned on but is not enrolled
- h.MDMInfo.Enrolled = true
+ isHostConnectedToFleet = true
h.OsqueryHostID = nil
checkEmptyNudgeConfig(h)
-
- // mdminfo is nil
- h.MDMInfo = nil
- checkEmptyNudgeConfig(h)
}
// global host
checkHostVariations(&fleet.Host{
OsqueryHostID: ptr.String("test"),
- MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: true,
- Name: fleet.WellKnownMDMFleet,
- }})
+ Platform: "darwin",
+ })
// team host
checkHostVariations(&fleet.Host{
OsqueryHostID: ptr.String("test"),
TeamID: ptr.Uint(team.ID),
- MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: true,
- Name: fleet.WellKnownMDMFleet,
- }})
+ Platform: "darwin",
+ })
})
@@ -259,6 +253,9 @@ func TestGetOrbitConfigNudge(t *testing.T) {
ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) {
return nil, nil
}
+ ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
+ return true, nil
+ }
appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}
appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01")
appCfg.MDM.MacOSUpdates.MinimumVersion = optjson.SetString("12.3")
diff --git a/server/service/osquery.go b/server/service/osquery.go
index a806798c03..c4112f7d0d 100644
--- a/server/service/osquery.go
+++ b/server/service/osquery.go
@@ -1799,7 +1799,8 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
unmarshaledResults, queriesDBData := svc.preProcessOsqueryResults(ctx, logs, queryReportsDisabled)
if !queryReportsDisabled {
- svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData)
+ maxQueryReportRows := appConfig.ServerSettings.GetQueryReportCap()
+ svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData, maxQueryReportRows)
}
var filteredLogs []json.RawMessage
@@ -1861,7 +1862,12 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
// Query Reports
////////////////////////////////////////////////////////////////////////////////
-func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshaledResults []*fleet.ScheduledQueryResult, queriesDBData map[string]*fleet.Query) {
+func (svc *Service) saveResultLogsToQueryReports(
+ ctx context.Context,
+ unmarshaledResults []*fleet.ScheduledQueryResult,
+ queriesDBData map[string]*fleet.Query,
+ maxQueryReportRows int,
+) {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
@@ -1903,11 +1909,11 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
level.Error(svc.logger).Log("msg", "get result count for query", "err", err, "query_id", dbQuery.ID)
continue
}
- if count >= fleet.MaxQueryReportRows {
+ if count >= maxQueryReportRows {
continue
}
- if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID); err != nil {
+ if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID, maxQueryReportRows); err != nil {
level.Error(svc.logger).Log("msg", "overwrite results", "err", err, "query_id", dbQuery.ID, "host_id", host.ID)
continue
}
@@ -1919,7 +1925,7 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
// The "snapshot" array in a ScheduledQueryResult can contain multiple rows.
// Each row is saved as a separate ScheduledQueryResultRow, i.e. a result could contain
// many USB Devices or a result could contain all user accounts on a host.
-func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint) error {
+func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint, maxQueryReportRows int) error {
fetchTime := time.Now()
rows := make([]*fleet.ScheduledQueryResultRow, 0, len(result.Snapshot))
@@ -1945,7 +1951,7 @@ func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.Sched
rows = append(rows, row)
}
- if err := svc.ds.OverwriteQueryResultRows(ctx, rows); err != nil {
+ if err := svc.ds.OverwriteQueryResultRows(ctx, rows, maxQueryReportRows); err != nil {
return ctxerr.Wrap(ctx, err, "overwriting query result rows")
}
return nil
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index 9aa9ce246f..f8505b3155 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -35,8 +35,8 @@ import (
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -614,7 +614,7 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
return 0, nil
}
teamQueryResultsStored := false
- ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+ ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
if len(rows) == 0 {
return nil
}
@@ -766,7 +766,7 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
Logging: fleet.LoggingSnapshot,
},
}
- serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse)
+ serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse, fleet.DefaultMaxQueryReportRows)
assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked)
// Happy Path: Results saved
@@ -777,13 +777,13 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
Logging: fleet.LoggingSnapshot,
},
}
- ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+ ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
return nil
}
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return 0, nil
}
- serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue)
+ serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue, fleet.DefaultMaxQueryReportRows)
require.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
}
@@ -825,7 +825,7 @@ func TestSubmitResultLogsToQueryResultsWithEmptySnapShot(t *testing.T) {
return 0, nil
}
- ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+ ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
require.Len(t, rows, 1)
require.Equal(t, uint(999), rows[0].HostID)
require.NotZero(t, rows[0].LastFetched)
@@ -876,7 +876,7 @@ func TestSubmitResultLogsToQueryResultsDoesNotCountNullDataRows(t *testing.T) {
return 0, nil
}
- ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+ ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
require.Len(t, rows, 1)
require.Equal(t, uint(999), rows[0].HostID)
require.NotZero(t, rows[0].LastFetched)
@@ -933,7 +933,7 @@ func TestSubmitResultLogsFail(t *testing.T) {
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return 0, nil
}
- ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
+ ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
return nil
}
@@ -1061,6 +1061,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) {
hostDetailQueryPrefix + "kubequery_info": {},
hostDetailQueryPrefix + "orbit_info": {},
hostDetailQueryPrefix + "software_vscode_extensions": {},
+ hostDetailQueryPrefix + "software_macos_firefox": {},
}
for name := range queries {
require.NotEmpty(t, discovery[name])
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index 6c8d30585c..9e061d9bd7 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -24,8 +24,8 @@ import (
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/spf13/cast"
)
@@ -755,7 +755,7 @@ var mdmQueries = map[string]DetailQuery{
// discoveryTable returns a query to determine whether a table exists or not.
func discoveryTable(tableName string) string {
- return fmt.Sprintf("SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = '%s';", tableName)
+ return fmt.Sprintf("SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = '%s'", tableName)
}
func macOSBundleIDExistsQuery(appName string) string {
@@ -774,7 +774,11 @@ func generateSQLForAllExists(subqueries ...string) string {
// Generate EXISTS clause for each subquery
var conditions []string
for _, query := range subqueries {
- condition := fmt.Sprintf("EXISTS (%s)", query)
+ // Remove trailing semicolons from the query to ensure subqueries
+ // are not terminated early (Issue #19401)
+ sanitized := strings.TrimRight(strings.TrimSpace(query), ";")
+
+ condition := fmt.Sprintf("EXISTS (%s)", sanitized)
conditions = append(conditions, condition)
}
@@ -1121,14 +1125,14 @@ var SoftwareOverrideQueries = map[string]DetailQuery{
Description: "A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`",
Query: `
WITH app_paths AS (
- SELECT path
- FROM apps
+ SELECT path
+ FROM apps
WHERE bundle_identifier = 'org.mozilla.firefox'
- ),
+ ),
remoting_name AS (
- SELECT value, path
- FROM parse_ini
- WHERE key = 'RemotingName'
+ SELECT value, path
+ FROM parse_ini
+ WHERE key = 'RemotingName'
AND path IN (SELECT CONCAT(path, '/Contents/Resources/application.ini') FROM app_paths)
)
SELECT
@@ -1707,7 +1711,15 @@ func directIngestMDMMac(ctx context.Context, logger log.Logger, host *fleet.Host
}
if fleetEnrollRef != "" {
if err := ds.SetOrUpdateHostEmailsFromMdmIdpAccounts(ctx, host.ID, fleetEnrollRef); err != nil {
- return ctxerr.Wrap(ctx, err, "updating host emails from mdm idp accounts")
+ if !fleet.IsNotFound(err) {
+ return ctxerr.Wrap(ctx, err, "updating host emails from mdm idp accounts")
+ }
+
+ level.Warn(logger).Log(
+ "component", "service",
+ "method", "directIngestMDMMac",
+ "msg", err.Error(),
+ )
}
}
}
@@ -2015,13 +2027,9 @@ func GetDetailQueries(
generatedMap["software_chrome"] = softwareChrome
generatedMap["software_vscode_extensions"] = softwareVSCodeExtensions
- //
- // Commenting out until we find root cause of discovery query hanging osquery:
- // https://github.com/fleetdm/fleet/issues/19401
- //
- // for key, query := range SoftwareOverrideQueries {
- // generatedMap["software_"+key] = query
- // }
+ for key, query := range SoftwareOverrideQueries {
+ generatedMap["software_"+key] = query
+ }
}
if features != nil && features.EnableHostUsers {
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 14639a33a8..8fb86bcc1f 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -304,7 +304,7 @@ func TestGetDetailQueries(t *testing.T) {
sortedKeysCompare(t, queriesWithUsers, qs)
queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true})
- qs = append(baseQueries, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", "software_chrome", "scheduled_query_stats")
+ qs = append(baseQueries, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", "software_chrome", "scheduled_query_stats", "software_macos_firefox")
require.Len(t, queriesWithUsersAndSoftware, len(qs))
sortedKeysCompare(t, queriesWithUsersAndSoftware, qs)
@@ -502,6 +502,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
got map[string]string
wantParams []any
wantErr string
+ enrollRef string
}{
{
"empty server URL",
@@ -512,6 +513,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{false, false, "", false, fleet.UnknownMDMName},
"",
+ "",
},
{
"with Fleet payload identifier",
@@ -523,6 +525,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{false, true, "https://test.example.com", true, fleet.WellKnownMDMFleet},
"",
+ "",
},
{
"with a query string on the server URL",
@@ -533,6 +536,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{false, true, "https://jamf.com/1/some/path", true, fleet.WellKnownMDMJamf},
"",
+ "",
},
{
"with invalid installed_from_dep",
@@ -543,6 +547,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{},
"parsing installed_from_dep",
+ "",
},
{
"with invalid enrolled",
@@ -553,6 +558,7 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{},
"parsing enrolled",
+ "",
},
{
"with invalid server_url",
@@ -563,6 +569,19 @@ func TestDirectIngestMDMMac(t *testing.T) {
},
[]any{},
"parsing server_url",
+ "",
+ },
+ {
+ "with invalid enrollment reference",
+ map[string]string{
+ "enrolled": "true",
+ "installed_from_dep": "true",
+ "server_url": "https://test.example.com?enroll_reference=foobar",
+ "payload_identifier": apple_mdm.FleetPayloadIdentifier,
+ },
+ []any{false, true, "https://test.example.com", true, fleet.WellKnownMDMFleet},
+ "",
+ "foobar",
},
}
@@ -574,13 +593,19 @@ func TestDirectIngestMDMMac(t *testing.T) {
require.Equal(t, serverURL, c.wantParams[2])
require.Equal(t, installedFromDep, c.wantParams[3])
require.Equal(t, name, c.wantParams[4])
- require.Empty(t, fleetEnrollmentRef)
+ require.Equal(t, fleetEnrollmentRef, c.enrollRef)
return nil
}
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
return nil
}
+ if c.name == "with invalid enrollment reference" {
+ ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
+ return &nfe{}
+ }
+ }
+
err := directIngestMDMMac(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{c.got})
if c.wantErr != "" {
require.ErrorContains(t, err, c.wantErr)
@@ -590,7 +615,9 @@ func TestDirectIngestMDMMac(t *testing.T) {
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
require.NoError(t, err)
ds.SetOrUpdateMDMDataFuncInvoked = false
- require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
+ if c.name != "with invalid enrollment reference" {
+ require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
+ }
}
})
}
@@ -1935,12 +1962,33 @@ func TestIngestNetworkInterface(t *testing.T) {
}
func TestGenerateSQLForAllExists(t *testing.T) {
+ // Combine two queries
query1 := "SELECT 1 WHERE foo = bar"
query2 := "SELECT 1 WHERE baz = qux"
-
sql := generateSQLForAllExists(query1, query2)
assert.Equal(t, "SELECT 1 WHERE EXISTS (SELECT 1 WHERE foo = bar) AND EXISTS (SELECT 1 WHERE baz = qux)", sql)
+ // Default
sql = generateSQLForAllExists()
require.Equal(t, "SELECT 0 LIMIT 0", sql)
+
+ // sanitize semicolons from subqueries
+ query1 = "SELECT 1 WHERE foo = bar;"
+ query2 = "SELECT 1 WHERE baz = qux;"
+ sql = generateSQLForAllExists(query1, query2)
+ assert.Equal(t, "SELECT 1 WHERE EXISTS (SELECT 1 WHERE foo = bar) AND EXISTS (SELECT 1 WHERE baz = qux)", sql)
+
+ // sanitize only trailing semicolons
+ query1 = "SELECT 1 WHERE foo = 'ba;r';"
+ query2 = "SELECT 1 WHERE baz = 'qu;x';;; "
+ sql = generateSQLForAllExists(query1, query2)
+ assert.Equal(t, "SELECT 1 WHERE EXISTS (SELECT 1 WHERE foo = 'ba;r') AND EXISTS (SELECT 1 WHERE baz = 'qu;x')", sql)
}
+
+type nfe struct{}
+
+func (e nfe) Error() string {
+ return "foobar"
+}
+
+func (e nfe) IsNotFound() bool { return true }
diff --git a/server/service/queries.go b/server/service/queries.go
index dcfe1a455e..07b9c43a69 100644
--- a/server/service/queries.go
+++ b/server/service/queries.go
@@ -121,16 +121,17 @@ type getQueryReportRequest struct {
}
type getQueryReportResponse struct {
- QueryID uint `json:"query_id"`
- Results []fleet.HostQueryResultRow `json:"results"`
- Err error `json:"error,omitempty"`
+ QueryID uint `json:"query_id"`
+ Results []fleet.HostQueryResultRow `json:"results"`
+ ReportClipped bool `json:"report_clipped"`
+ Err error `json:"error,omitempty"`
}
func (r getQueryReportResponse) error() error { return r.Err }
func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getQueryReportRequest)
- queryReportResults, err := svc.GetQueryReportResults(ctx, req.ID)
+ queryReportResults, reportClipped, err := svc.GetQueryReportResults(ctx, req.ID)
if err != nil {
return listQueriesResponse{Err: err}, nil
}
@@ -140,44 +141,53 @@ func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.
results = queryReportResults
}
return getQueryReportResponse{
- QueryID: req.ID,
- Results: results,
+ QueryID: req.ID,
+ Results: results,
+ ReportClipped: reportClipped,
}, nil
}
-func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, error) {
+func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, bool, error) {
// Load query first to get its teamID.
query, err := svc.ds.Query(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
- return nil, ctxerr.Wrap(ctx, err, "get query from datastore")
+ return nil, false, ctxerr.Wrap(ctx, err, "get query from datastore")
}
if err := svc.authz.Authorize(ctx, query, fleet.ActionRead); err != nil {
- return nil, err
+ return nil, false, err
}
if query.DiscardData {
- return nil, nil
+ return nil, false, nil
}
vc, ok := viewer.FromContext(ctx)
if !ok {
- return nil, fleet.ErrNoContext
+ return nil, false, fleet.ErrNoContext
}
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
queryReportResultRows, err := svc.ds.QueryResultRows(ctx, id, filter)
if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "get query report results")
+ return nil, false, ctxerr.Wrap(ctx, err, "get query report results")
}
queryReportResults, err := fleet.MapQueryReportResultsToRows(queryReportResultRows)
if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "map db rows to results")
+ return nil, false, ctxerr.Wrap(ctx, err, "map db rows to results")
}
- return queryReportResults, nil
+ appConfig, err := svc.ds.AppConfig(ctx)
+ if err != nil {
+ return nil, false, ctxerr.Wrap(ctx, err, "get app config")
+ }
+ reportClipped, err := svc.QueryReportIsClipped(ctx, id, appConfig.ServerSettings.GetQueryReportCap())
+ if err != nil {
+ return nil, false, ctxerr.Wrap(ctx, err, "check query report is clipped")
+ }
+ return queryReportResults, reportClipped, nil
}
-func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error) {
+func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error) {
query, err := svc.ds.Query(ctx, queryID)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
@@ -191,7 +201,7 @@ func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (boo
if err != nil {
return false, err
}
- return count >= fleet.MaxQueryReportRows, nil
+ return count >= maxQueryReportRows, nil
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/queries_test.go b/server/service/queries_test.go
index fc8631264b..95ae244d7a 100644
--- a/server/service/queries_test.go
+++ b/server/service/queries_test.go
@@ -644,7 +644,7 @@ func TestQueryAuth(t *testing.T) {
_, err = svc.GetQuery(ctx, tt.qid)
checkAuthErr(t, tt.shouldFailRead, err)
- _, err = svc.QueryReportIsClipped(ctx, tt.qid)
+ _, err = svc.QueryReportIsClipped(ctx, tt.qid, fleet.DefaultMaxQueryReportRows)
checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false)
@@ -688,15 +688,15 @@ func TestQueryReportIsClipped(t *testing.T) {
return 0, nil
}
- isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1)
+ isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
require.False(t, isClipped)
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
- return fleet.MaxQueryReportRows, nil
+ return fleet.DefaultMaxQueryReportRows, nil
}
- isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1)
+ isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
require.True(t, isClipped)
}
@@ -725,9 +725,10 @@ func TestQueryReportReturnsNilIfDiscardDataIsTrue(t *testing.T) {
}, nil
}
- results, err := svc.GetQueryReportResults(viewerCtx, 1)
+ results, reportClipped, err := svc.GetQueryReportResults(viewerCtx, 1)
require.NoError(t, err)
require.Nil(t, results)
+ require.False(t, reportClipped)
}
func TestComparePlatforms(t *testing.T) {
diff --git a/server/service/schedule/schedule.go b/server/service/schedule/schedule.go
index 2d0bacb645..8965416a64 100644
--- a/server/service/schedule/schedule.go
+++ b/server/service/schedule/schedule.go
@@ -12,8 +12,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// ReloadInterval reloads and returns a new interval.
diff --git a/server/service/scripts.go b/server/service/scripts.go
index bde4991867..b9d8fec23a 100644
--- a/server/service/scripts.go
+++ b/server/service/scripts.go
@@ -400,23 +400,30 @@ func (svc *Service) GetScriptResult(ctx context.Context, execID string) (*fleet.
return nil, ctxerr.Wrap(ctx, err, "get script result")
}
- host, err := svc.ds.HostLite(ctx, scriptResult.HostID)
- if err != nil {
- // if error is because the host does not exist, check first if the user
- // had access to run a script (to prevent leaking valid host ids).
- if fleet.IsNotFound(err) {
- if err := svc.authz.Authorize(ctx, &fleet.HostScriptResult{}, fleet.ActionRead); err != nil {
- return nil, err
+ if scriptResult.HostDeletedAt == nil {
+ // host is not deleted, get it and authorize for the host's team
+ host, err := svc.ds.HostLite(ctx, scriptResult.HostID)
+ if err != nil {
+ // if error is because the host does not exist, check first if the user
+ // had access to run a script (to prevent leaking valid host ids).
+ if fleet.IsNotFound(err) {
+ if err := svc.authz.Authorize(ctx, &fleet.HostScriptResult{}, fleet.ActionRead); err != nil {
+ return nil, err
+ }
}
+ svc.authz.SkipAuthorization(ctx)
+ return nil, ctxerr.Wrap(ctx, err, "get host lite")
+ }
+ if err := svc.authz.Authorize(ctx, &fleet.HostScriptResult{TeamID: host.TeamID}, fleet.ActionRead); err != nil {
+ return nil, err
+ }
+ scriptResult.Hostname = host.DisplayName()
+ } else {
+ // host was deleted, authorize for no-team as a fallback
+ if err := svc.authz.Authorize(ctx, &fleet.HostScriptResult{}, fleet.ActionRead); err != nil {
+ return nil, err
}
- svc.authz.SkipAuthorization(ctx)
- return nil, ctxerr.Wrap(ctx, err, "get host lite")
}
- if err := svc.authz.Authorize(ctx, &fleet.HostScriptResult{TeamID: host.TeamID}, fleet.ActionRead); err != nil {
- return nil, err
- }
-
- scriptResult.Hostname = host.DisplayName()
return scriptResult, nil
}
@@ -908,26 +915,37 @@ type lockHostRequest struct {
}
type lockHostResponse struct {
- Err error `json:"error,omitempty"`
+ Err error `json:"error,omitempty"`
+ UnlockPIN string `json:"unlock_pin,omitempty"`
+ StatusCode int `json:"-"`
}
-func (r lockHostResponse) Status() int { return http.StatusNoContent }
+func (r lockHostResponse) Status() int {
+ if r.StatusCode != 0 {
+ return r.StatusCode
+ }
+ return http.StatusNoContent
+}
func (r lockHostResponse) error() error { return r.Err }
func lockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*lockHostRequest)
- if err := svc.LockHost(ctx, req.HostID); err != nil {
+ unlockPIN, err := svc.LockHost(ctx, req.HostID)
+ if err != nil {
return lockHostResponse{Err: err}, nil
}
+ if unlockPIN != "" {
+ return lockHostResponse{UnlockPIN: unlockPIN, StatusCode: http.StatusOK}, nil
+ }
return lockHostResponse{}, nil
}
-func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
+func (svc *Service) LockHost(ctx context.Context, hostID uint) (string, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
- return fleet.ErrMissingLicense
+ return "", fleet.ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/service.go b/server/service/service.go
index 72a097c112..f1293ca64d 100644
--- a/server/service/service.go
+++ b/server/service/service.go
@@ -20,7 +20,7 @@ import (
nanomdm_storage "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage"
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/fleetdm/fleet/v4/server/sso"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
)
var _ fleet.Service = (*Service)(nil)
diff --git a/server/service/service_campaign_test.go b/server/service/service_campaign_test.go
index 25e2ae1896..9eb5a23816 100644
--- a/server/service/service_campaign_test.go
+++ b/server/service/service_campaign_test.go
@@ -21,7 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
ws "github.com/fleetdm/fleet/v4/server/websocket"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/service/service_campaigns.go b/server/service/service_campaigns.go
index c353cfed3b..5b2ec0e40f 100644
--- a/server/service/service_campaigns.go
+++ b/server/service/service_campaigns.go
@@ -13,8 +13,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/websocket"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/igm/sockjs-go/v3/sockjs"
)
diff --git a/server/service/sessions.go b/server/service/sessions.go
index a1f2fe36ab..1b61861a43 100644
--- a/server/service/sessions.go
+++ b/server/service/sessions.go
@@ -18,7 +18,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/sso"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
)
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/software_installers.go b/server/service/software_installers.go
index f03f05d0b7..4b71f1ca09 100644
--- a/server/service/software_installers.go
+++ b/server/service/software_installers.go
@@ -2,9 +2,11 @@ package service
import (
"context"
+ "errors"
"fmt"
"io"
"mime/multipart"
+ "net"
"net/http"
"strconv"
@@ -29,14 +31,32 @@ type uploadSoftwareInstallerResponse struct {
Err error `json:"error,omitempty"`
}
+// MaxSoftwareInstallerSize is the maximum size allowed for software
+// installers. This is enforced by the endpoint that uploads installers.
+const MaxSoftwareInstallerSize = 500 * units.MiB
+
// TODO: We parse the whole body before running svc.authz.Authorize.
// An authenticated but unauthorized user could abuse this.
func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
decoded := uploadSoftwareInstallerRequest{}
err := r.ParseMultipartForm(512 * units.MiB)
if err != nil {
+ var mbe *http.MaxBytesError
+ if errors.As(err, &mbe) {
+ return nil, &fleet.BadRequestError{
+ Message: "The maximum file size is 500 MB.",
+ InternalErr: err,
+ }
+ }
+ var nerr net.Error
+ if errors.As(err, &nerr) && nerr.Timeout() {
+ return nil, fleet.NewUserMessageError(
+ ctxerr.New(ctx, "Couldn't upload. Please ensure your internet connection speed is sufficient and stable."),
+ http.StatusRequestTimeout,
+ )
+ }
return nil, &fleet.BadRequestError{
- Message: "failed to parse multipart form",
+ Message: "failed to parse multipart form: " + err.Error(),
InternalErr: err,
}
}
@@ -49,9 +69,9 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http
}
decoded.File = r.MultipartForm.File["software"][0]
-
- if decoded.File.Size > 500*units.MiB {
- // TODO: Should we try to assess the size earlier in the request processing (before parsing the form)?
+ if decoded.File.Size > MaxSoftwareInstallerSize {
+ // Should never happen here since the request's body is limited to the
+ // maximum size.
return nil, &fleet.BadRequestError{
Message: "The maximum file size is 500 MB.",
}
diff --git a/server/service/teams_test.go b/server/service/teams_test.go
index c145c34714..9011e8bce7 100644
--- a/server/service/teams_test.go
+++ b/server/service/teams_test.go
@@ -431,7 +431,7 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
return false, nil
}
_, err := svc.ApplyTeamSpecs(
- ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}},
+ ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}},
fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}},
)
assert.ErrorContains(t, err, "is already being used")
@@ -441,14 +441,14 @@ func TestApplyTeamSpecEnrollSecretForNewTeams(t *testing.T) {
return true, nil
}
_, err = svc.ApplyTeamSpecs(
- ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}},
+ ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}},
fleet.ApplyTeamSpecOptions{ApplySpecOptions: fleet.ApplySpecOptions{DryRun: true}},
)
assert.NoError(t, err)
assert.False(t, ds.NewTeamFuncInvoked)
_, err = svc.ApplyTeamSpecs(
- ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: []fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{},
+ ctx, []*fleet.TeamSpec{{Name: "Foo", Secrets: &[]fleet.EnrollSecret{enrollSecret}}}, fleet.ApplyTeamSpecOptions{},
)
require.NoError(t, err)
require.True(t, ds.TeamByNameFuncInvoked)
diff --git a/server/service/testing_client.go b/server/service/testing_client.go
index 028a452d91..b5232c2b1c 100644
--- a/server/service/testing_client.go
+++ b/server/service/testing_client.go
@@ -25,7 +25,7 @@ import (
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/ghodss/yaml"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go
index fd08a9e232..73c0d495c0 100644
--- a/server/service/testing_utils.go
+++ b/server/service/testing_utils.go
@@ -36,7 +36,7 @@ import (
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/server/service/transport.go b/server/service/transport.go
index fadc537764..b32c939579 100644
--- a/server/service/transport.go
+++ b/server/service/transport.go
@@ -420,6 +420,11 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error)
)
}
+ connectedToFleet := r.URL.Query().Has("connected_to_fleet")
+ if connectedToFleet {
+ hopt.ConnectedToFleetFilter = ptr.Bool(true)
+ }
+
macOSSettingsStatus := r.URL.Query().Get("macos_settings")
switch fleet.OSSettingsStatus(macOSSettingsStatus) {
case fleet.OSSettingsFailed, fleet.OSSettingsPending, fleet.OSSettingsVerifying, fleet.OSSettingsVerified:
diff --git a/server/service/users.go b/server/service/users.go
index 795fbb02d6..2845243860 100644
--- a/server/service/users.go
+++ b/server/service/users.go
@@ -10,7 +10,7 @@ import (
"net/http"
"time"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log/level"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/authz"
@@ -34,38 +34,43 @@ type createUserRequest struct {
type createUserResponse struct {
User *fleet.User `json:"user,omitempty"`
- Err error `json:"error,omitempty"`
+ // Token is only returned when creating API-only (non-SSO) users.
+ Token *string `json:"token,omitempty"`
+ Err error `json:"error,omitempty"`
}
func (r createUserResponse) error() error { return r.Err }
func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createUserRequest)
- user, err := svc.CreateUser(ctx, req.UserPayload)
+ user, sessionKey, err := svc.CreateUser(ctx, req.UserPayload)
if err != nil {
return createUserResponse{Err: err}, nil
}
- return createUserResponse{User: user}, nil
+ return createUserResponse{
+ User: user,
+ Token: sessionKey,
+ }, nil
}
-func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
+func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, *string, error) {
var teams []fleet.UserTeam
if p.Teams != nil {
teams = *p.Teams
}
if err := svc.authz.Authorize(ctx, &fleet.User{Teams: teams}, fleet.ActionWrite); err != nil {
- return nil, err
+ return nil, nil, err
}
if err := p.VerifyAdminCreate(); err != nil {
- return nil, ctxerr.Wrap(ctx, err, "verify user payload")
+ return nil, nil, ctxerr.Wrap(ctx, err, "verify user payload")
}
if teams != nil {
// Validate that the teams exist
teamsSummary, err := svc.ds.TeamsSummary(ctx)
if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
+ return nil, nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
}
teamIDs := map[uint]struct{}{}
for _, team := range teamsSummary {
@@ -74,7 +79,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
for _, userTeam := range teams {
_, ok := teamIDs[userTeam.Team.ID]
if !ok {
- return nil, ctxerr.Wrap(
+ return nil, nil, ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("teams.id", fmt.Sprintf("team with id %d does not exist", userTeam.Team.ID)),
)
}
@@ -82,7 +87,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
}
if invite, err := svc.ds.InviteByEmail(ctx, *p.Email); err == nil && invite != nil {
- return nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
+ return nil, nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
}
if p.AdminForcedPasswordReset == nil {
@@ -90,7 +95,28 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
p.AdminForcedPasswordReset = ptr.Bool(true)
}
- return svc.NewUser(ctx, p)
+ user, err := svc.NewUser(ctx, p)
+ if err != nil {
+ return nil, nil, ctxerr.Wrap(ctx, err, "create user")
+ }
+
+ // The sessionKey is returned for API-only non-SSO users only.
+ var sessionKey *string
+ if user.APIOnly && !user.SSOEnabled {
+ if p.Password == nil {
+ // Should not happen but let's log just in case.
+ level.Error(svc.logger).Log("err", err, "msg", "password not set during admin user creation")
+ } else {
+ // Create a session for the API-only user by logging in.
+ _, session, err := svc.Login(ctx, user.Email, *p.Password)
+ if err != nil {
+ return nil, nil, ctxerr.Wrap(ctx, err, "create session for api-only user")
+ }
+ sessionKey = &session.Key
+ }
+ }
+
+ return user, sessionKey, nil
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/users_test.go b/server/service/users_test.go
index d47e1bacd5..2f935d1c83 100644
--- a/server/service/users_test.go
+++ b/server/service/users_test.go
@@ -367,7 +367,7 @@ func TestUserAuth(t *testing.T) {
}
teams := []fleet.UserTeam{{Team: fleet.Team{ID: teamID}, Role: fleet.RoleMaintainer}}
- _, err = svc.CreateUser(ctx, fleet.UserPayload{
+ _, _, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String(test.GoodPassword),
@@ -375,7 +375,7 @@ func TestUserAuth(t *testing.T) {
})
checkAuthErr(t, tt.shouldFailTeamWrite, err)
- _, err = svc.CreateUser(ctx, fleet.UserPayload{
+ _, _, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String(test.GoodPassword),
@@ -641,6 +641,7 @@ func TestUsersWithDS(t *testing.T) {
{"CreateUserForcePasswdReset", testUsersCreateUserForcePasswdReset},
{"ChangePassword", testUsersChangePassword},
{"RequirePasswordReset", testUsersRequirePasswordReset},
+ {"UsersCreateUserWithAPIOnly", testUsersCreateUserWithAPIOnly},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -668,13 +669,14 @@ func testUsersCreateUserForcePasswdReset(t *testing.T, ds *mysql.Datastore) {
// As the admin, create a new user.
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
- user, err := svc.CreateUser(ctx, fleet.UserPayload{
+ user, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Observer"),
Email: ptr.String("some-observer@email.com"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleObserver),
})
require.NoError(t, err)
+ require.Nil(t, sessionKey) // only set when creating API-only users
user, err = ds.UserByID(context.Background(), user.ID)
require.NoError(t, err)
@@ -1319,3 +1321,50 @@ func TestTeamAdminAddRoleOtherTeam(t *testing.T) {
require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
require.False(t, ds.SaveUserFuncInvoked)
}
+
+func testUsersCreateUserWithAPIOnly(t *testing.T, ds *mysql.Datastore) {
+ svc, ctx := newTestService(t, ds, nil, nil)
+
+ host, err := ds.NewHost(ctx, &fleet.Host{
+ UUID: "uuid-42",
+ OsqueryHostID: ptr.String("osquery_host_id-42"),
+ })
+ require.NoError(t, err)
+
+ // Create admin user.
+ admin := &fleet.User{
+ Name: "Fleet Admin",
+ Email: "admin@foo.com",
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ }
+ err = admin.SetPassword(test.GoodPassword, 10, 10)
+ require.NoError(t, err)
+ admin, err = ds.NewUser(ctx, admin)
+ require.NoError(t, err)
+
+ // As the admin, create a new API-only user.
+ ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
+ apiOnlyUser, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
+ Name: ptr.String("Some Observer"),
+ Email: ptr.String("some-observer@email.com"),
+ Password: ptr.String(test.GoodPassword),
+ GlobalRole: ptr.String(fleet.RoleObserver),
+ APIOnly: ptr.Bool(true),
+ })
+ require.NoError(t, err)
+ require.NotNil(t, sessionKey)
+ require.NotEmpty(t, *sessionKey)
+
+ sessions, err := svc.GetInfoAboutSessionsForUser(ctx, apiOnlyUser.ID)
+ require.NoError(t, err)
+ require.Len(t, sessions, 1)
+ session := sessions[0]
+ require.Equal(t, *sessionKey, session.Key)
+
+ refreshCtx(t, ctx, apiOnlyUser, ds, session)
+
+ hosts, err := svc.ListHosts(ctx, fleet.HostListOptions{})
+ require.NoError(t, err)
+ require.Len(t, hosts, 1)
+ require.Equal(t, host.ID, hosts[0].ID)
+}
diff --git a/server/vulnerabilities/nvd/cpe.go b/server/vulnerabilities/nvd/cpe.go
index 6ffe51b7dd..6375bfaa21 100644
--- a/server/vulnerabilities/nvd/cpe.go
+++ b/server/vulnerabilities/nvd/cpe.go
@@ -19,9 +19,9 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
- 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/google/go-github/v37/github"
"github.com/jmoiron/sqlx"
)
diff --git a/server/vulnerabilities/nvd/cpe_matching_rule.go b/server/vulnerabilities/nvd/cpe_matching_rule.go
index 32b331d4f3..9bd8a9b751 100644
--- a/server/vulnerabilities/nvd/cpe_matching_rule.go
+++ b/server/vulnerabilities/nvd/cpe_matching_rule.go
@@ -42,6 +42,7 @@ type CPEMatchingRule struct {
// IgnoreAll will cause all CPEs to not match hence ignoring a CVE.
IgnoreAll bool
// IgnoreIf is a function that can determine if a CPE matching rule should be ignored or not.
+ // If IgnoreIf is set, CPESpecs will not be evaluated.
IgnoreIf func(cpeMeta *wfn.Attributes) bool
}
@@ -55,8 +56,11 @@ func (rule CPEMatchingRule) CPEMatches(cpeMeta *wfn.Attributes) bool {
return false
}
- if rule.IgnoreIf != nil && rule.IgnoreIf(cpeMeta) {
- return false
+ if rule.IgnoreIf != nil {
+ if rule.IgnoreIf(cpeMeta) {
+ return false
+ }
+ return true
}
ver, err := semver.NewVersion(wfn.StripSlashes(cpeMeta.Version))
diff --git a/server/vulnerabilities/nvd/cpe_matching_rules.go b/server/vulnerabilities/nvd/cpe_matching_rules.go
index f745f97ec0..01fa401bb2 100644
--- a/server/vulnerabilities/nvd/cpe_matching_rules.go
+++ b/server/vulnerabilities/nvd/cpe_matching_rules.go
@@ -197,6 +197,18 @@ func GetKnownNVDBugRules() (CPEMatchingRules, error) {
return cpeMeta.TargetSW == "visual_studio_code"
},
},
+ // Issue #18733 incorrect CPEs that should be matching
+ // visual studio code extensions
+ CPEMatchingRule{
+ CVEs: map[string]struct{}{
+ "CVE-2021-28967": {},
+ "CVE-2020-1192": {},
+ "CVE-2020-1171": {},
+ },
+ IgnoreIf: func(cpeMeta *wfn.Attributes) bool {
+ return cpeMeta.Product == "visual_studio_code" && cpeMeta.TargetSW == wfn.Any
+ },
+ },
}
for i, rule := range rules {
diff --git a/server/vulnerabilities/nvd/cpe_test.go b/server/vulnerabilities/nvd/cpe_test.go
index 929005f13e..d6e47dee63 100644
--- a/server/vulnerabilities/nvd/cpe_test.go
+++ b/server/vulnerabilities/nvd/cpe_test.go
@@ -15,8 +15,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cpedict"
- kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/server/vulnerabilities/nvd/cve_test.go b/server/vulnerabilities/nvd/cve_test.go
index 4b7f095277..05cdca110d 100644
--- a/server/vulnerabilities/nvd/cve_test.go
+++ b/server/vulnerabilities/nvd/cve_test.go
@@ -307,6 +307,12 @@ func TestTranslateCPEToCVE(t *testing.T) {
},
continuesToUpdate: false,
},
+ "cpe:2.3:a:adobe:animate:*:*:*:*:*:macos:*:*": {
+ includedCVEs: []cve{
+ {ID: "CVE-2023-44325"},
+ },
+ continuesToUpdate: true,
+ },
}
cveOSTests := []struct {
diff --git a/server/vulnerabilities/nvd/sync.go b/server/vulnerabilities/nvd/sync.go
index a4405b7c9d..c2e52cc40a 100644
--- a/server/vulnerabilities/nvd/sync.go
+++ b/server/vulnerabilities/nvd/sync.go
@@ -21,8 +21,8 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed"
feednvd "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed/nvd"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
type SyncOptions struct {
diff --git a/server/vulnerabilities/nvd/sync_test.go b/server/vulnerabilities/nvd/sync_test.go
index 4173c3a2e9..d9824cd489 100644
--- a/server/vulnerabilities/nvd/sync_test.go
+++ b/server/vulnerabilities/nvd/sync_test.go
@@ -11,7 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/nettest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"github.com/tj/assert"
)
diff --git a/server/vulnerabilities/nvd/tools/cvefeed/matching_json_test.go b/server/vulnerabilities/nvd/tools/cvefeed/matching_json_test.go
index baa73cad43..df6629b88b 100644
--- a/server/vulnerabilities/nvd/tools/cvefeed/matching_json_test.go
+++ b/server/vulnerabilities/nvd/tools/cvefeed/matching_json_test.go
@@ -94,6 +94,15 @@ func TestMatchJSON(t *testing.T) {
{Part: "a", Vendor: "mozilla", Product: "firefox", Version: "64\\.0"},
},
},
+ {
+ Rule: 3,
+ Inventory: []*wfn.Attributes{
+ {Part: "o", Vendor: "apple", Product: "macos", Version: "14\\.1\\.2"},
+ },
+ Matches: []*wfn.Attributes{
+ {Part: "o", Vendor: "apple", Product: "macos", Version: "14\\.1\\.2"},
+ },
+ },
}
items, err := ParseJSON(bytes.NewBufferString(testJSONdict))
if err != nil {
@@ -103,7 +112,7 @@ func TestMatchJSON(t *testing.T) {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
mm := items[c.Rule].Match(c.Inventory, false)
if len(mm) != len(c.Matches) {
- t.Fatalf("expected %d matches, got %d matches", len(mm), len(c.Matches))
+ t.Fatalf("expected %d matches, got %d matches", len(c.Matches), len(mm))
}
if len(mm) > 0 && !matchesAll(mm, c.Matches) {
t.Fatalf("wrong match: expected %v, got %v", c.Matches, mm)
@@ -112,6 +121,45 @@ func TestMatchJSON(t *testing.T) {
}
}
+func TestTargetSWMatching(t *testing.T) {
+ inventoryAcrobat := []*wfn.Attributes{
+ {Part: "a", Vendor: "adobe", Product: "acrobat", Version: "20\\.001\\.3005", TargetSW: "macos"},
+ }
+
+ items, err := ParseJSON(bytes.NewBufferString(targetSWMatchingJSON))
+ if err != nil {
+ t.Fatalf("failed to parse the dictionary: %v", err)
+ }
+ // matches OS on targetSW
+ if mm := items[0].Match(inventoryAcrobat, true); len(mm) == 0 {
+ t.Fatal("expected Match to match, it did not")
+ }
+
+ // does not match OS on targetSW
+ if mm := items[1].Match(inventoryAcrobat, true); len(mm) != 0 {
+ t.Fatal("expected Match to not match, it did")
+ }
+
+ // matches when OS is not present
+ if mm := items[2].Match(inventoryAcrobat, true); len(mm) == 0 {
+ t.Fatal("expected Match to match, it did not")
+ }
+
+ // does not match OS on targetSW with multiple nodes
+ if mm := items[3].Match(inventoryAcrobat, true); len(mm) != 0 {
+ t.Fatal("expected Match to not match, it did")
+ }
+
+ inventoryWrongOS := []*wfn.Attributes{
+ {Part: "a", Vendor: "adobe", Product: "acrobat", Version: "20\\.001\\.3005", TargetSW: "linux"},
+ }
+
+ // does not match OS on targetSW
+ if mm := items[0].Match(inventoryWrongOS, true); len(mm) != 0 {
+ t.Fatal("expected Match to not match, it did")
+ }
+}
+
func TestMatchJSONrequireVersion(t *testing.T) {
inventory := []*wfn.Attributes{
{Part: "a", Vendor: "microsoft", Product: "ie", Version: "6\\.0"},
@@ -279,9 +327,265 @@ var testJSONdict = `{
}
]
}
- }
+ },
+ {
+ "cve": {
+ "affects": null,
+ "CVE_data_meta": {
+ "ASSIGNER": "product-security@apple.com",
+ "ID": "CVE-2023-42919"
+ },
+ "data_format": "MITRE",
+ "data_type": "CVE",
+ "data_version": "4.0"
+ },
+ "configurations": {
+ "CVE_data_version": "4.0",
+ "nodes": [
+ {
+ "cpe_match": [
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:ipados:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "16.7.3",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:ipados:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "17.2",
+ "versionStartIncluding": "17.0",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:iphone_os:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "16.7.3",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:iphone_os:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "17.2",
+ "versionStartIncluding": "17.0",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "12.7.2",
+ "versionStartIncluding": "12.0.0",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "13.6.3",
+ "versionStartIncluding": "13.0",
+ "vulnerable": true
+ },
+ {
+ "cpe23Uri": "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding": "14.2",
+ "versionStartIncluding": "14.0",
+ "vulnerable": true
+ }
+ ],
+ "operator": "OR"
+ }
+ ]
+ }
+}
] }`
+var targetSWMatchingJSON = `{
+ "CVE_data_type" : "CVE",
+ "CVE_data_format" : "MITRE",
+ "CVE_data_version" : "4.0",
+ "CVE_data_numberOfCVEs" : "7083",
+ "CVE_data_timestamp" : "2018-07-31T07:00Z",
+ "CVE_Items" : [ {
+ "cve" : {
+ "data_type" : "CVE",
+ "data_format" : "MITRE",
+ "data_version" : "4.0",
+ "CVE_data_meta" : {
+ "ID" : "CVE-2023-26369",
+ "ASSIGNER" : "psirt@adobe.com"
+ }
+ },
+ "configurations" : {
+ "CVE_data_version" : "4.0",
+ "nodes" : [ {
+ "operator" : "AND",
+ "children" : [ {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat:*:*:*:*:classic:*:*:*",
+ "versionStartIncluding" : "20.001.3005",
+ "versionEndExcluding" : "20.005.30524",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_dc:*:*:*:*:continuous:*:*:*",
+ "versionStartIncluding" : "15.007.20033",
+ "versionEndExcluding" : "23.006.20320",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_reader:*:*:*:*:classic:*:*:*",
+ "versionStartIncluding" : "20.001.3005",
+ "versionEndExcluding" : "20.005.30524",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_reader_dc:*:*:*:*:continuous:*:*:*",
+ "versionStartIncluding" : "15.007.20033",
+ "versionEndExcluding" : "23.006.20320",
+ "cpe_name" : [ ]
+ } ]
+ }, {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : false,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:-:*:*:*:*:*:*:*",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : false,
+ "cpe23Uri" : "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*",
+ "cpe_name" : [ ]
+ } ]
+ } ],
+ "cpe_match" : [ ]
+ } ]
+ }
+ }, {
+ "cve" : {
+ "data_type" : "CVE",
+ "data_format" : "MITRE",
+ "data_version" : "4.0",
+ "CVE_data_meta" : {
+ "ID" : "CVE-2023-27928",
+ "ASSIGNER" : "product-security@apple.com"
+ }
+ },
+ "configurations" : {
+ "CVE_data_version" : "4.0",
+ "nodes" : [ {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionStartIncluding" : "13.0",
+ "versionEndExcluding" : "13.3",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:tvos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding" : "16.4",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:watchos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding" : "9.4",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionEndExcluding" : "11.7.5",
+ "cpe_name" : [ ]
+ } ]
+ } ]
+ }
+ }, {
+ "cve" : {
+ "data_type" : "CVE",
+ "data_format" : "MITRE",
+ "data_version" : "4.0",
+ "CVE_data_meta" : {
+ "ID" : "CVE-2023-27928",
+ "ASSIGNER" : "product-security@apple.com"
+ }
+ },
+ "configurations" : {
+ "CVE_data_version" : "4.0",
+ "nodes" : [ {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat:*:*:*:*:classic:*:*:*",
+ "versionStartIncluding" : "20.001.3005",
+ "versionEndExcluding" : "20.005.30524",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_dc:*:*:*:*:continuous:*:*:*",
+ "versionStartIncluding" : "15.007.20033",
+ "versionEndExcluding" : "23.006.20320",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_reader:*:*:*:*:classic:*:*:*",
+ "versionStartIncluding" : "20.001.3005",
+ "versionEndExcluding" : "20.005.30524",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:adobe:acrobat_reader_dc:*:*:*:*:continuous:*:*:*",
+ "versionStartIncluding" : "15.007.20033",
+ "versionEndExcluding" : "23.006.20320",
+ "cpe_name" : [ ]
+ } ]
+ } ]
+ }
+ }, {
+ "cve" : {
+ "data_type" : "CVE",
+ "data_format" : "MITRE",
+ "data_version" : "4.0",
+ "CVE_data_meta" : {
+ "ID" : "CVE-2023-28321",
+ "ASSIGNER" : "support@hackerone.com"
+ }
+ },
+ "configurations" : {
+ "CVE_data_version" : "4.0",
+ "nodes" : [ {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:a:haxx:curl:*:*:*:*:*:*:*:*",
+ "versionEndExcluding" : "8.1.0",
+ "cpe_name" : [ ]
+ } ]
+ }, {
+ "operator" : "OR",
+ "children" : [ ],
+ "cpe_match" : [ {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionStartIncluding" : "13.0",
+ "versionEndExcluding" : "13.5",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionStartIncluding" : "12.0",
+ "versionEndExcluding" : "12.6.8",
+ "cpe_name" : [ ]
+ }, {
+ "vulnerable" : true,
+ "cpe23Uri" : "cpe:2.3:o:apple:macos:*:*:*:*:*:*:*:*",
+ "versionStartIncluding" : "11.0",
+ "versionEndExcluding" : "11.7.9",
+ "cpe_name" : [ ]
+ } ]
+ } ]
+ }
+ }
+] }
+`
+
func matchesAll(src, tgt []*wfn.Attributes) bool {
if len(src) != len(tgt) {
return false
diff --git a/server/vulnerabilities/nvd/tools/cvefeed/nvd/match_cpe.go b/server/vulnerabilities/nvd/tools/cvefeed/nvd/match_cpe.go
index 636a78a93a..f82866a3dc 100644
--- a/server/vulnerabilities/nvd/tools/cvefeed/nvd/match_cpe.go
+++ b/server/vulnerabilities/nvd/tools/cvefeed/nvd/match_cpe.go
@@ -69,6 +69,9 @@ func (cm *cpeMatch) Match(attrs []*wfn.Attributes, requireVersion bool) (matches
if cm.match(attr, requireVersion) {
matches = append(matches, attr)
}
+ if osMatch := cm.MatchTargetSW(attr); osMatch != nil {
+ matches = append(matches, osMatch)
+ }
}
return matches
}
diff --git a/server/vulnerabilities/nvd/tools/wfn/matcher.go b/server/vulnerabilities/nvd/tools/wfn/matcher.go
index fca69e6e47..edb0043b4c 100644
--- a/server/vulnerabilities/nvd/tools/wfn/matcher.go
+++ b/server/vulnerabilities/nvd/tools/wfn/matcher.go
@@ -49,14 +49,37 @@ func (a *Attributes) MatchWithoutVersion(attr *Attributes) bool {
matchAttr(a.Other, attr.Other)
}
+func (a *Attributes) MatchTargetSW(attr *Attributes) *Attributes {
+ if a == nil || attr == nil {
+ return nil
+ }
+
+ var osMatch bool
+ var osAttr *Attributes
+ if attr.Part == "a" && attr.TargetSW != "" {
+ osAttr = &Attributes{
+ Part: "o",
+ Product: attr.TargetSW,
+ }
+
+ osMatch = matchAttr(a.Part, osAttr.Part) && matchAttr(a.Product, osAttr.Product)
+ }
+
+ if !osMatch {
+ return nil
+ }
+
+ return osAttr
+}
+
// MatchAll returns a Matcher which matches only if all matchers match
func MatchAll(ms ...Matcher) Matcher {
- return &multiMatcher{ms, true}
+ return &multiMatcher{matchers: ms, allMatch: true}
}
// MatchAll returns a Matcher which matches if any of the matchers match
func MatchAny(ms ...Matcher) Matcher {
- return &multiMatcher{ms, false}
+ return &multiMatcher{matchers: ms, allMatch: false}
}
// DontMatch returns a Matcher which matches if the given matchers doesn't
@@ -68,12 +91,23 @@ type multiMatcher struct {
matchers []Matcher
// if true, match will only return something if all matchers matched at least something
allMatch bool
+ depth int
}
// Match is part of the Matcher interface
func (mm *multiMatcher) Match(attrs []*Attributes, requireVersion bool) []*Attributes {
+ defer func() {
+ if mm.depth > 0 {
+ mm.depth--
+ }
+ }()
+
matched := make(map[*Attributes]bool)
for _, matcher := range mm.matchers {
+ // type check matcher against multiMatcher
+ if _, ok := matcher.(*multiMatcher); !ok {
+ mm.depth++
+ }
matches := matcher.Match(attrs, requireVersion)
if mm.allMatch && len(matches) == 0 {
// all matchers need to match at least one attr
@@ -88,6 +122,11 @@ func (mm *multiMatcher) Match(attrs []*Attributes, requireVersion bool) []*Attri
for m := range matched {
matches = append(matches, m)
}
+
+ if mm.depth == 0 && len(matches) > 1 && !attributesIncludeApp(matches) {
+ return nil
+ }
+
return matches
}
@@ -118,3 +157,12 @@ func (nm notMatcher) Match(attrs []*Attributes, requireVersion bool) (matches []
}
return matches
}
+
+func attributesIncludeApp(attrs []*Attributes) bool {
+ for _, a := range attrs {
+ if a.Part == "a" {
+ return true
+ }
+ }
+ return false
+}
diff --git a/server/webhooks/failing_policies.go b/server/webhooks/failing_policies.go
index e242446b57..53c1b112a8 100644
--- a/server/webhooks/failing_policies.go
+++ b/server/webhooks/failing_policies.go
@@ -11,8 +11,8 @@ import (
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// SendFailingPoliciesBatchedPOSTs sends a failing policy to the provided
diff --git a/server/webhooks/failing_policies_test.go b/server/webhooks/failing_policies_test.go
index 1c2edda535..28b9048ea4 100644
--- a/server/webhooks/failing_policies_test.go
+++ b/server/webhooks/failing_policies_test.go
@@ -18,7 +18,7 @@ import (
"github.com/fleetdm/fleet/v4/server/policies"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
- 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/server/webhooks/host_status.go b/server/webhooks/host_status.go
index 0e140e1145..a3407c86df 100644
--- a/server/webhooks/host_status.go
+++ b/server/webhooks/host_status.go
@@ -6,8 +6,8 @@ import (
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/hashicorp/go-multierror"
)
diff --git a/server/webhooks/host_status_test.go b/server/webhooks/host_status_test.go
index 940bece6ab..c48222991b 100644
--- a/server/webhooks/host_status_test.go
+++ b/server/webhooks/host_status_test.go
@@ -9,7 +9,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
- 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/server/webhooks/vulnerabilities.go b/server/webhooks/vulnerabilities.go
index 05a92ebab6..939f594853 100644
--- a/server/webhooks/vulnerabilities.go
+++ b/server/webhooks/vulnerabilities.go
@@ -8,8 +8,8 @@ import (
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
// TriggerVulnerabilitiesWebhook performs the webhook requests for vulnerabilities.
diff --git a/server/webhooks/vulnerabilities_test.go b/server/webhooks/vulnerabilities_test.go
index f4e58417ba..438b6450d2 100644
--- a/server/webhooks/vulnerabilities_test.go
+++ b/server/webhooks/vulnerabilities_test.go
@@ -12,7 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"github.com/tj/assert"
)
diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go
index c43b2a70ce..3ac41503c8 100644
--- a/server/worker/apple_mdm.go
+++ b/server/worker/apple_mdm.go
@@ -12,8 +12,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest"
- 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/google/uuid"
)
diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go
index beca8767c3..66d47070ba 100644
--- a/server/worker/apple_mdm_test.go
+++ b/server/worker/apple_mdm_test.go
@@ -13,7 +13,7 @@ import (
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
"github.com/fleetdm/fleet/v4/server/ptr"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
diff --git a/server/worker/automation_failer_test.go b/server/worker/automation_failer_test.go
index 842ce9a20e..c1e68613c8 100644
--- a/server/worker/automation_failer_test.go
+++ b/server/worker/automation_failer_test.go
@@ -12,7 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
diff --git a/server/worker/jira.go b/server/worker/jira.go
index 06c75da367..f6c4d2f78e 100644
--- a/server/worker/jira.go
+++ b/server/worker/jira.go
@@ -16,8 +16,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
- 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"
)
// jiraName is the name of the job as registered in the worker.
diff --git a/server/worker/jira_test.go b/server/worker/jira_test.go
index ab6112d164..3a458c6759 100644
--- a/server/worker/jira_test.go
+++ b/server/worker/jira_test.go
@@ -16,7 +16,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
diff --git a/server/worker/macos_setup_assistant.go b/server/worker/macos_setup_assistant.go
index 3850adc641..55f808c4ea 100644
--- a/server/worker/macos_setup_assistant.go
+++ b/server/worker/macos_setup_assistant.go
@@ -9,8 +9,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
- 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"
)
// Name of the macos setup assistant job as registered in the worker. Note that
diff --git a/server/worker/macos_setup_assistant_test.go b/server/worker/macos_setup_assistant_test.go
index cf18ae367b..333362460c 100644
--- a/server/worker/macos_setup_assistant_test.go
+++ b/server/worker/macos_setup_assistant_test.go
@@ -18,7 +18,7 @@ import (
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/ptr"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
@@ -72,7 +72,7 @@ func TestMacosSetupAssistant(t *testing.T) {
DEPClient: apple_mdm.NewDEPClient(depStorage, ds, logger),
}
- const defaultProfileName = "FleetDM default enrollment profile"
+ const defaultProfileName = "Fleet default enrollment profile"
// track the profile assigned to each device
serialsToProfile := map[string]string{
diff --git a/server/worker/worker.go b/server/worker/worker.go
index b670a67029..a5f021ccce 100644
--- a/server/worker/worker.go
+++ b/server/worker/worker.go
@@ -8,8 +8,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ kitlog "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
)
type ctxKey int
diff --git a/server/worker/worker_test.go b/server/worker/worker_test.go
index 4e0446bff5..797f290ae9 100644
--- a/server/worker/worker_test.go
+++ b/server/worker/worker_test.go
@@ -11,7 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
"github.com/tj/assert"
diff --git a/server/worker/zendesk.go b/server/worker/zendesk.go
index 04bda15e5f..d554d8cad9 100644
--- a/server/worker/zendesk.go
+++ b/server/worker/zendesk.go
@@ -15,8 +15,8 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
- 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"
zendesk "github.com/nukosuke/go-zendesk/zendesk"
)
diff --git a/server/worker/zendesk_test.go b/server/worker/zendesk_test.go
index ea593e6e8d..5e8a51315b 100644
--- a/server/worker/zendesk_test.go
+++ b/server/worker/zendesk_test.go
@@ -14,7 +14,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
zendesk "github.com/nukosuke/go-zendesk/zendesk"
"github.com/stretchr/testify/require"
)
diff --git a/terraform/README.md b/terraform/README.md
index da376de755..8ca2664e3b 100644
--- a/terraform/README.md
+++ b/terraform/README.md
@@ -75,7 +75,7 @@ No resources.
| [alb\_config](#input\_alb\_config) | n/a | object({ name = optional(string, "fleet") security_groups = optional(list(string), []) access_logs = optional(map(string), {}) allowed_cidrs = optional(list(string), ["0.0.0.0/0"]) allowed_ipv6_cidrs = optional(list(string), ["::/0"]) egress_cidrs = optional(list(string), ["0.0.0.0/0"]) egress_ipv6_cidrs = optional(list(string), ["::/0"]) extra_target_groups = optional(any, []) https_listener_rules = optional(any, []) tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01") idle_timeout = optional(number, 60) }) | `{}` | no |
| [certificate\_arn](#input\_certificate\_arn) | n/a | `string` | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({ autoscaling_capacity_providers = optional(any, {}) cluster_configuration = optional(any, { execute_command_configuration = { logging = "OVERRIDE" log_configuration = { cloud_watch_log_group_name = "/aws/ecs/aws-ec2" } } }) cluster_name = optional(string, "fleet") cluster_settings = optional(map(string), { "name" : "containerInsights", "value" : "enabled", }) create = optional(bool, true) default_capacity_provider_use_fargate = optional(bool, true) fargate_capacity_providers = optional(any, { FARGATE = { default_capacity_provider_strategy = { weight = 100 } } FARGATE_SPOT = { default_capacity_provider_strategy = { weight = 0 } } }) tags = optional(map(string)) }) | { "autoscaling_capacity_providers": {}, "cluster_configuration": { "execute_command_configuration": { "log_configuration": { "cloud_watch_log_group_name": "/aws/ecs/aws-ec2" }, "logging": "OVERRIDE" } }, "cluster_name": "fleet", "cluster_settings": { "name": "containerInsights", "value": "enabled" }, "create": true, "default_capacity_provider_use_fargate": true, "fargate_capacity_providers": { "FARGATE": { "default_capacity_provider_strategy": { "weight": 100 } }, "FARGATE_SPOT": { "default_capacity_provider_strategy": { "weight": 0 } } }, "tags": {} } | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ mem = optional(number, 4096) cpu = optional(number, 512) image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.31.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "volumes": [] } | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ task_mem = optional(number, null) task_cpu = optional(number, null) mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) image = optional(string, "fleetdm/fleet:v4.51.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) software_installers = optional(object({ create_bucket = optional(bool, true) bucket_name = optional(string, null) bucket_prefix = optional(string, "fleet-software-installers-") s3_object_prefix = optional(string, "") }), { create_bucket = true bucket_name = null bucket_prefix = "fleet-software-installers-" s3_object_prefix = "" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.51.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "pid_mode": null, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "software_installers": { "bucket_name": null, "bucket_prefix": "fleet-software-installers-", "create_bucket": true, "s3_object_prefix": "" }, "task_cpu": null, "task_mem": null, "volumes": [] } | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({ mem = number cpu = number }) | { "cpu": 1024, "mem": 2048 } | no |
| [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module | object({ name = optional(string, "fleet") engine_version = optional(string, "8.0.mysql_aurora.3.04.2") instance_class = optional(string, "db.t4g.large") subnets = optional(list(string), []) allowed_security_groups = optional(list(string), []) allowed_cidr_blocks = optional(list(string), []) apply_immediately = optional(bool, true) monitoring_interval = optional(number, 10) db_parameter_group_name = optional(string) db_parameters = optional(map(string), {}) db_cluster_parameter_group_name = optional(string) db_cluster_parameters = optional(map(string), {}) enabled_cloudwatch_logs_exports = optional(list(string), []) master_username = optional(string, "fleet") snapshot_identifier = optional(string) cluster_tags = optional(map(string), {}) }) | { "allowed_cidr_blocks": [], "allowed_security_groups": [], "apply_immediately": true, "cluster_tags": {}, "db_cluster_parameter_group_name": null, "db_cluster_parameters": {}, "db_parameter_group_name": null, "db_parameters": {}, "enabled_cloudwatch_logs_exports": [], "engine_version": "8.0.mysql_aurora.3.04.2", "instance_class": "db.t4g.large", "master_username": "fleet", "monitoring_interval": 10, "name": "fleet", "snapshot_identifier": null, "subnets": [] } | no |
| [redis\_config](#input\_redis\_config) | n/a | object({ name = optional(string, "fleet") replication_group_id = optional(string) elasticache_subnet_group_name = optional(string) allowed_security_group_ids = optional(list(string), []) subnets = optional(list(string)) availability_zones = optional(list(string)) cluster_size = optional(number, 3) instance_type = optional(string, "cache.m5.large") apply_immediately = optional(bool, true) automatic_failover_enabled = optional(bool, false) engine_version = optional(string, "6.x") family = optional(string, "redis6.x") at_rest_encryption_enabled = optional(bool, true) transit_encryption_enabled = optional(bool, true) parameter = optional(list(object({ name = string value = string })), []) log_delivery_configuration = optional(list(map(any)), []) tags = optional(map(string), {}) }) | { "allowed_security_group_ids": [], "apply_immediately": true, "at_rest_encryption_enabled": true, "automatic_failover_enabled": false, "availability_zones": null, "cluster_size": 3, "elasticache_subnet_group_name": null, "engine_version": "6.x", "family": "redis6.x", "instance_type": "cache.m5.large", "log_delivery_configuration": [], "name": "fleet", "parameter": [], "replication_group_id": null, "subnets": null, "tags": {}, "transit_encryption_enabled": true } | no |
diff --git a/terraform/addons/byo-file-carving/carving/outputs.tf b/terraform/addons/byo-file-carving/carving/outputs.tf
index d3b11ff580..3622e7dfd6 100644
--- a/terraform/addons/byo-file-carving/carving/outputs.tf
+++ b/terraform/addons/byo-file-carving/carving/outputs.tf
@@ -1,10 +1,10 @@
output "fleet_extra_environment_variables" {
value = {
- FLEET_S3_STS_ASSUME_ROLE_ARN = var.iam_role_arn
- FLEET_S3_STS_EXTERNAL_ID = var.sts_external_id
- FLEET_S3_BUCKET = var.s3_bucket_name
- FLEET_S3_REGION = var.s3_bucket_region
- FLEET_S3_PREFIX = var.s3_carve_prefix
+ FLEET_S3_CARVES_STS_ASSUME_ROLE_ARN = var.iam_role_arn
+ FLEET_S3_CARVES_STS_EXTERNAL_ID = var.sts_external_id
+ FLEET_S3_CARVES_BUCKET = var.s3_bucket_name
+ FLEET_S3_CARVES_REGION = var.s3_bucket_region
+ FLEET_S3_CARVES_PREFIX = var.s3_carve_prefix
}
}
diff --git a/terraform/addons/external-vuln-scans/.terraform.lock.hcl b/terraform/addons/external-vuln-scans/.terraform.lock.hcl
index 99ef555634..f284c8030c 100644
--- a/terraform/addons/external-vuln-scans/.terraform.lock.hcl
+++ b/terraform/addons/external-vuln-scans/.terraform.lock.hcl
@@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/aws" {
version = "5.11.0"
hashes = [
"h1:OyEBhYcTPChBb0gooSlLIcrxakh72qAN+Sd8Oo12uoc=",
+ "h1:Wo6WCPXNnbyeRp57Jvlp7VBm9acVAAg6jVmFRU2IWjk=",
"zh:2913af44f9b584f756e5548d5ddc5a251c6d68a7fcd7c41d1418a800a94ef113",
"zh:31d2bfa84608b74ff5896f41b09e5927d7c37d18875277a51dcd75a1fea3f909",
"zh:8538ff18e3b4822178e793f06764efdbb84c62227c1051af7d2409ab7be37bfc",
diff --git a/terraform/addons/external-vuln-scans/README.md b/terraform/addons/external-vuln-scans/README.md
index 5c0e755359..b7a030a206 100644
--- a/terraform/addons/external-vuln-scans/README.md
+++ b/terraform/addons/external-vuln-scans/README.md
@@ -39,14 +39,19 @@ No modules.
| [ecs\_cluster](#input\_ecs\_cluster) | The ecs cluster module that is created by the byo-db module | `any` | n/a | yes |
| [execution\_iam\_role\_arn](#input\_execution\_iam\_role\_arn) | The ARN of the fleet execution role, this is necessary to pass role from ecs events | `any` | n/a | yes |
| [fleet\_config](#input\_fleet\_config) | The root Fleet config object | `any` | n/a | yes |
+| [fleet\_s3\_software\_installers\_config](#input\_fleet\_s3\_software\_installers\_config) | use the output of the byo-vpc module with the same name | `map(string)` | n/a | yes |
+| [fleet\_server\_private\_key\_secret\_arn](#input\_fleet\_server\_private\_key\_secret\_arn) | The ARN of the secret that stores the Fleet private key | `string` | n/a | yes |
| [security\_groups](#input\_security\_groups) | n/a | `list(string)` | n/a | yes |
| [subnets](#input\_subnets) | n/a | `list(string)` | n/a | yes |
| [task\_role\_arn](#input\_task\_role\_arn) | The ARN of the fleet task role, this is necessary to pass role from ecs events | `any` | n/a | yes |
| [vuln\_processing\_cpu](#input\_vuln\_processing\_cpu) | The amount of CPU to dedicate to the vuln processing command | `number` | `1024` | no |
| [vuln\_processing\_memory](#input\_vuln\_processing\_memory) | The amount of memory to dedicate to the vuln processing command | `number` | `4096` | no |
+| [vuln\_processing\_task\_cpu](#input\_vuln\_processing\_task\_cpu) | The amount of CPU to dedicate to the vuln processing task including sidecars | `number` | `1024` | no |
+| [vuln\_processing\_task\_memory](#input\_vuln\_processing\_task\_memory) | The amount of memory to dedicate to the vuln processing task including sidecars | `number` | `4096` | no |
## Outputs
| Name | Description |
|------|-------------|
| [extra\_environment\_variables](#output\_extra\_environment\_variables) | n/a |
+| [vuln\_service\_arn](#output\_vuln\_service\_arn) | n/a |
diff --git a/terraform/addons/external-vuln-scans/main.tf b/terraform/addons/external-vuln-scans/main.tf
index 210531823c..e38514e448 100644
--- a/terraform/addons/external-vuln-scans/main.tf
+++ b/terraform/addons/external-vuln-scans/main.tf
@@ -50,17 +50,20 @@ resource "aws_ecs_service" "fleet" {
resource "aws_ecs_task_definition" "vuln-processing" {
family = "${var.fleet_config.family}-vuln-processing"
- cpu = var.vuln_processing_cpu
- memory = var.vuln_processing_memory
+ cpu = var.vuln_processing_task_cpu
+ memory = var.vuln_processing_task_memory
execution_role_arn = var.execution_iam_role_arn
task_role_arn = var.task_role_arn
network_mode = "awsvpc"
+ pid_mode = var.fleet_config.pid_mode
requires_compatibilities = ["FARGATE"]
container_definitions = jsonencode(concat([
{
name = "fleet-vuln-processing"
image = var.fleet_config.image
+ cpu = var.vuln_processing_cpu
+ memory = var.vuln_processing_memory
essential = true
networkMode = "awsvpc"
secrets = local.secrets
@@ -109,6 +112,14 @@ resource "aws_ecs_task_definition" "vuln-processing" {
name = "FLEET_SERVER_TLS"
value = "false"
},
+ {
+ name = "FLEET_S3_SOFTWARE_INSTALLERS_BUCKET"
+ value = var.fleet_s3_software_installers_config.bucket_name
+ },
+ {
+ name = "FLEET_S3_SOFTWARE_INSTALLERS_PREFIX"
+ value = var.fleet_s3_software_installers_config.s3_object_prefix
+ },
], local.environment),
logConfiguration = {
logDriver = "awslogs"
diff --git a/terraform/addons/external-vuln-scans/outputs.tf b/terraform/addons/external-vuln-scans/outputs.tf
index 69e8da7bef..913a55da72 100644
--- a/terraform/addons/external-vuln-scans/outputs.tf
+++ b/terraform/addons/external-vuln-scans/outputs.tf
@@ -1,6 +1,6 @@
output "extra_environment_variables" {
value = {
- FLEET_VULNERABILITIES_DISABLE_SCHEDULE = "true"
+ FLEET_VULNERABILITIES_DISABLE_SCHEDULE = "true"
}
}
diff --git a/terraform/addons/external-vuln-scans/variables.tf b/terraform/addons/external-vuln-scans/variables.tf
index fbb14d86d7..03f2a4d471 100644
--- a/terraform/addons/external-vuln-scans/variables.tf
+++ b/terraform/addons/external-vuln-scans/variables.tf
@@ -31,6 +31,10 @@ variable "customer_prefix" {
default = "fleet"
}
+variable "fleet_s3_software_installers_config" {
+ type = map(string)
+ description = "use the output of the byo-vpc module with the same name"
+}
variable "fleet_server_private_key_secret_arn" {
type = string
@@ -45,6 +49,19 @@ variable "task_role_arn" {
description = "The ARN of the fleet task role, this is necessary to pass role from ecs events"
}
+variable "vuln_processing_task_memory" {
+ // note must conform to FARGATE breakpoints https://docs.aws.amazon.com/AmazonECS/latest/userguide/fargate-task-defs.html
+ default = 4096
+ description = "The amount of memory to dedicate to the vuln processing task including sidecars"
+}
+
+variable "vuln_processing_task_cpu" {
+ // note must conform to FARGETE breakpoints https://docs.aws.amazon.com/AmazonECS/latest/userguide/fargate-task-defs.html
+ default = 1024
+ description = "The amount of CPU to dedicate to the vuln processing task including sidecars"
+}
+
+
variable "vuln_processing_memory" {
// note must conform to FARGATE breakpoints https://docs.aws.amazon.com/AmazonECS/latest/userguide/fargate-task-defs.html
default = 4096
diff --git a/terraform/addons/osquery-carve/outputs.tf b/terraform/addons/osquery-carve/outputs.tf
index 0b488ccc10..c62c013d10 100644
--- a/terraform/addons/osquery-carve/outputs.tf
+++ b/terraform/addons/osquery-carve/outputs.tf
@@ -1,7 +1,7 @@
output "fleet_extra_environment_variables" {
value = {
- FLEET_S3_BUCKET = aws_s3_bucket.main.bucket
- FLEET_S3_PREFIX = "carve_results/"
+ FLEET_S3_CARVES_BUCKET = aws_s3_bucket.main.bucket
+ FLEET_S3_CARVES_PREFIX = "carve_results/"
}
}
diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf
index 07615950db..93ff50d824 100644
--- a/terraform/addons/vuln-processing/variables.tf
+++ b/terraform/addons/vuln-processing/variables.tf
@@ -24,7 +24,7 @@ variable "fleet_config" {
vuln_processing_cpu = optional(number, 2048)
vuln_data_stream_mem = optional(number, 1024)
vuln_data_stream_cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.51.0")
+ image = optional(string, "fleetdm/fleet:v4.51.1")
family = optional(string, "fleet-vuln-processing")
sidecars = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
@@ -82,7 +82,7 @@ variable "fleet_config" {
vuln_processing_cpu = 2048
vuln_data_stream_mem = 1024
vuln_data_stream_cpu = 512
- image = "fleetdm/fleet:v4.51.0"
+ image = "fleetdm/fleet:v4.51.1"
family = "fleet-vuln-processing"
sidecars = []
extra_environment_variables = {}
diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md
index 131f4305cb..591056fab5 100644
--- a/terraform/byo-vpc/README.md
+++ b/terraform/byo-vpc/README.md
@@ -33,7 +33,7 @@ No requirements.
|------|-------------|------|---------|:--------:|
| [alb\_config](#input\_alb\_config) | n/a | object({ name = optional(string, "fleet") subnets = list(string) security_groups = optional(list(string), []) access_logs = optional(map(string), {}) certificate_arn = string allowed_cidrs = optional(list(string), ["0.0.0.0/0"]) allowed_ipv6_cidrs = optional(list(string), ["::/0"]) egress_cidrs = optional(list(string), ["0.0.0.0/0"]) egress_ipv6_cidrs = optional(list(string), ["::/0"]) extra_target_groups = optional(any, []) https_listener_rules = optional(any, []) tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01") idle_timeout = optional(number, 60) }) | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({ autoscaling_capacity_providers = optional(any, {}) cluster_configuration = optional(any, { execute_command_configuration = { logging = "OVERRIDE" log_configuration = { cloud_watch_log_group_name = "/aws/ecs/aws-ec2" } } }) cluster_name = optional(string, "fleet") cluster_settings = optional(map(string), { "name" : "containerInsights", "value" : "enabled", }) create = optional(bool, true) default_capacity_provider_use_fargate = optional(bool, true) fargate_capacity_providers = optional(any, { FARGATE = { default_capacity_provider_strategy = { weight = 100 } } FARGATE_SPOT = { default_capacity_provider_strategy = { weight = 0 } } }) tags = optional(map(string)) }) | { "autoscaling_capacity_providers": {}, "cluster_configuration": { "execute_command_configuration": { "log_configuration": { "cloud_watch_log_group_name": "/aws/ecs/aws-ec2" }, "logging": "OVERRIDE" } }, "cluster_name": "fleet", "cluster_settings": { "name": "containerInsights", "value": "enabled" }, "create": true, "default_capacity_provider_use_fargate": true, "fargate_capacity_providers": { "FARGATE": { "default_capacity_provider_strategy": { "weight": 100 } }, "FARGATE_SPOT": { "default_capacity_provider_strategy": { "weight": 0 } } }, "tags": {} } | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ mem = optional(number, 4096) cpu = optional(number, 512) image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.31.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "volumes": [] } | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ task_mem = optional(number, null) task_cpu = optional(number, null) mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) image = optional(string, "fleetdm/fleet:v4.51.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) software_installers = optional(object({ create_bucket = optional(bool, true) bucket_name = optional(string, null) bucket_prefix = optional(string, "fleet-software-installers-") s3_object_prefix = optional(string, "") }), { create_bucket = true bucket_name = null bucket_prefix = "fleet-software-installers-" s3_object_prefix = "" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.51.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "pid_mode": null, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "software_installers": { "bucket_name": null, "bucket_prefix": "fleet-software-installers-", "create_bucket": true, "s3_object_prefix": "" }, "task_cpu": null, "task_mem": null, "volumes": [] } | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({ mem = number cpu = number }) | { "cpu": 1024, "mem": 2048 } | no |
| [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module | object({ name = optional(string, "fleet") engine_version = optional(string, "8.0.mysql_aurora.3.04.2") instance_class = optional(string, "db.t4g.large") subnets = optional(list(string), []) allowed_security_groups = optional(list(string), []) allowed_cidr_blocks = optional(list(string), []) apply_immediately = optional(bool, true) monitoring_interval = optional(number, 10) db_parameter_group_name = optional(string) db_parameters = optional(map(string), {}) db_cluster_parameter_group_name = optional(string) db_cluster_parameters = optional(map(string), {}) enabled_cloudwatch_logs_exports = optional(list(string), []) master_username = optional(string, "fleet") snapshot_identifier = optional(string) cluster_tags = optional(map(string), {}) preferred_maintenance_window = optional(string, "thu:23:00-fri:00:00") }) | { "allowed_cidr_blocks": [], "allowed_security_groups": [], "apply_immediately": true, "cluster_tags": {}, "db_cluster_parameter_group_name": null, "db_cluster_parameters": {}, "db_parameter_group_name": null, "db_parameters": {}, "enabled_cloudwatch_logs_exports": [], "engine_version": "8.0.mysql_aurora.3.04.2", "instance_class": "db.t4g.large", "master_username": "fleet", "monitoring_interval": 10, "name": "fleet", "preferred_maintenance_window": "thu:23:00-fri:00:00", "snapshot_identifier": null, "subnets": [] } | no |
| [redis\_config](#input\_redis\_config) | n/a | object({ name = optional(string, "fleet") replication_group_id = optional(string) elasticache_subnet_group_name = optional(string, "") allowed_security_group_ids = optional(list(string), []) subnets = list(string) allowed_cidrs = list(string) availability_zones = optional(list(string), []) cluster_size = optional(number, 3) instance_type = optional(string, "cache.m5.large") apply_immediately = optional(bool, true) automatic_failover_enabled = optional(bool, false) engine_version = optional(string, "6.x") family = optional(string, "redis6.x") at_rest_encryption_enabled = optional(bool, true) transit_encryption_enabled = optional(bool, true) parameter = optional(list(object({ name = string value = string })), []) log_delivery_configuration = optional(list(map(any)), []) tags = optional(map(string), {}) }) | { "allowed_cidrs": null, "allowed_security_group_ids": [], "apply_immediately": true, "at_rest_encryption_enabled": true, "automatic_failover_enabled": false, "availability_zones": [], "cluster_size": 3, "elasticache_subnet_group_name": "", "engine_version": "6.x", "family": "redis6.x", "instance_type": "cache.m5.large", "log_delivery_configuration": [], "name": "fleet", "parameter": [], "replication_group_id": null, "subnets": null, "tags": {}, "transit_encryption_enabled": true } | no |
diff --git a/terraform/byo-vpc/byo-db/README.md b/terraform/byo-vpc/byo-db/README.md
index d2c17644d1..b65261fa5d 100644
--- a/terraform/byo-vpc/byo-db/README.md
+++ b/terraform/byo-vpc/byo-db/README.md
@@ -28,7 +28,7 @@ No requirements.
|------|-------------|------|---------|:--------:|
| [alb\_config](#input\_alb\_config) | n/a | object({ name = optional(string, "fleet") subnets = list(string) security_groups = optional(list(string), []) access_logs = optional(map(string), {}) certificate_arn = string allowed_cidrs = optional(list(string), ["0.0.0.0/0"]) allowed_ipv6_cidrs = optional(list(string), ["::/0"]) egress_cidrs = optional(list(string), ["0.0.0.0/0"]) egress_ipv6_cidrs = optional(list(string), ["::/0"]) extra_target_groups = optional(any, []) https_listener_rules = optional(any, []) tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01") idle_timeout = optional(number, 60) }) | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({ autoscaling_capacity_providers = optional(any, {}) cluster_configuration = optional(any, { execute_command_configuration = { logging = "OVERRIDE" log_configuration = { cloud_watch_log_group_name = "/aws/ecs/aws-ec2" } } }) cluster_name = optional(string, "fleet") cluster_settings = optional(map(string), { "name" : "containerInsights", "value" : "enabled", }) create = optional(bool, true) default_capacity_provider_use_fargate = optional(bool, true) fargate_capacity_providers = optional(any, { FARGATE = { default_capacity_provider_strategy = { weight = 100 } } FARGATE_SPOT = { default_capacity_provider_strategy = { weight = 0 } } }) tags = optional(map(string)) }) | { "autoscaling_capacity_providers": {}, "cluster_configuration": { "execute_command_configuration": { "log_configuration": { "cloud_watch_log_group_name": "/aws/ecs/aws-ec2" }, "logging": "OVERRIDE" } }, "cluster_name": "fleet", "cluster_settings": { "name": "containerInsights", "value": "enabled" }, "create": true, "default_capacity_provider_use_fargate": true, "fargate_capacity_providers": { "FARGATE": { "default_capacity_provider_strategy": { "weight": 100 } }, "FARGATE_SPOT": { "default_capacity_provider_strategy": { "weight": 0 } } }, "tags": {} } | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ mem = optional(number, 4096) cpu = optional(number, 512) image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.31.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "volumes": [] } | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ task_mem = optional(number, null) task_cpu = optional(number, null) mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) image = optional(string, "fleetdm/fleet:v4.51.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = optional(object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }), { password_secret_arn = null user = null database = null address = null rr_address = null }) redis = optional(object({ address = string use_tls = optional(bool, true) }), { address = null use_tls = true }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = optional(object({ arn = string }), { arn = null }) extra_load_balancers = optional(list(any), []) networking = optional(object({ subnets = list(string) security_groups = optional(list(string), null) }), { subnets = null security_groups = null }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) software_installers = optional(object({ create_bucket = optional(bool, true) bucket_name = optional(string, null) bucket_prefix = optional(string, "fleet-software-installers-") s3_object_prefix = optional(string, "") }), { create_bucket = true bucket_name = null bucket_prefix = "fleet-software-installers-" s3_object_prefix = "" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balancers": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.51.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "pid_mode": null, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "software_installers": { "bucket_name": null, "bucket_prefix": "fleet-software-installers-", "create_bucket": true, "s3_object_prefix": "" }, "task_cpu": null, "task_mem": null, "volumes": [] } | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({ mem = number cpu = number }) | { "cpu": 1024, "mem": 2048 } | no |
| [vpc\_id](#input\_vpc\_id) | n/a | `string` | n/a | yes |
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/README.md b/terraform/byo-vpc/byo-db/byo-ecs/README.md
index 0ee9d4b5dc..2055467628 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/README.md
+++ b/terraform/byo-vpc/byo-db/byo-ecs/README.md
@@ -25,6 +25,7 @@ No modules.
| [aws_ecs_task_definition.backend](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource |
| [aws_iam_policy.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+| [aws_iam_policy.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
@@ -32,6 +33,10 @@ No modules.
| [aws_iam_role_policy_attachment.extras](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_iam_role_policy_attachment.role_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_iam_role_policy_attachment.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_s3_bucket.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
+| [aws_s3_bucket_public_access_block.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
+| [aws_s3_bucket_server_side_encryption_configuration.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_secretsmanager_secret.fleet_server_private_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource |
| [aws_secretsmanager_secret_version.fleet_server_private_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource |
| [aws_security_group.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
@@ -39,6 +44,7 @@ No modules.
| [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.fleet](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.fleet-execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.software_installers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
## Inputs
@@ -46,7 +52,7 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| [ecs\_cluster](#input\_ecs\_cluster) | The name of the ECS cluster to use | `string` | n/a | yes |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ mem = optional(number, 4096) cpu = optional(number, 512) image = optional(string, "fleetdm/fleet:v4.51.0") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }) redis = object({ address = string use_tls = optional(bool, true) }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = object({ arn = string }) extra_load_balancers = optional(list(any), []) networking = object({ subnets = list(string) security_groups = optional(list(string), null) }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balacners": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.31.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "volumes": [] } | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({ task_mem = optional(number, null) task_cpu = optional(number, null) mem = optional(number, 4096) cpu = optional(number, 512) pid_mode = optional(string, null) image = optional(string, "fleetdm/fleet:v4.51.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) mount_points = optional(list(any), []) volumes = optional(list(any), []) extra_environment_variables = optional(map(string), {}) extra_iam_policies = optional(list(string), []) extra_execution_iam_policies = optional(list(string), []) extra_secrets = optional(map(string), {}) security_groups = optional(list(string), null) security_group_name = optional(string, "fleet") iam_role_arn = optional(string, null) repository_credentials = optional(string, "") private_key_secret_name = optional(string, "fleet-server-private-key") service = optional(object({ name = optional(string, "fleet") }), { name = "fleet" }) database = object({ password_secret_arn = string user = string database = string address = string rr_address = optional(string, null) }) redis = object({ address = string use_tls = optional(bool, true) }) awslogs = optional(object({ name = optional(string, null) region = optional(string, null) create = optional(bool, true) prefix = optional(string, "fleet") retention = optional(number, 5) }), { name = null region = null prefix = "fleet" retention = 5 }) loadbalancer = object({ arn = string }) extra_load_balancers = optional(list(any), []) networking = object({ subnets = list(string) security_groups = optional(list(string), null) }) autoscaling = optional(object({ max_capacity = optional(number, 5) min_capacity = optional(number, 1) memory_tracking_target_value = optional(number, 80) cpu_tracking_target_value = optional(number, 80) }), { max_capacity = 5 min_capacity = 1 memory_tracking_target_value = 80 cpu_tracking_target_value = 80 }) iam = optional(object({ role = optional(object({ name = optional(string, "fleet-role") policy_name = optional(string, "fleet-iam-policy") }), { name = "fleet-role" policy_name = "fleet-iam-policy" }) execution = optional(object({ name = optional(string, "fleet-execution-role") policy_name = optional(string, "fleet-execution-role") }), { name = "fleet-execution-role" policy_name = "fleet-iam-policy-execution" }) }), { name = "fleetdm-execution-role" }) software_installers = optional(object({ create_bucket = optional(bool, true) bucket_name = optional(string, null) bucket_prefix = optional(string, "fleet-software-installers-") s3_object_prefix = optional(string, "") }), { create_bucket = true bucket_name = null bucket_prefix = "fleet-software-installers-" s3_object_prefix = "" }) }) | { "autoscaling": { "cpu_tracking_target_value": 80, "max_capacity": 5, "memory_tracking_target_value": 80, "min_capacity": 1 }, "awslogs": { "create": true, "name": null, "prefix": "fleet", "region": null, "retention": 5 }, "cpu": 256, "database": { "address": null, "database": null, "password_secret_arn": null, "rr_address": null, "user": null }, "depends_on": [], "extra_environment_variables": {}, "extra_execution_iam_policies": [], "extra_iam_policies": [], "extra_load_balacners": [], "extra_secrets": {}, "family": "fleet", "iam": { "execution": { "name": "fleet-execution-role", "policy_name": "fleet-iam-policy-execution" }, "role": { "name": "fleet-role", "policy_name": "fleet-iam-policy" } }, "iam_role_arn": null, "image": "fleetdm/fleet:v4.51.1", "loadbalancer": { "arn": null }, "mem": 512, "mount_points": [], "networking": { "security_groups": null, "subnets": null }, "pid_mode": null, "private_key_secret_name": "fleet-server-private-key", "redis": { "address": null, "use_tls": true }, "repository_credentials": "", "security_group_name": "fleet", "security_groups": null, "service": { "name": "fleet" }, "sidecars": [], "software_installers": { "bucket_name": null, "bucket_prefix": "fleet-software-installers-", "create_bucket": true, "s3_object_prefix": "" }, "task_cpu": null, "task_mem": null, "volumes": [] } | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({ mem = number cpu = number }) | { "cpu": 1024, "mem": 2048 } | no |
| [vpc\_id](#input\_vpc\_id) | n/a | `string` | `null` | no |
@@ -57,6 +63,8 @@ No modules.
| [appautoscaling\_target](#output\_appautoscaling\_target) | n/a |
| [execution\_iam\_role\_arn](#output\_execution\_iam\_role\_arn) | n/a |
| [fleet\_config](#output\_fleet\_config) | n/a |
+| [fleet\_s3\_software\_installers\_config](#output\_fleet\_s3\_software\_installers\_config) | n/a |
+| [fleet\_server\_private\_key\_secret\_arn](#output\_fleet\_server\_private\_key\_secret\_arn) | n/a |
| [iam\_role\_arn](#output\_iam\_role\_arn) | n/a |
| [logging\_config](#output\_logging\_config) | n/a |
| [non\_circular](#output\_non\_circular) | n/a |
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/iam.tf b/terraform/byo-vpc/byo-db/byo-ecs/iam.tf
index d1f00302e2..a3cc53e830 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/iam.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/iam.tf
@@ -1,3 +1,31 @@
+data "aws_iam_policy_document" "software_installers" {
+ statement {
+ actions = [
+ "s3:GetObject*",
+ "s3:PutObject*",
+ "s3:ListBucket*",
+ "s3:ListMultipartUploadParts*",
+ "s3:DeleteObject",
+ "s3:CreateMultipartUpload",
+ "s3:AbortMultipartUpload",
+ "s3:ListMultipartUploadParts",
+ "s3:GetBucketLocation"
+ ]
+ resources = [aws_s3_bucket.software_installers[0].arn, "${aws_s3_bucket.software_installers[0].arn}/*"]
+ }
+}
+
+resource "aws_iam_policy" "software_installers" {
+ count = var.fleet_config.software_installers.create_bucket == true ? 1 : 0
+ policy = data.aws_iam_policy_document.software_installers.json
+}
+
+resource "aws_iam_role_policy_attachment" "software_installers" {
+ count = var.fleet_config.iam_role_arn == null && var.fleet_config.software_installers.create_bucket == true ? 1 : 0
+ policy_arn = aws_iam_policy.software_installers[0].arn
+ role = aws_iam_role.main[0].name
+}
+
data "aws_iam_policy_document" "fleet" {
statement {
effect = "Allow"
@@ -21,8 +49,8 @@ data "aws_iam_policy_document" "assume_role" {
data "aws_iam_policy_document" "fleet-execution" {
// allow fleet application to obtain the database password from secrets manager
statement {
- effect = "Allow"
- actions = ["secretsmanager:GetSecretValue"]
+ effect = "Allow"
+ actions = ["secretsmanager:GetSecretValue"]
resources = [
var.fleet_config.database.password_secret_arn,
aws_secretsmanager_secret.fleet_server_private_key.arn
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/main.tf b/terraform/byo-vpc/byo-db/byo-ecs/main.tf
index fabf20b413..1cc311cec6 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/main.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/main.tf
@@ -58,8 +58,9 @@ resource "aws_ecs_task_definition" "backend" {
requires_compatibilities = ["FARGATE"]
task_role_arn = var.fleet_config.iam_role_arn == null ? aws_iam_role.main[0].arn : var.fleet_config.iam_role_arn
execution_role_arn = aws_iam_role.execution.arn
- cpu = var.fleet_config.cpu
- memory = var.fleet_config.mem
+ cpu = var.fleet_config.task_cpu == null ? var.fleet_config.cpu : var.fleet_config.task_cpu
+ memory = var.fleet_config.task_mem == null ? var.fleet_config.mem : var.fleet_config.task_mem
+ pid_mode = var.fleet_config.pid_mode
container_definitions = jsonencode(
concat([
{
@@ -146,6 +147,14 @@ resource "aws_ecs_task_definition" "backend" {
name = "FLEET_SERVER_TLS"
value = "false"
},
+ {
+ name = "FLEET_S3_SOFTWARE_INSTALLERS_BUCKET"
+ value = var.fleet_config.software_installers.create_bucket == true ? aws_s3_bucket.software_installers[0].bucket : var.fleet_config.software_installers.bucket_name
+ },
+ {
+ name = "FLEET_S3_SOFTWARE_INSTALLERS_PREFIX"
+ value = var.fleet_config.software_installers.s3_object_prefix
+ },
], local.environment)
}
], var.fleet_config.sidecars))
@@ -262,3 +271,35 @@ resource "aws_secretsmanager_secret_version" "fleet_server_private_key" {
secret_id = aws_secretsmanager_secret.fleet_server_private_key.id
secret_string = random_password.fleet_server_private_key.result
}
+
+// Customer keys are not supported in our Fleet Terraforms at the moment. We will evaluate the
+// possibility of providing this capability in the future.
+// No versioning on this bucket is by design.
+// Bucket logging is not supported in our Fleet Terraforms at the moment. It can be enabled by the
+// organizations deploying Fleet, and we will evaluate the possibility of providing this capability
+// in the future.
+
+resource "aws_s3_bucket" "software_installers" { #tfsec:ignore:aws-s3-encryption-customer-key:exp:2022-07-01 #tfsec:ignore:aws-s3-enable-versioning #tfsec:ignore:aws-s3-enable-bucket-logging:exp:2022-06-15
+ count = var.fleet_config.software_installers.create_bucket == true ? 1 : 0
+ bucket = var.fleet_config.software_installers.bucket_name
+ bucket_prefix = var.fleet_config.software_installers.bucket_prefix
+}
+
+resource "aws_s3_bucket_server_side_encryption_configuration" "software_installers" {
+ count = var.fleet_config.software_installers.create_bucket == true ? 1 : 0
+ bucket = aws_s3_bucket.software_installers[0].bucket
+ rule {
+ apply_server_side_encryption_by_default {
+ sse_algorithm = "aws:kms"
+ }
+ }
+}
+
+resource "aws_s3_bucket_public_access_block" "software_installers" {
+ count = var.fleet_config.software_installers.create_bucket == true ? 1 : 0
+ bucket = aws_s3_bucket.software_installers[0].id
+ block_public_acls = true
+ block_public_policy = true
+ ignore_public_acls = true
+ restrict_public_buckets = true
+}
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/outputs.tf b/terraform/byo-vpc/byo-db/byo-ecs/outputs.tf
index 020f82db51..4c72c7a0b0 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/outputs.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/outputs.tf
@@ -42,3 +42,10 @@ output "fleet_config" {
output "fleet_server_private_key_secret_arn" {
value = aws_secretsmanager_secret.fleet_server_private_key.arn
}
+
+output "fleet_s3_software_installers_config" {
+ value = {
+ bucket_name = var.fleet_config.software_installers.create_bucket == true ? aws_s3_bucket.software_installers[0].bucket : var.fleet_config.software_installers.bucket_name
+ s3_object_prefix = var.fleet_config.software_installers.s3_object_prefix
+ }
+}
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 47a8f4a61b..5f97d5d83e 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -11,9 +11,12 @@ variable "vpc_id" {
variable "fleet_config" {
type = object({
+ task_mem = optional(number, null)
+ task_cpu = optional(number, null)
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.51.0")
+ pid_mode = optional(string, null)
+ image = optional(string, "fleetdm/fleet:v4.51.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -93,11 +96,25 @@ variable "fleet_config" {
}), {
name = "fleetdm-execution-role"
})
+ software_installers = optional(object({
+ create_bucket = optional(bool, true)
+ bucket_name = optional(string, null)
+ bucket_prefix = optional(string, "fleet-software-installers-")
+ s3_object_prefix = optional(string, "")
+ }), {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ })
})
default = {
+ task_mem = null
+ task_cpu = null
mem = 512
cpu = 256
- image = "fleetdm/fleet:v4.51.0"
+ pid_mode = null
+ image = "fleetdm/fleet:v4.51.1"
family = "fleet"
sidecars = []
depends_on = []
@@ -157,6 +174,12 @@ variable "fleet_config" {
policy_name = "fleet-iam-policy-execution"
}
}
+ software_installers = {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ }
}
description = "The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified."
nullable = false
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index db2132225d..47af53f303 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -72,9 +72,12 @@ variable "ecs_cluster" {
variable "fleet_config" {
type = object({
+ task_mem = optional(number, null)
+ task_cpu = optional(number, null)
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.51.0")
+ pid_mode = optional(string, null)
+ image = optional(string, "fleetdm/fleet:v4.51.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -168,11 +171,25 @@ variable "fleet_config" {
}), {
name = "fleetdm-execution-role"
})
+ software_installers = optional(object({
+ create_bucket = optional(bool, true)
+ bucket_name = optional(string, null)
+ bucket_prefix = optional(string, "fleet-software-installers-")
+ s3_object_prefix = optional(string, "")
+ }), {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ })
})
default = {
+ task_mem = null
+ task_cpu = null
mem = 512
cpu = 256
- image = "fleetdm/fleet:v4.51.0"
+ pid_mode = null
+ image = "fleetdm/fleet:v4.51.1"
family = "fleet"
sidecars = []
depends_on = []
@@ -232,6 +249,12 @@ variable "fleet_config" {
policy_name = "fleet-iam-policy-execution"
}
}
+ software_installers = {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ }
}
description = "The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified."
nullable = false
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index 9175ef6bce..f37fcc3a4d 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.51.0"
+ fleet_image = "fleetdm/fleet:v4.51.1"
domain_name = "example.com"
}
@@ -92,7 +92,7 @@ module "vpc" {
}
module "byo-vpc" {
- source = "github.com/fleetdm/fleet//terraform/byo-vpc?ref=tf-mod-byo-vpc-v1.7.0"
+ source = "github.com/fleetdm/fleet//terraform/byo-vpc?ref=tf-mod-byo-vpc-v1.10.1"
vpc_config = {
vpc_id = module.vpc.vpc_id
networking = {
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index 6fd1789b23..abad2853d9 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -165,9 +165,12 @@ variable "ecs_cluster" {
variable "fleet_config" {
type = object({
+ task_mem = optional(number, null)
+ task_cpu = optional(number, null)
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.51.0")
+ pid_mode = optional(string, null)
+ image = optional(string, "fleetdm/fleet:v4.51.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -261,11 +264,25 @@ variable "fleet_config" {
}), {
name = "fleetdm-execution-role"
})
+ software_installers = optional(object({
+ create_bucket = optional(bool, true)
+ bucket_name = optional(string, null)
+ bucket_prefix = optional(string, "fleet-software-installers-")
+ s3_object_prefix = optional(string, "")
+ }), {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ })
})
default = {
+ task_mem = null
+ task_cpu = null
mem = 512
cpu = 256
- image = "fleetdm/fleet:v4.51.0"
+ pid_mode = null
+ image = "fleetdm/fleet:v4.51.1"
family = "fleet"
sidecars = []
depends_on = []
@@ -325,6 +342,12 @@ variable "fleet_config" {
policy_name = "fleet-iam-policy-execution"
}
}
+ software_installers = {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ }
}
description = "The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified."
nullable = false
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 4a69b86f8d..35220057e2 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -50,7 +50,7 @@ locals {
}
module "fleet" {
- source = "github.com/fleetdm/fleet//terraform?ref=tf-mod-root-v1.8.0"
+ source = "github.com/fleetdm/fleet//terraform?ref=tf-mod-root-v1.9.1"
certificate_arn = module.acm.acm_certificate_arn
vpc = {
@@ -63,8 +63,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.51.0"
- image = "fleetdm/fleet:v4.51.0" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.51.1"
+ image = "fleetdm/fleet:v4.51.1" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
@@ -116,7 +116,7 @@ module "fleet" {
# doesn't directly support all the features required. the aws cli is invoked via a null-resource.
module "migrations" {
- source = "github.com/fleetdm/fleet//terraform/addons/migrations?ref=tf-mod-addon-migrations-v2.0.0"
+ source = "github.com/fleetdm/fleet//terraform/addons/migrations?ref=tf-mod-addon-migrations-v2.0.1"
ecs_cluster = module.fleet.byo-vpc.byo-db.byo-ecs.service.cluster
task_definition = module.fleet.byo-vpc.byo-db.byo-ecs.task_definition.family
task_definition_revision = module.fleet.byo-vpc.byo-db.byo-ecs.task_definition.revision
@@ -129,7 +129,7 @@ module "migrations" {
module "osquery-carve" {
# The carve bucket also stores software.
- source = "github.com/fleetdm/fleet//terraform/addons/osquery-carve?ref=tf-mod-addon-osquery-carve-v1.0.1"
+ source = "github.com/fleetdm/fleet//terraform/addons/osquery-carve?ref=tf-mod-addon-osquery-carve-v1.1.0"
osquery_carve_s3_bucket = {
name = local.osquery_carve_bucket_name
}
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 7b58e7fbbf..c73a29640f 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -213,9 +213,12 @@ variable "ecs_cluster" {
variable "fleet_config" {
type = object({
+ task_mem = optional(number, null)
+ task_cpu = optional(number, null)
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.51.0")
+ pid_mode = optional(string, null)
+ image = optional(string, "fleetdm/fleet:v4.51.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -309,11 +312,25 @@ variable "fleet_config" {
}), {
name = "fleetdm-execution-role"
})
+ software_installers = optional(object({
+ create_bucket = optional(bool, true)
+ bucket_name = optional(string, null)
+ bucket_prefix = optional(string, "fleet-software-installers-")
+ s3_object_prefix = optional(string, "")
+ }), {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ })
})
default = {
+ task_mem = null
+ task_cpu = null
mem = 512
cpu = 256
- image = "fleetdm/fleet:v4.51.0"
+ pid_mode = null
+ image = "fleetdm/fleet:v4.51.1"
family = "fleet"
sidecars = []
depends_on = []
@@ -373,6 +390,12 @@ variable "fleet_config" {
policy_name = "fleet-iam-policy-execution"
}
}
+ software_installers = {
+ create_bucket = true
+ bucket_name = null
+ bucket_prefix = "fleet-software-installers-"
+ s3_object_prefix = ""
+ }
}
description = "The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified."
nullable = false
diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt
index 3babb18073..79a8c47a76 100644
--- a/tools/cloner-check/generated_files/appconfig.txt
+++ b/tools/cloner-check/generated_files/appconfig.txt
@@ -12,6 +12,7 @@ github.com/fleetdm/fleet/v4/server/fleet/ServerSettings DeferredSaveHost bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportsDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings ScriptsDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings AIFeaturesDisabled bool
+github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportCap int
github.com/fleetdm/fleet/v4/server/fleet/AppConfig SMTPSettings *fleet.SMTPSettings
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPEnabled bool
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPConfigured bool
diff --git a/tools/dbutils/schema_generator.go b/tools/dbutils/schema_generator.go
index 29735beeea..2fecce8683 100644
--- a/tools/dbutils/schema_generator.go
+++ b/tools/dbutils/schema_generator.go
@@ -12,7 +12,7 @@ import (
"github.com/WatchBeam/clock"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
- "github.com/go-kit/kit/log"
+ "github.com/go-kit/log"
)
const (
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index 2c6c831dfd..49cb2d6119 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.51.0",
+ "version": "v4.51.1",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/jira-integration/main.go b/tools/jira-integration/main.go
index 6015e18bb6..80da821341 100644
--- a/tools/jira-integration/main.go
+++ b/tools/jira-integration/main.go
@@ -19,7 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
"github.com/fleetdm/fleet/v4/server/worker"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
)
func main() {
diff --git a/tools/mdm/apple/applebmapi/main.go b/tools/mdm/apple/applebmapi/main.go
index ae47ee1902..419bb48ed8 100644
--- a/tools/mdm/apple/applebmapi/main.go
+++ b/tools/mdm/apple/applebmapi/main.go
@@ -19,31 +19,25 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
- nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
)
func main() {
mysqlAddr := flag.String("mysql", "localhost:3306", "mysql address")
- appleBMToken := flag.String("apple-bm-token", "", "path to (decrypted) Apple BM token")
+ serverPrivateKey := flag.String("server-private-key", "", "fleet server's private key (to decrypt MDM assets)")
profileUUID := flag.String("profile-uuid", "", "the Apple profile UUID to retrieve")
serialNum := flag.String("serial-number", "", "serial number of a device to get the device details")
flag.Parse()
- if *appleBMToken == "" {
- log.Fatal("must provide Apple BM token")
+ if *serverPrivateKey == "" {
+ log.Fatal("must provide -server-private-key")
}
if *profileUUID != "" && *serialNum != "" {
log.Fatal("only one of -profile-uuid or -serial-number must be provided")
}
- tok, err := os.ReadFile(*appleBMToken)
- if err != nil {
- log.Fatal(err)
- }
-
cfg := config.MysqlConfig{
Protocol: "tcp",
Address: *mysqlAddr,
@@ -55,17 +49,19 @@ func main() {
ConnMaxLifetime: 0,
}
logger := kitlog.NewLogfmtLogger(os.Stderr)
- opts := []mysql.DBOption{mysql.Logger(logger)}
+ opts := []mysql.DBOption{
+ mysql.Logger(logger),
+ mysql.WithFleetConfig(&config.FleetConfig{
+ Server: config.ServerConfig{
+ PrivateKey: *serverPrivateKey,
+ },
+ }),
+ }
mds, err := mysql.New(cfg, clock.C, opts...)
if err != nil {
log.Fatal(err)
}
- var jsonTok nanodep_client.OAuth1Tokens
- if err := json.Unmarshal(tok, &jsonTok); err != nil {
- log.Fatal(err)
- }
-
depStorage, err := mds.NewMDMAppleDEPStorage()
if err != nil {
log.Fatal(err)
diff --git a/tools/nvd/nvdvuln/nvdvuln.go b/tools/nvd/nvdvuln/nvdvuln.go
index b2f2f565da..5ba23209b7 100644
--- a/tools/nvd/nvdvuln/nvdvuln.go
+++ b/tools/nvd/nvdvuln/nvdvuln.go
@@ -184,6 +184,14 @@ func main() {
return nil
}
+ ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) {
+ return nil, nil
+ }
+
+ ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
+ return nil
+ }
+
printf("Translating software to CPE...\n")
err := nvd.TranslateSoftwareToCPE(ctx, ds, *dbDir, logger)
if err != nil {
diff --git a/tools/zendesk-integration/main.go b/tools/zendesk-integration/main.go
index 8cb5c2db56..61218c12c7 100644
--- a/tools/zendesk-integration/main.go
+++ b/tools/zendesk-integration/main.go
@@ -19,7 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
"github.com/fleetdm/fleet/v4/server/worker"
- kitlog "github.com/go-kit/kit/log"
+ kitlog "github.com/go-kit/log"
)
func main() {
diff --git a/website/api/controllers/create-vanta-authorization-request.js b/website/api/controllers/create-vanta-authorization-request.js
index 30fb4dbf15..8bb2c89c63 100644
--- a/website/api/controllers/create-vanta-authorization-request.js
+++ b/website/api/controllers/create-vanta-authorization-request.js
@@ -19,6 +19,10 @@ module.exports = {
fleetApiKey: {
type: 'string',
required: true,
+ },
+ redirectToExternalPageAfterAuthorization: {
+ type: 'string',
+ description: 'If provided, the user will be sent to this URL after they complete the setup of this integration'
}
},
@@ -59,7 +63,6 @@ module.exports = {
},
fn: async function (inputs) {
-
let url = require('url');
// Look for any existing VantaConnection records that use this fleet instance URL.
@@ -139,17 +142,26 @@ module.exports = {
fleetApiKey: inputs.fleetApiKey,
});
}
-
+ let callbackUrl = `/vanta-authorization`;
+ if(inputs.redirectToExternalPageAfterAuthorization){
+ callbackUrl += `?redirectAfterSetup=${inputs.redirectToExternalPageAfterAuthorization}`;
+ }
// Build the authorization URL for this request.
- let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, '/vanta-authorization'))}&response_type=code`;
+ let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, callbackUrl))}&response_type=code`;
- // Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
- this.res.cookie('state', generatedStateForThisRequest, {signed: true});
+ if(inputs.redirectToExternalPageAfterAuthorization){
+ let internalRedirectUrl = `${sails.config.custom.baseUrl}/redirect-vanta-authorization-request?vantaSourceId=${encodeURIComponent(sourceIDForThisRequest)}&state=${encodeURIComponent(generatedStateForThisRequest)}&vantaAuthorizationRequestURL=${encodeURIComponent(vantaAuthorizationRequestURL)}&redirectAfterSetup=${encodeURIComponent(inputs.redirectToExternalPageAfterAuthorization)}`;
- // Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
- this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
-
- return vantaAuthorizationRequestURL;
+ return internalRedirectUrl;
+ // If the useInternalRedirect input was provided, we'll return the URL of an internal endpoiint that will set the required cookies for this request.
+ } else {
+ // Otherwise, if this request came from a user on the connect-vanta page, we'll set the cookies are redirect them directly to Vanta.
+ // Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
+ this.res.cookie('state', generatedStateForThisRequest, {signed: true});
+ // Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
+ this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
+ return vantaAuthorizationRequestURL;
+ }
}
diff --git a/website/api/controllers/redirect-vanta-authorization-request.js b/website/api/controllers/redirect-vanta-authorization-request.js
new file mode 100644
index 0000000000..82e380342f
--- /dev/null
+++ b/website/api/controllers/redirect-vanta-authorization-request.js
@@ -0,0 +1,61 @@
+module.exports = {
+
+
+ friendlyName: 'Redirect vanta authorization request',
+
+
+ description: 'Sets provided inputs in the user`s browser as cookies and redirects them to Vanta.',
+
+
+ inputs: {
+ vantaSourceId: {
+ type: 'string',
+ description: 'The generated vanta Source ID for this request.',
+ required: true,
+ },
+ state: {
+ type: 'string',
+ description: 'The state provided to Vanta when an authorization request was created',
+ required: true,
+ },
+ vantaAuthorizationRequestURL: {
+ type: 'string',
+ description: 'The Vanta authorization url that the user will be directed to after they are sent to this page.',
+ required: true,
+ },
+ redirectAfterSetup: {
+ type: 'string',
+ description: 'The URL that the user will be redirected to after they complete setup.',
+ required: true,
+ }
+ },
+
+
+ exits: {
+ noMatchingVantaConnection: {
+ description: 'No Vanta connection could be found using the provided vantaSourceId',
+ responseType: 'badRequest'
+ },
+ },
+
+
+ fn: async function ({vantaSourceId, state, vantaAuthorizationRequestURL, redirectAfterSetup}) {
+
+ // Find the VantaConnection record that we created when the user created this request.
+ let recordOfThisAuthorization = await VantaConnection.findOne({vantaSourceId: vantaSourceId});
+
+ // If no record of this authorization could be found, return a noMatchingVantaConnection response.
+ if(!recordOfThisAuthorization){
+ throw 'noMatchingVantaConnection';
+ }
+
+ // Set a 'state' and 'vantaSourceId' cookie on the users browser.
+ this.res.cookie('redirectAfterSetup', redirectAfterSetup, {signed: true});
+ this.res.cookie('state', state, {signed: true});
+ this.res.cookie('vantaSourceId', vantaSourceId, {signed: true});
+ // now that the user has the required cookies to complete the vanta integration setup, redirect them to the provided VantaAuthorizationUrl.
+ return this.res.redirect(vantaAuthorizationRequestURL);
+ }
+
+
+};
diff --git a/website/api/controllers/try-fleet/view-explore-data.js b/website/api/controllers/try-fleet/view-explore-data.js
deleted file mode 100644
index e170169934..0000000000
--- a/website/api/controllers/try-fleet/view-explore-data.js
+++ /dev/null
@@ -1,27 +0,0 @@
-module.exports = {
-
-
- friendlyName: 'View explore data',
-
-
- description: 'Display "Explore data" page.',
-
-
- exits: {
-
- success: {
- viewTemplatePath: 'pages/try-fleet/explore-data'
- }
-
- },
-
-
- fn: async function () {
-
- // Respond with view.
- return {};
-
- }
-
-
-};
diff --git a/website/api/controllers/try-fleet/view-query-report.js b/website/api/controllers/try-fleet/view-query-report.js
deleted file mode 100644
index efa6265bbe..0000000000
--- a/website/api/controllers/try-fleet/view-query-report.js
+++ /dev/null
@@ -1,236 +0,0 @@
-module.exports = {
-
-
- friendlyName: 'View query report',
-
-
- description: 'Display "Query report" page.',
-
- inputs: {
-
- hostPlatform: {
- type: 'string',
- required: true,
- description: 'The platform of the host to display results for',
- extendedDescription: '',
- isIn: ['macos', 'linux', 'windows']
- },
-
- tableName: {
- type: 'string',
- required: true,
- description: 'The name of the osquery table to show results for.',
- },
-
- },
-
-
- exits: {
-
- success: {
- viewTemplatePath: 'pages/try-fleet/query-report'
- },
-
- badConfig: {
- responseType: 'badConfig'
- },
-
- redirect: {
- description: 'The requesting user is not logged in.',
- responseType: 'redirect'
- },
-
- invalidTable: {
- responseType: 'notFound',
- description: 'No osquery table with the specified name could be found.'
- },
-
- },
-
-
- fn: async function ({hostPlatform, tableName}) {
-
- if(!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.osqueryTables)){
- throw {badConfig: 'builtStaticContent.osqueryTables'};
- }
-
- // If the requesting user is not logged in, redirect them to the /try-fleet/register page with the specified hostPlatform added as a query parameter.
- if(!this.req.me){
- throw {redirect: `/register?targetPlatform=${encodeURIComponent(hostPlatform)}` };
- }
-
- if(!sails.config.custom.queryIdsByTableName){
- throw new Error('Missing config variable: The dictionary of query ids required to use the query-report page is missing! (sails.config.custom.queryIdsByTableName)');
- }
-
- if(!sails.config.custom.hostIdsByHostPlatform){
- throw new Error('Missing config variable: The dictionary of host ids required to use the query-report page is missing! (sails.config.custom.hostIdsByHostPlatform)');
- }
-
- if(!sails.config.custom.teamApidForQueryReports){
- throw new Error('Missing config variable: The id of the team the query report page gets results for is missing! (sails.config.custom.teamApidForQueryReports)');
- }
-
- if(!sails.config.custom.fleetBaseUrlForQueryReports){
- throw new Error('Missing config variable: The URL of the fleet instance used for query reports is missing! (sails.config.custom.fleetBaseUrlForQueryReports)');
- }
-
- if(!sails.config.custom.fleetTokenForQueryReports){
- throw new Error('Missing config variable: The API token for requests to the Fleet instance used for queyr reports is missing! (sails.config.custom.fleetTokenForQueryReports)');
- }
-
- // ┬ ┬┌─┐┌─┐┌┬┐ ┬┌┐┌┌─┐┌─┐┬─┐┌┬┐┌─┐┌┬┐┬┌─┐┌┐┌
- // ├─┤│ │└─┐ │ ││││├┤ │ │├┬┘│││├─┤ │ ││ ││││
- // ┴ ┴└─┘└─┘ ┴ ┴┘└┘└ └─┘┴└─┴ ┴┴ ┴ ┴ ┴└─┘┘└┘
- let hostIdsByHostPlatform = sails.config.custom.hostIdsByHostPlatform;
- // Get the ID of the host we'll be showing results for.
- let selectedHostId = hostIdsByHostPlatform[hostPlatform];
-
- // Send an HTTP request to get the host details for hosts on the query report team.
- let hostsOnQueryReportTeamApiResponse = await sails.helpers.http.get.with({
- url: sails.config.custom.fleetBaseUrlForQueryReports+'/api/v1/fleet/hosts?team_id='+encodeURIComponent(sails.config.custom.teamApidForQueryReports),
- headers: {
- Authorization: `Bearer ${sails.config.custom.fleetTokenForQueryReports}`
- }
- })
- .intercept((error)=>{
- return new Error(`When sending an API request to ${sails.config.custom.fleetBaseUrlForQueryReports}/api/v1/fleet/hosts?team_id=${sails.config.custom.teamApidForQueryReports} to get information about hosts on the query report team, an error occured: ${error.stack}`);
- });
- if(hostsOnQueryReportTeamApiResponse.hosts.length < 1) {
- throw new Error(`Error! When view-query-report sent a request to ${sails.config.custom.fleetBaseUrlForQueryReports} to get information about the hosts on the query reports team, the API response contained no hosts.`);
- }
-
- let hostsOnTheQueryReportTeam = hostsOnQueryReportTeamApiResponse.hosts;
- let hostsAvailableToQuery = [];
-
- // Get information about these hosts for the host selector dropdown.
- for(let host of hostsOnTheQueryReportTeam) {
- let hostInfoForDropdownSelector = {
- name: host.hostname,
- platform: undefined,
- };
- if(host.platform === 'windows'){
- hostInfoForDropdownSelector.platform = 'Windows';
- } else if(host.platform === 'darwin'){
- hostInfoForDropdownSelector.platform = 'macOS';
- } else {
- hostInfoForDropdownSelector.platform = 'Linux';
- }
- hostsAvailableToQuery.push(hostInfoForDropdownSelector);
- }
-
- // Get the host from the host response
- let hostToGetReportFor = _.find(hostsOnTheQueryReportTeam, {'id': selectedHostId});
- // Convert the host's memory from bytes into GB.
- let hostsMemoryInGb = hostToGetReportFor.memory / (1024 * 1024 * 1024);
-
- // If the host's memory is not a whole number of GB, we'll show the first two decimal places.
- if(Math.floor(hostsMemoryInGb) !== hostsMemoryInGb){
- hostsMemoryInGb = hostsMemoryInGb.toFixed(2);
- }
- // Build a dictionary containing information about this host.
- let hostDetails = {
- os: hostToGetReportFor.os_version,
- hardwareType: hostToGetReportFor.hardware_model,
- memory: hostsMemoryInGb+'GB',
- processor: hostToGetReportFor.cpu_type,
- osqueryVersion: hostToGetReportFor.osquery_version,
- name: hostToGetReportFor.hostname,
- };
-
- // ┌─┐┌─┐┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ┌┬┐┌─┐┌┐ ┬ ┌─┐┌─┐
- // │ │└─┐│─┼┐│ │├┤ ├┬┘└┬┘ │ ├─┤├┴┐│ ├┤ └─┐
- // └─┘└─┘└─┘└└─┘└─┘┴└─ ┴ ┴ ┴ ┴└─┘┴─┘└─┘└─┘
-
- // Get the IDs of the queries for this team.
- let queryIdsByTableName = sails.config.custom.queryIdsByTableName;
-
- // Build an array of osquery tables to display,
- let osqueryTablesToDisplay = [];
- // Only show tables that are compatible with the hosts platform, and that have query ids associated with them in the queryIdsByTableName dictionary.
- // This is so when new tables are added, they will only be displayed if they have a query associated with them.
- if(hostPlatform === 'macos'){
- osqueryTablesToDisplay = _.filter(sails.config.builtStaticContent.osqueryTables, (table)=>{
- return _.contains(table.platforms, 'darwin') && queryIdsByTableName[`${table.name}`] !== undefined;
- });
- } else if(hostPlatform === 'linux'){
- osqueryTablesToDisplay = _.filter(sails.config.builtStaticContent.osqueryTables, (table)=>{
- return _.contains(table.platforms, 'linux') && queryIdsByTableName[`${table.name}`] !== undefined;
- });
- } else if(hostPlatform === 'windows'){
- osqueryTablesToDisplay = _.filter(sails.config.builtStaticContent.osqueryTables, (table)=>{
- return _.contains(table.platforms, 'windows') && queryIdsByTableName[`${table.name}`] !== undefined;
- });
- }
-
- // If the specified table does not exist, or is not compatible with the selected host
- if(!_.contains(_.pluck(osqueryTablesToDisplay, 'name'), tableName)){
- throw 'invalidTable';
- }
- let specifiedOsqueryTable = _.find(osqueryTablesToDisplay, {'name': tableName});
-
- // ┌─┐ ┬ ┬┌─┐┬─┐┬ ┬ ┬─┐┌─┐┌─┐┬ ┬┬ ┌┬┐┌─┐
- // │─┼┐│ │├┤ ├┬┘└┬┘ ├┬┘├┤ └─┐│ ││ │ └─┐
- // └─┘└└─┘└─┘┴└─ ┴ ┴└─└─┘└─┘└─┘┴─┘┴ └─┘
-
- let queryIdToGetReportFor = queryIdsByTableName[`${tableName}`];
- // Send an HTTP request to get the query report for the query for this table.
- let queryReportResponse = await sails.helpers.http.get.with({
- url: sails.config.custom.fleetBaseUrlForQueryReports+'/api/v1/fleet/queries/'+encodeURIComponent(queryIdToGetReportFor)+'/report',
- headers: {
- Authorization: `Bearer ${sails.config.custom.fleetTokenForQueryReports}`
- }
- })
- .intercept((error)=>{
- return new Error(`When sending an API request to ${sails.config.custom.fleetBaseUrlForQueryReports}/api/v1/fleet/queries/${queryIdToGetReportFor}/report to get the latest query report for the ${tableName} table, an error occured: ${error.stack}`);
- });
-
- let queryResults = queryReportResponse.results;
- // Group the query results by the host that reported them.
- let queryResultsByHostIds = _.groupBy(queryResults, 'host_id');
- // Default these to empty arrays, if there are no results for this host, we'll send an empty array of results array to the page and show the user an empty state.
- let reportForThisHost = [];
- let reportWithSortedColumns = [];
- let topResultLastFetchedAt = 0;
-
- // Process the query results for this host (If there are any)
- if(queryResultsByHostIds[selectedHostId]) {
- let resultsForThisHost = queryResultsByHostIds[selectedHostId];
- // Sort the results by their last fetched value.
- let resultsOrderedByLastFetched = _.sortByOrder(resultsForThisHost, 'last_fetched');
- // Get a timestamp of when the last result was fetched from the host.
- topResultLastFetchedAt = Date.parse(resultsOrderedByLastFetched[0].last_fetched);
- // Get an array of the every columns dictionary in the results of this host.
- let unsortedReportForThisHost = _.pluck(resultsOrderedByLastFetched, 'columns');
- // Iterate through the results to sort the columns by their order in the osquery schema.
- for(let result of unsortedReportForThisHost) {
- let sortedColumns = {};
- // Reorder the results by the order of the columns in hte osquery schema, and add the new sorted dictionary to the reportWithSortedColumns array.
- specifiedOsqueryTable.columns.forEach(column => {
-
- if (result[column.name] !== undefined) {
- sortedColumns[column.name] = result[column.name];
- }
- });
- reportWithSortedColumns.push(sortedColumns);
- }
- // Break the results into smaller arrays with 20 values each for table pagination
- reportForThisHost = _.chunk(reportWithSortedColumns, 20);
- }
-
-
- // Respond with view.
- return {
- lastFetchedAt: topResultLastFetchedAt,
- queryReportPages: reportForThisHost,
- osqueryTables: osqueryTablesToDisplay,
- hostPlatform,
- tableName,
- osqueryTableInfo: specifiedOsqueryTable,
- hostDetails, hostsAvailableToQuery,
- };
-
- }
-
-
-};
diff --git a/website/api/controllers/view-vanta-authorization.js b/website/api/controllers/view-vanta-authorization.js
index 3fd465bb0b..cf6bb03afd 100644
--- a/website/api/controllers/view-vanta-authorization.js
+++ b/website/api/controllers/view-vanta-authorization.js
@@ -89,7 +89,9 @@ module.exports = {
if(!updatedRecord){
throw new Error(`When trying to update a VantaConnection record (id: ${recordOfThisAuthorization.id}) with an authorization token from Vanta, the database record associated with this request has gone missing.`);
}
-
+ if(this.req.signedCookies.redirectAfterSetup){
+ return this.res.redirect(this.req.signedCookies.redirectAfterSetup);
+ }
return {
showSuccessMessage: true
};
diff --git a/website/assets/.eslintrc b/website/assets/.eslintrc
index 94cfe4e273..96d18f3427 100644
--- a/website/assets/.eslintrc
+++ b/website/assets/.eslintrc
@@ -48,7 +48,7 @@
"moment": true,
"docsearch": true,
"Chart": true,
- // "google": true,
+ "gtag": true,
// ...etc.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-1600x900@2x.png b/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-1600x900@2x.png
new file mode 100644
index 0000000000..82a8c3dbc1
Binary files /dev/null and b/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-1600x900@2x.png differ
diff --git a/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-internews_logo-256x237@2x.png b/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-internews_logo-256x237@2x.png
new file mode 100644
index 0000000000..189dd8f6bf
Binary files /dev/null and b/website/assets/images/articles/deploy-fleet-on-ubuntu-with-elastic-internews_logo-256x237@2x.png differ
diff --git a/website/assets/images/demo/fleet-system-maintenance.png b/website/assets/images/demo/fleet-system-maintenance.png
new file mode 100644
index 0000000000..c9f0e46ee4
Binary files /dev/null and b/website/assets/images/demo/fleet-system-maintenance.png differ
diff --git a/website/assets/images/permanent/mdm-migration-sonoma-1500x938.png b/website/assets/images/permanent/mdm-migration-sonoma-1500x938.png
new file mode 100644
index 0000000000..494d04ea3c
Binary files /dev/null and b/website/assets/images/permanent/mdm-migration-sonoma-1500x938.png differ
diff --git a/website/assets/js/pages/contact.page.js b/website/assets/js/pages/contact.page.js
index 3a4314bf01..5b8865b67c 100644
--- a/website/assets/js/pages/contact.page.js
+++ b/website/assets/js/pages/contact.page.js
@@ -80,13 +80,18 @@ parasails.registerPage('contact', {
methods: {
submittedContactForm: async function() {
-
+ if(typeof gtag !== 'undefined'){
+ gtag('event','website_contact_forms');
+ }
// Show the success message.
this.cloudSuccess = true;
},
submittedTalkToUsForm: async function() {
this.syncing = true;
+ if(typeof gtag !== 'undefined'){
+ gtag('event','website_contact_forms');
+ }
if(this.formData.numberOfHosts > 700){
this.goto(`https://calendly.com/fleetdm/talk-to-us?email=${encodeURIComponent(this.formData.emailAddress)}&name=${encodeURIComponent(this.formData.firstName+' '+this.formData.lastName)}`);
} else {
diff --git a/website/assets/js/pages/docs/basic-documentation.page.js b/website/assets/js/pages/docs/basic-documentation.page.js
index 5b7262b8f2..a2540321f7 100644
--- a/website/assets/js/pages/docs/basic-documentation.page.js
+++ b/website/assets/js/pages/docs/basic-documentation.page.js
@@ -222,6 +222,13 @@ parasails.registerPage('basic-documentation', {
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
+ clickSwagRequestCTA: function () {
+ if(typeof gtag !== 'undefined') {
+ gtag('event','website_swag_request');
+ }
+ this.goto('https://kqphpqst851.typeform.com/to/ZfA3sOu0');
+ },
+
clickCTA: function (slug) {
this.goto(slug);
},
diff --git a/website/assets/js/pages/entrance/signup.page.js b/website/assets/js/pages/entrance/signup.page.js
index 598e3b073e..4f0904a935 100644
--- a/website/assets/js/pages/entrance/signup.page.js
+++ b/website/assets/js/pages/entrance/signup.page.js
@@ -67,6 +67,9 @@ parasails.registerPage('signup', {
// redirect to the /start page.
// > (Note that we re-enable the syncing state here. This is on purpose--
// > to make sure the spinner stays there until the page navigation finishes.)
+ if(typeof gtag !== 'undefined'){
+ gtag('event','website_sign_up');
+ }
this.syncing = true;
this.goto(this.pageToRedirectToAfterRegistration);// « / start if the user came here from the start now button, or customers/new-license if the user came here from the "Get your license" link.
}
diff --git a/website/assets/js/pages/try-fleet/explore-data.page.js b/website/assets/js/pages/try-fleet/explore-data.page.js
deleted file mode 100644
index bc237e3f11..0000000000
--- a/website/assets/js/pages/try-fleet/explore-data.page.js
+++ /dev/null
@@ -1,25 +0,0 @@
-parasails.registerPage('explore-data', {
- // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
- // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
- // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
- data: {
- //…
- },
-
- // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
- // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
- // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
- beforeMount: function() {
- //…
- },
- mounted: async function() {
- //…
- },
-
- // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
- // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
- // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
- methods: {
- //…
- }
-});
diff --git a/website/assets/js/pages/try-fleet/query-report.page.js b/website/assets/js/pages/try-fleet/query-report.page.js
deleted file mode 100644
index f3d76c9494..0000000000
--- a/website/assets/js/pages/try-fleet/query-report.page.js
+++ /dev/null
@@ -1,115 +0,0 @@
-parasails.registerPage('query-report', {
- // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
- // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
- // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
- data: {
- pageToDisplay: 0,
- numberOfPages: undefined,
- selectedTable: undefined,
- selectedHost: undefined,
- tableToDisplay: undefined,
- tableHeaders: undefined,
- hostToDisplayResultsFor: undefined,
- hostPlatformFriendlyName: '',
- hostInfo: {},
- },
-
- // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
- // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
- // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
- beforeMount: function() {
- this.selectedHost = this.hostPlatform;
- this.hostInfo = this.hostDetails;
- if(this.selectedHost === 'macos'){
- this.hostPlatformFriendlyName = 'macOS';
- }
- if(this.selectedHost === 'windows'){
- this.hostPlatformFriendlyName = 'Windows';
- }
- if(this.selectedHost === 'linux'){
- this.hostPlatformFriendlyName = 'Linux';
- }
- this.numberOfPages = this.queryReportPages.length;
- this.tableToDisplay = this.tableName;
- this.selectedTable = this.tableToDisplay;
- this.hostToDisplayResultsFor = this.selectedHost;
- this.tableHeaders = [];
- if(this.numberOfPages !== 0){
- let columnsToShow = _.keys(this.queryReportPages[this.pageToDisplay][0]);
- for(let column in columnsToShow){
- let columnName = columnsToShow[column];
- let columnDefinition = _.find(this.osqueryTableInfo.columns, {name: columnName});
- let columnInfo = {name: columnName, description: columnDefinition.description};
- this.tableHeaders.push(columnInfo);
- }
- }
-
- },
- mounted: async function() {
- if(this.numberOfPages > 0){
- this.addTableEdgeShadow();
- $('[data-toggle="tooltip"]').tooltip();
- }
- },
-
- watch: {
- selectedTable: function(val){
- if(val !== this.tableToDisplay){
- this.goto(`/try-fleet/explore-data/${this.selectedHost}/${this.selectedTable}`);
- }
- },
- hostToDisplayResultsFor: function(val){
- if(val !== this.selectedHost){
- if(val === 'Linux'){
- this.goto(`/try-fleet/explore-data/linux/apparmor_events`);
- } else if(val === 'Windows'){
- this.goto(`/try-fleet/explore-data/windows/appcompat_shims`);
- } else {
- this.goto(`/try-fleet/explore-data/macos/account_policy_data`);
- }
- }
- }
- },
-
- // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
- // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
- // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
- methods: {
- addTableEdgeShadow: function() {
- let tableContainer = document.querySelector('.table-responsive');
- if(tableContainer) {
- let isEdgeOfResultsTableVisible = tableContainer.scrollWidth - tableContainer.scrollLeft === tableContainer.clientWidth;
- if (!isEdgeOfResultsTableVisible) {
- tableContainer.classList.add('right-edge-shadow');
- }
-
- tableContainer.addEventListener('scroll', (event)=>{
- let container = event.target;
- let isScrolledFullyToLeft = container.scrollLeft === 0;
- let isScrolledFullyToRight = (container.scrollWidth - container.scrollLeft <= container.clientWidth + 1);
- // Update the class on the table container based on how much the table is scrolled.
- if (isScrolledFullyToLeft) {
- container.classList.remove('edge-shadow', 'left-edge-shadow');
- container.classList.add('right-edge-shadow');
- } else if (isScrolledFullyToRight) {
- container.classList.remove('edge-shadow', 'right-edge-shadow');
- container.classList.add('left-edge-shadow');
- } else if(!isScrolledFullyToRight && !isScrolledFullyToLeft) {
- container.classList.remove('left-edge-shadow', 'right-edge-shadow');
- container.classList.add('edge-shadow');
- }
- });
- }
- },
- clickChangePage: function(page){
- this.pageToDisplay = page - 1;
- let tableContainer = document.querySelector('.table-responsive');
- window.scrollTo({
- top: tableContainer.offsetTop - 90,
- left: 0,
- behavior: 'smooth',
- });
- },
-
- }
-});
diff --git a/website/assets/styles/importer.less b/website/assets/styles/importer.less
index e6624efd3d..7bc353d322 100644
--- a/website/assets/styles/importer.less
+++ b/website/assets/styles/importer.less
@@ -74,7 +74,5 @@
@import 'pages/try-fleet/waitlist.less';
@import 'pages/admin/sandbox-waitlist.less';
@import 'pages/integrations.less';
-@import 'pages/try-fleet/query-report.less';
-@import 'pages/try-fleet/explore-data.less';
@import 'pages/start.less';
diff --git a/website/assets/styles/pages/try-fleet/explore-data.less b/website/assets/styles/pages/try-fleet/explore-data.less
deleted file mode 100644
index a89455a8e2..0000000000
--- a/website/assets/styles/pages/try-fleet/explore-data.less
+++ /dev/null
@@ -1,104 +0,0 @@
-#explore-data {
- [purpose='page-container'] {
- max-width: 1200px;
- padding-top: 120px;
- padding-left: 100px;
- padding-right: 100px;
- }
- [purpose='page-title'] {
- text-align: center;
- h1 {
- font-size: 48px;
- font-weight: 800;
- line-height: 57.6px;
- }
- p {
- color: @core-fleet-black-75;
- font-size: 14px;
- font-weight: 400;
- line-height: 21px;
- margin-bottom: 24px;
- }
- }
- a {
- color: @core-fleet-black-75;
- }
- a:hover {
- text-decoration: none;
- [purpose='explore-data-card'] {
- box-shadow: 0px 4px 16px 0px #E2E4EA;
- }
- }
- [purpose='explore-data-card'] {
- padding: 43px 52px;
- margin-left: 12px;
- margin-right: 12px;
- border-radius: 12px;
- box-shadow: none;
- }
-
- [purpose='card-body'] {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- text-align: center;
- width: 240px;
- h4 {
- font-size: 16px;
- font-style: normal;
- font-weight: 800;
- line-height: 19.2px;
- margin-bottom: 4px;
- }
- p {
- margin-bottom: 0px;
- }
- img {
- height: 60px;
- width: auto;
- margin-bottom: 16px;
- }
- }
- @media (max-width: 1201px) {
- [purpose='page-container'] {
- padding-left: 60px;
- padding-right: 60px;
- }
- [purpose='card-body'] {
- width: unset;
- }
- }
- @media (max-width: 991px) {
- [purpose='page-container'] {
- padding-left: 40px;
- padding-right: 40px;
- }
- [purpose='explore-data-card'] {
- padding: 32px;
- }
- }
- @media (max-width: 767px) {
- [purpose='page-container'] {
- padding-top: 40px;
- padding-left: 24px;
- padding-right: 24px;
- }
- [purpose='card-body'] {
- width: 240px;
- }
- [purpose='explore-data-card'] {
- padding: 43px 52px;
- margin-bottom: 24px;
- margin-left: 0;
- margin-right: 0;
- border-radius: 12px;
- box-shadow: none;
- }
- }
- @media (max-width: 450px) {
- [purpose='card-body'] {
- width: 100%;
- }
- }
-}
diff --git a/website/assets/styles/pages/try-fleet/query-report.less b/website/assets/styles/pages/try-fleet/query-report.less
deleted file mode 100644
index 2b761eeadb..0000000000
--- a/website/assets/styles/pages/try-fleet/query-report.less
+++ /dev/null
@@ -1,394 +0,0 @@
-#query-report {
-
- h1 {
- font-size: 24px;
- font-weight: 800;
- line-height: 28.8px;
- margin-bottom: 8px;
- }
-
- p {
- color: @core-fleet-black-75;
- font-size: 14px;
- line-height: 21px;
- }
-
- strong {
- color: @core-fleet-black;
- }
-
- a {
- color: @core-vibrant-blue;
- &:hover {
- text-decoration: none;
- }
- }
-
- hr {
- margin-top: 40px;
- margin-bottom: 40px;
- }
-
- [purpose='page-container'] {
- max-width: 1200px;
- padding-top: 40px;
- padding-bottom: 40px;
- padding-left: 80px;
- padding-right: 80px;
- }
-
- [purpose='host-details-card'] {
- margin-bottom: 60px;
- box-shadow: none;
- border-radius: 8px;
- padding: 40px;
-
- }
-
- [purpose='host-details'] {
- div:not(:last-of-type) {
- margin-right: 40px;
- }
- }
-
- [purpose='host-selector'] {
- margin-right: 16px;
- min-width: 150px;
- height: 40px;
- border-radius: 8px;
- padding: 8px 12px;
- background: #FAFAFA;
- border: 1px solid @core-vibrant-blue-15;
- cursor: pointer;
- }
-
- [purpose='host-selector-dropwdown'] {
- cursor: pointer;
- width: 150px;
- }
-
- [purpose='query-results-container'] {
- width: calc(~'100% - 267px - 80px');
- flex-grow: 1;
- padding-right: 40px;
- padding-left: 20px;
- }
-
- [purpose='table-selector'] {
- width: 370px;
- height: 40px;
- border-radius: 8px;
- padding: 8px 12px;
- background: #FAFAFA;
- border: 1px solid @core-vibrant-blue-15;
- cursor: pointer;
- }
-
- [purpose='table-selector-dropwdown'] {
- width: 370px;
- cursor: pointer;
- max-height: 400px;
- overflow-y: scroll;
- }
-
- .edge-shadow {
- box-shadow: -4px 0 3px 0px rgba(0, 0, 0, 0.07) inset, 4px 0 3px 0px rgba(0, 0, 0, 0.07) inset;
- }
-
- .right-edge-shadow {
- box-shadow: -4px 0px 3px 0px rgba(0, 0, 0, 0.07) inset;
- }
-
- .left-edge-shadow {
- box-shadow: 4px 0 3px 0px rgba(0, 0, 0, 0.07) inset;
- }
-
- [purpose='table-description'] {
- margin-top: 24px;
- word-break: break-word;
- color: @core-fleet-black-75;
- font-size: 14px;
- p:first-child {
- font-size: 16px;
- margin-bottom: 24px;
- }
- ul {
- padding-inline-start: 16px;
- }
- li {
- padding-bottom: 8px;
- }
- code:not(.bash):not(.hljs):not(.nohighlight):not(.mermaid) {
- background: #F1F0FF;
- padding: 4px 8px;
- font-family: @code-font;
- font-size: 13px;
- line-height: 16px;
- color: @core-fleet-black;
- }
- pre {
- code {
- background: none;
- padding: 0px;
- font-family: @code-font;
- font-size: 13px;
- line-height: 16px;
- color: @core-fleet-black;
- }
- padding: 24px;
- border: 1px solid #E2E4EA;
- border-radius: 6px;
- margin: 0px 0px 40px;
- font-family: @code-font;
- background: #F9FAFC;
- white-space: break-spaces;
- }
- p, a {
- font-size: 14px;
- }
- }
- [purpose='table-container'] {
- margin-bottom: 24px;
- z-index: 0;
- position: relative;
- border-radius: 8px;
- border: 1px solid #D6DCE2;
- }
- [purpose='query-result-table'] {
- margin-bottom: 0px;
- p {
- color: @core-fleet-black-75;
- font-size: 14px;
- line-height: 21px;
- margin-bottom: 0px;
- white-space: nowrap;
- }
- [purpose='column-name'] {
- p {
- font-weight: 700;
- text-transform: capitalize;
- border-bottom: 1px dashed;
- display: inline;
- }
- }
- tbody {
- color: #515774;
- border-radius: 8px;
- td {
- max-height: 48px;
- height: 48px;
- padding-left: 16px;
- padding-right: 16px;
- border-right: 1px solid @border-lt-gray;
- border-top: 1px solid @border-lt-gray;
- position: relative;
- }
- tr {
- td:last-child {
- border-right: none;
- }
- }
- tr:first-child {
- td {
- border-top: none;
- background-color: rgba(0, 43, 128, 0.0235294);
- // background-color: #F9FAFC
- }
- td:first-child {
- border-top-left-radius: 8px;
- }
- td:last-child {
- border-top-right-radius: 8px;
- }
- }
- tr:last-child {
- td:first-child {
- border-bottom-left-radius: 8px;
- }
- td:last-child {
- border-bottom-right-radius: 8px;
- }
- }
- }
- }
- [purpose='page-indicator'] {
- margin-bottom: 40px;
- a:not(:first-of-type) {
- padding-left: 8px;
- }
- a:not(:last-of-type) {
- padding-right: 8px;
- }
- a {
- color: @core-vibrant-blue;
- font-weight: 700;
- white-space: nowrap;
- cursor: pointer;
- user-select: none;
- [purpose='previous-chevron'] {
- display: inline;
- height: 8px;
- width: 4px;
- margin-right: 8px;
- }
- [purpose='next-chevron'] {
- display: inline;
- height: 8px;
- width: 4px;
- margin-left: 8px;
- }
- }
- }
-
- [purpose='call-to-action-container'] {
- width: 269px;
- margin-left: 20px;
- [purpose='call-to-action-card'] {
- padding: 16px;
- border-radius: 8px;
- box-shadow: none;
- }
- [purpose='banner-text'] {
- h3 {
- font-size: 20px;
- font-weight: 800;
- line-height: 24px;
- margin-bottom: 24px;
- }
- p {
- margin-bottom: 24px;
- font-size: 14px;
- font-style: normal;
- font-weight: 400;
- line-height: 21px;
- }
- a {
- color: #FFF;
- height: 48px;
- font-size: 16px;
- font-weight: 700;
- line-height: 21px;
- margin-bottom: 24px;
- }
- [purpose='fleetctl-link'] {
- a {
- color: @core-vibrant-blue;
- font-size: 14px;
- font-weight: 400;
- line-height: 21px;
- text-align: center;
- margin-bottom: 4px;
- height: auto;
- }
- p {
- font-size: 12px;
- font-weight: 400;
- line-height: 18px;
- text-align: center;
- margin-bottom: 8px;
- }
-
- }
-
- }
- [purpose='banner-image'] {
- height: auto;
- width: 100%;
- img {
- margin-bottom: 24px;
- height: auto;
- width: 100%;
- }
- }
- }
-
- @media (max-width: 1201px) {
- // <1201 width:
- }
- @media (max-width: 991px) {
- // <992 width:
- // - The pages padding is reduced: 80px » 40px
- // - The available-data div's horizontal padding is reduced to 0
- // - The call to action banner's width is reduced: 269px » 214px
- [purpose='page-container'] {
- padding-left: 40px;
- padding-right: 40px;
- }
- [purpose='query-results-container'] {
- padding-right: 0px;
- padding-left: 0px;
- }
- [purpose='call-to-action-container'] {
- width: 214px;
- }
-
- }
- @media (max-width: 767px) {
- // <768px width:
- // - The layout of the page shifts to a column layout.
- // - the padding of the host-details-card is reduced.
- // - The host selector's width is set to 100%
- // - The call to action's width is increased to 100% and it is now stacked under the table
- // - The available-data div's width is increased to 100%
- [purpose='host-details-card'] {
- padding: 32px 24px;
- }
- [purpose='host-details'] {
- div:not(:last-of-type) {
- margin-right: unset;
- }
- }
- [purpose='host-selector-container'] {
- width: 100%;
- }
- [purpose='host-selector'] {
- margin-right: unset;
- width: 100%;
- }
- [purpose='host-selector-dropwdown'] {
- width: calc(~'100% - 48px');
- cursor: pointer;
- }
- [purpose='query-results-container'] {
- width: 100%;
- }
- [purpose='call-to-action-container'] {
- max-width: unset;
- width: 100%;
- margin-left: 0px;
- [purpose='banner-image'] {
- img {
- height: auto;
- width: 100%;
- }
- }
- }
- }
- @media (max-width: 576px) {
- // <577px width:
- // - The page's padding is reduced to 20px.
- // - The table selector's width is increased to 100%
- [purpose='page-container'] {
- padding-top: 20px;
- padding-left: 20px;
- padding-right: 20px;
- }
- [purpose='table-selector'] {
- width: 100%;
- }
- [purpose='table-selector-dropwdown'] {
- width: calc(~'100% - 40px');
- overflow-x: hidden;
- .dropdown-item {
- padding-left: 8px;
- padding-right: 16px;
- span {
- max-width: calc(~'100% - 40px');
- white-space: break-spaces;
- }
- }
- }
- }
-}
-
-
diff --git a/website/config/custom.js b/website/config/custom.js
index 777e34f195..682b35fb47 100644
--- a/website/config/custom.js
+++ b/website/config/custom.js
@@ -119,6 +119,7 @@ module.exports.custom = {
// 🚀 Code for core product and integrations
'ee/tools/puppet': 'georgekarrv', //« Puppet integration (especially useful with macOS MDM turned on) -- FYI: Originally developed by request from "customer-eponym"
+ 'tools/api': 'lukeheath', //« Scripts used to interact with the Fleet API
// ⚗️ Reference, config surface, built-in queries, API, and other documentation
// 'docs/Using-Fleet/REST-API.md': '', // « Covered in CODEOWNERS (2023-07-22)
@@ -197,6 +198,7 @@ module.exports.custom = {
// Code for core product and integrations
'ee/tools/puppet': ['lukeheath', 'roperzh', 'gillespi314', 'mna', 'georgekarrv'],
+ 'tools/api': ['lukeheath', 'georgekarrv', 'sharon-fdm'],//« Scripts for interacting with the Fleet API
// Reference, config surface, built-in queries, API, and other documentation
'docs': ['rachaelshaw'],// (default for docs)
@@ -305,27 +307,6 @@ module.exports.custom = {
// (both in Fleet's query console and on fleetdm.com)
versionOfOsquerySchemaToUseWhenGeneratingDocumentation: '5.12.1',
- // ███████╗██╗ ██╗██████╗ ██╗ ██████╗ ██████╗ ███████╗ ██████╗ █████╗ ████████╗ █████╗
- // ██╔════╝╚██╗██╔╝██╔══██╗██║ ██╔═══██╗██╔══██╗██╔════╝ ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗
- // █████╗ ╚███╔╝ ██████╔╝██║ ██║ ██║██████╔╝█████╗ ██║ ██║███████║ ██║ ███████║
- // ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██╔══██║ ██║ ██╔══██║
- // ███████╗██╔╝ ██╗██║ ███████╗╚██████╔╝██║ ██║███████╗ ██████╔╝██║ ██║ ██║ ██║ ██║
- // ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
- //
- // Config variables in this section are used for the /try-fleet/explore-data page on fleetdm.com
-
- // For sending requests to a Fleet instance:
- // fleetBaseUrlForQueryReports: '…',
- // fleetTokenForQueryReports: '…',
-
- // The API ID of the team of hosts created for query reports.
- // teamApidForQueryReports:
-
- // A dictionary where each key is the name of an osquery table, and the value is the API ID of the query that selects all information from that table. e.g., {'account_policy_data': 2045, 'ad_config': 2047, …}
- // queryIdsByTableName: {…}
-
- // A dictionary where each key is the lowercased platform, and the value is the API ID of a host. e.g., {'macos': 92, 'windows': 94, 'linux': 93}
- // hostIdsByHostPlatform: {…}
// ███╗ ███╗██╗███████╗ ██████╗
// ████╗ ████║██║██╔════╝██╔════╝
diff --git a/website/config/http.js b/website/config/http.js
index 7a6eb57744..8dca7de67c 100644
--- a/website/config/http.js
+++ b/website/config/http.js
@@ -29,16 +29,17 @@ module.exports.http = {
* *
***************************************************************************/
- // order: [
- // 'cookieParser',
- // 'session',
- // 'bodyParser',
- // 'compress',
- // 'poweredBy',
- // 'router',
- // 'www',
- // 'favicon',
- // ],
+ order: [
+ 'cookieParser',
+ 'session',
+ 'bodyParser',
+ 'compress',
+ 'poweredBy',
+ 'router',
+ 'www',
+ 'favicon',
+ 'middlewareErrorHandler'
+ ],
/***************************************************************************
@@ -67,6 +68,16 @@ module.exports.http = {
return middlewareFn;
})(),
+ // Note: this middleware function will run for every HTTP request, but will only handle errors thrown by the serve-static middleware if a user requests an invalid byte range of a static asset.
+ middlewareErrorHandler: function(err, req, res, next) {
+ // If this is a 'RangeNotSatisfiableError' error, respond with a 416 status code.
+ if (err.message === 'Range Not Satisfiable') {
+ return res.status(416).send();
+ } else {
+ return next(err);
+ }
+ },
+
},
};
diff --git a/website/config/policies.js b/website/config/policies.js
index 2c46a7f477..5eeba3abdd 100644
--- a/website/config/policies.js
+++ b/website/config/policies.js
@@ -50,9 +50,8 @@ module.exports.policies = {
'deliver-mdm-demo-email': true,
'view-support': true,
'view-integrations': true,
- 'try-fleet/view-explore-data': true,
- 'try-fleet/view-query-report': true,
'deliver-talk-to-us-form-submission': true,
'get-human-interpretation-from-osquery-sql': true,
'customers/view-new-license': true,
+ 'redirect-vanta-authorization-request': true,
};
diff --git a/website/config/routes.js b/website/config/routes.js
index 7814ed055c..23108166bd 100644
--- a/website/config/routes.js
+++ b/website/config/routes.js
@@ -167,22 +167,6 @@ module.exports.routes = {
}
},
- 'GET /try-fleet/explore-data': {
- action: 'try-fleet/view-explore-data',
- locals: {
- pageTitleForMeta: 'Explore real data | Fleet',
- pageDescriptionForMeta: 'See live data collected from a real device enrolled in Fleet.',
- }
- },
-
- 'GET /try-fleet/explore-data/:hostPlatform/:tableName': {// [?]: https://github.com/fleetdm/fleet/blob/97a0d419e1a25d2155606c09b9c483ae5067544e/website/api/controllers/try-fleet/view-query-report.js#L16
- action: 'try-fleet/view-query-report',
- locals: {
- pageTitleForMeta: 'Explore real data | Fleet',
- pageDescriptionForMeta: 'See live data collected from a real device enrolled in Fleet.',
- }
- },
-
'GET /admin/email-preview': {
action: 'admin/view-email-templates',
locals: {
@@ -452,6 +436,12 @@ module.exports.routes = {
'GET /docs/deploy/deploy-fleet-on-kubernetes': '/guides/deploy-fleet-on-kubernetes',
'GET /docs/using-fleet/mdm-macos-setup': '/docs/using-fleet/mdm-setup',
'GET /transparency': '/better',
+ 'GET /try-fleet/explore-data': '/tables/account_policy_data',
+ 'GET /try-fleet/explore-data/:hostPlatform/:tableName': {
+ fn: (req, res)=>{
+ return res.redirect('/tables/'+req.param('tableName'));
+ }
+ },
// ╔╦╗╦╔═╗╔═╗ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗ ┬ ╔╦╗╔═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔╦╗╔═╗
// ║║║║╚═╗║ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗ ┌┼─ ║║║ ║║║║║║║║ ║ ║╠═╣ ║║╚═╗
@@ -525,6 +515,7 @@ module.exports.routes = {
'GET /learn-more-about/renew-apns': '/docs/using-fleet/mdm-setup#apple-push-notification-service-apns',
'GET /learn-more-about/renew-abm': '/docs/using-fleet/mdm-setup#apple-business-manager-abm',
'GET /learn-more-about/fleet-server-private-key': '/docs/configuration/fleet-server-configuration#server-private-key',
+ 'GET /learn-more-about/host-identifiers': '/docs/rest-api/rest-api#get-host-by-identifier',
// Sitemap
// =============================================================================================================
@@ -589,7 +580,8 @@ module.exports.routes = {
'POST /api/v1/create-or-update-one-newsletter-subscription': { action: 'create-or-update-one-newsletter-subscription' },
'/api/v1/unsubscribe-from-all-newsletters': { action: 'unsubscribe-from-all-newsletters' },
'POST /api/v1/admin/build-license-key': { action: 'admin/build-license-key' },
- 'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request' },
+ 'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request', csrf: false },
+ 'GET /redirect-vanta-authorization-request': { action: 'redirect-vanta-authorization-request' },
'POST /api/v1/deliver-mdm-beta-signup': { action: 'deliver-mdm-beta-signup' },
'POST /api/v1/get-human-interpretation-from-osquery-sql': { action: 'get-human-interpretation-from-osquery-sql', csrf: false },
'POST /api/v1/deliver-apple-csr ': { action: 'deliver-apple-csr', csrf: false},
diff --git a/website/scripts/build-static-content.js b/website/scripts/build-static-content.js
index 098eecd1f5..3c29eb18ba 100644
--- a/website/scripts/build-static-content.js
+++ b/website/scripts/build-static-content.js
@@ -24,7 +24,7 @@ module.exports = {
let builtStaticContent = {};
let rootRelativeUrlPathsSeen = [];
let baseHeadersForGithubRequests;
- let osqueryTables = [];
+
if(githubAccessToken) {// If a github token was provided, set headers for requests to GitHub.
baseHeadersForGithubRequests = {
'User-Agent': 'Fleet-Standard-Query-Library',
@@ -674,8 +674,7 @@ module.exports = {
let keywordsForSyntaxHighlighting = [];
keywordsForSyntaxHighlighting.push(table.name);
if(!table.hidden) { // If a table has `"hidden": true` the table won't be shown in the final schema, and we'll ignore it
- // If the table is not hidden, we'l ladd it to our osquery tables configuration.
- let tableInfoForQueryReports = { name: table.name, columns: [], platforms: table.platforms};
+
// Start building the markdown string for this table.
let tableMdString = '\n## '+table.name;
if(table.evented){
@@ -684,28 +683,13 @@ module.exports = {
}
// Add the tables description to the markdown string and start building the table in the markdown string
tableMdString += '\n\n'+table.description+'\n\n|Column | Type | Description |\n|-|-|-|\n';
- if(table.description !== ''){
- let tableDescriptionForQueryReports = table.description;
- if(table.notes){
- tableDescriptionForQueryReports += '\n\n**Notes:**\n\n'+table.notes;
- }
- let htmlDescriptionForTableInfo = await sails.helpers.strings.toHtml.with({mdString: tableDescriptionForQueryReports, addIdsToHeadings: false});
- tableInfoForQueryReports.description = htmlDescriptionForTableInfo;
- }
// Iterate through the columns of the table, we'll add a row to the markdown table element for each column in this schema table
for(let column of _.sortBy(table.columns, 'name')) {
- // Create an object for this column to add to the osqueryTables config.
- let columnInfoForQueryReports = {
- name: column.name
- };
let columnDescriptionForTable = '';// Set the initial value of the description that will be added to the table for this column.
if(column.description) {
columnDescriptionForTable = column.description;
- // Convert the markdown description for this table into HTML for tooltips on /try-fleet/explore-data/* pages
- columnInfoForQueryReports.description = await sails.helpers.strings.toHtml.with({mdString: column.description, addIdsToHeadings: false});
}
- tableInfoForQueryReports.columns.push(columnInfoForQueryReports);
// Replacing pipe characters and newlines with html entities in column descriptions to keep it from breaking markdown tables.
columnDescriptionForTable = columnDescriptionForTable.replace(/\|/g, '|').replace(/\n/gm, '
');
@@ -787,8 +771,7 @@ module.exports = {
} else {
await sails.helpers.fs.write(htmlOutputPath, htmlString);
}
- // Add information about this table to the osqueryTables array
- osqueryTables.push(tableInfoForQueryReports);
+
// Add this table to the array of schemaTables in builtStaticContent.
builtStaticContent.markdownPages.push({
url: '/tables/'+encodeURIComponent(table.name),
@@ -1078,7 +1061,6 @@ module.exports = {
},
]);
- builtStaticContent.osqueryTables = osqueryTables;
// ██████╗ ███████╗██████╗ ██╗ █████╗ ██████╗███████╗ ███████╗ █████╗ ██╗██╗ ███████╗██████╗ ██████╗
// ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝ ██╔════╝██╔══██╗██║██║ ██╔════╝██╔══██╗██╔════╝██╗
// ██████╔╝█████╗ ██████╔╝██║ ███████║██║ █████╗ ███████╗███████║██║██║ ███████╗██████╔╝██║ ╚═╝
diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs
index d416310816..58108d5e53 100644
--- a/website/views/layouts/layout.ejs
+++ b/website/views/layouts/layout.ejs
@@ -546,8 +546,6 @@
-
-
diff --git a/website/views/pages/docs/basic-documentation.ejs b/website/views/pages/docs/basic-documentation.ejs
index 8e6853c7b7..eb47296580 100644
--- a/website/views/pages/docs/basic-documentation.ejs
+++ b/website/views/pages/docs/basic-documentation.ejs
@@ -157,7 +157,7 @@
Releases
Support
-
+
Request Fleet swag
diff --git a/website/views/pages/endpoint-ops.ejs b/website/views/pages/endpoint-ops.ejs
index 922d0422c8..0cc54f5e05 100644
--- a/website/views/pages/endpoint-ops.ejs
+++ b/website/views/pages/endpoint-ops.ejs
@@ -4,7 +4,7 @@
Endpoint operations
- Understand your computers
+ <%= primaryBuyingSituation==='eo-security'? 'Instrument your endpoints' : 'Understand your computers'%>
@@ -20,7 +20,7 @@
Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.
<% } else if(['eo-security', 'vm'].includes(primaryBuyingSituation)) { %>
Osquery on easy mode
-
You don’t need to be an osquery expert to get the answers you need from your endpoints.
+
Build the agent in "read-only" mode or enable remote scripting to automatically mitigate misconfigurations and vulnerabilities.
Pulse check anything
Use a live connection to every endpoint to simplify audit, compliance, and reporting from workstations to data centers.
Ship data to any platform
@@ -31,7 +31,7 @@
Ship data to any platform
Ship logs to any platform like Splunk, Snowflake, or any streaming infrastructure like AWS Kinesis and Apache Kafka.
Osquery on easy mode
-
You don’t need to be an osquery expert to get the answers you need from your devices.
+
Use "read-only" mode or enable remote scripting to automate anything on every operating system, including Linux.
<% }%>
Start now
@@ -219,6 +219,7 @@
Remotely disable/enable agent features, choose plugins, and keep osquery up to date.
Import community queries from other security teams at top brands like Palantir and Fastly.
Implement the Center for Internet Security (CIS) benchmarks (one click, 400+ queries, supported by Fleet). Or customize exactly the queries you need.
+
Monitor sensitive production environments without enabling remote scripting by building the agent in "read-only" mode.
@@ -288,7 +289,7 @@
Endpoint operations
-
Understand your computers
+
<%= primaryBuyingSituation==='eo-security'? 'Instrument your endpoints' : 'Understand your computers'%>
Start now
Talk to us
diff --git a/website/views/pages/homepage.ejs b/website/views/pages/homepage.ejs
index 82876b6272..be222748af 100644
--- a/website/views/pages/homepage.ejs
+++ b/website/views/pages/homepage.ejs
@@ -7,7 +7,7 @@
<%/* Hero text */%>
<%- partial('../partials/primary-tagline.partial.ejs') %>
-
Replace the sprawl with secure, open-source <%= primaryBuyingSituation === 'mdm'? 'device management' : primaryBuyingSituation === 'vm'? 'reporting' : 'software' %> that works the way you want.
+
Replace the sprawl with <%= primaryBuyingSituation === 'mdm'? 'secure, open-source device management that works the way you want' : primaryBuyingSituation === 'vm'? 'secure, open-source reporting that works the way you want' : primaryBuyingSituation === 'eo-security'? 'universal, open-source endpoint visibility' : 'simple, open-source software for managing computers' %>.
Start now
Talk to us
@@ -30,7 +30,7 @@
NEW
<% if(!primaryBuyingSituation) {%>
-
"This is a terrible time to mess with my computer."
+
Timing is everything
Install updates and force restarts when your users’ computers are actually free.
<%} else if(['mdm'].includes(primaryBuyingSituation)) {%>
Timing is everything
@@ -65,7 +65,7 @@
-
“Fleet lets us to be as lightweight as possible while surfacing data to other teams and partners.”
+
“Fleet lets us be as lightweight as possible while surfacing data to other teams and partners.”
Nick Fohs
Systems and IT infrastructure manager
@@ -93,8 +93,8 @@
Endpoint ops
-
Understand your computers
-
A quick-fast way to gather device data across all your endpoints. Pulse check or automate anything on any platform.
+
<%= primaryBuyingSituation==='eo-security'? 'Instrument your endpoints' : 'Understand your computers'%>
+
A <%= primaryBuyingSituation==='eo-security'? 'lightweight' : 'quick-fast' %> way to gather <%= primaryBuyingSituation==='vm'? 'patch level and custom reports across all your computing devices, even in OT and production environments' : primaryBuyingSituation==='eo-security'? 'deep context and custom telemetry across all your endpoints, even servers' : primaryBuyingSituation==='mdm'||primaryBuyingSituation==='eo-it'? 'compliance and inventory data across all your devices' : 'device data across all your computers' %>. Pulse check or automate anything on any platform.
@@ -137,8 +137,8 @@
Endpoint ops
-
Understand your computers
-
A quick-fast way to gather device data across all your endpoints. Pulse check or automate anything on any platform.
+
<%= primaryBuyingSituation==='eo-security'? 'Instrument your endpoints' : 'Understand your computers'%>
+
A <%= primaryBuyingSituation==='eo-security'? 'lightweight' : 'quick-fast' %> way to gather <%= primaryBuyingSituation==='vm'? 'patch level and custom reports across all your computing devices, even in OT and production environments' : primaryBuyingSituation==='eo-security'? 'deep context and custom telemetry across all your endpoints, even servers' : primaryBuyingSituation==='mdm'||primaryBuyingSituation==='eo-it'? 'compliance and inventory data across all your devices' : 'device data across all your computers' %>. Pulse check or automate anything on any platform.
@@ -216,8 +216,8 @@
Endpoint ops
-
Understand your computers
-
A quick-fast way to gather device data across all your endpoints. Pulse check or automate anything on any platform.
+
<%= primaryBuyingSituation==='eo-security'? 'Instrument your endpoints' : 'Understand your computers'%>
+
A <%= primaryBuyingSituation==='eo-security'? 'lightweight' : 'quick-fast' %> way to gather <%= primaryBuyingSituation==='vm'? 'patch level and custom reports across all your computing devices, even in OT and production environments' : primaryBuyingSituation==='eo-security'? 'deep context and custom telemetry across all your endpoints, even servers' : primaryBuyingSituation==='mdm'||primaryBuyingSituation==='eo-it'? 'compliance and inventory data across all your devices' : 'device data across all your computers' %>. Pulse check or automate anything on any platform.
diff --git a/website/views/pages/try-fleet/explore-data.ejs b/website/views/pages/try-fleet/explore-data.ejs
deleted file mode 100644
index 35264ba50f..0000000000
--- a/website/views/pages/try-fleet/explore-data.ejs
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
Explore real data
-
See live data collected from a real device enrolled in Fleet.
-
-
-
-
-<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>
diff --git a/website/views/pages/try-fleet/query-report.ejs b/website/views/pages/try-fleet/query-report.ejs
deleted file mode 100644
index ca0ca38393..0000000000
--- a/website/views/pages/try-fleet/query-report.ejs
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
- <% /* Host details card */%>
-
-
-
-
Explore real data
-
See live data collected from a real {{hostPlatformFriendlyName}} device running Fleet.
-
-
-
-
-
-
-
-
-
-
Hardware model
-
{{hostInfo.hardwareType}}
-
-
-
Memory
-
{{hostInfo.memory}}
-
-
Processor
-
{{hostInfo.processor}}
-
-
-
Operating system
-
{{hostInfo.os}}
-
-
-
Osquery
-
{{hostInfo.osqueryVersion}}
-
-
-
-
-
- <% /* Query results container (osquery table details, table selector, and query results table) */%>
-
-
Available data
-
-
- {{selectedTable}}
-
-
-
-
- <%- osqueryTableInfo.description %>
-
-
-
-
Last fetched:
- <%// Query table %>
-
-
-
-
-
-
- {{column.name}}
- {{column.name}}
-
-
-
- {{columnValue}}
---
-
-
-
-
- <%// Page indicator %>
-
-
- <%// page indicator for < 5 pages %>
-
-
-
-
-
Your live query returned no results.
-
-
- <% /* Call to action */%>
-
-
-
-
This is not Fleet
-
-
-
-
-
-
This is not Fleet
-
This is a simple app built on the Fleet API to get a taste of the data. Get in our calendar to see what you can do with multiple hosts in the Fleet UI.
-
Talk to us
-
-
-
-
-
-
-
-<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>
diff --git a/website/views/partials/primary-tagline.partial.ejs b/website/views/partials/primary-tagline.partial.ejs
index 28697e8272..a9ced91ac5 100644
--- a/website/views/partials/primary-tagline.partial.ejs
+++ b/website/views/partials/primary-tagline.partial.ejs
@@ -1 +1 @@
-<%= primaryBuyingSituation === 'mdm' ? 'Your last MDM migration' : primaryBuyingSituation === 'vm' ? 'Focus on vulnerabilities, not vendors' : primaryBuyingSituation === 'eo-security' ? 'Light in every corner' : primaryBuyingSituation === 'eo-it' ? 'Untangle your endpoints' : 'Automation at work' %>
+<%= primaryBuyingSituation === 'mdm' ? 'Your last MDM migration' : primaryBuyingSituation === 'vm' ? 'Focus on vulnerabilities, not vendors' : primaryBuyingSituation === 'eo-security' ? 'Light in every corner' : primaryBuyingSituation === 'eo-it' ? 'Untangle your endpoints' : 'Let people work' %>
diff --git a/yarn.lock b/yarn.lock
index d63a6f5f9b..88ad4e8ccb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6666,11 +6666,11 @@ brace-expansion@^2.0.1:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
- fill-range "^7.0.1"
+ fill-range "^7.1.1"
broadcast-channel@^3.4.1:
version "3.7.0"
@@ -9222,10 +9222,10 @@ filelist@^1.0.1:
dependencies:
minimatch "^5.0.1"
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"