Merge branch 'main' into mna-17401-puppet-related-integration-tests

This commit is contained in:
Martin Angers 2024-04-08 08:45:56 -04:00 committed by GitHub
commit 66f90ccd93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
190 changed files with 4588 additions and 6020 deletions

View file

@ -38,11 +38,11 @@ jobs:
run: |
expires=$(curl -s http://tuf.fleetctl.com/timestamp.json | jq -r '.signed.expires' | cut -c 1-10)
today=$(date "+%Y-%m-%d")
tomorrow=$(date -d "$today + 1 day" "+%Y-%m-%d")
warning_at=$(date -d "$today + 2 day" "+%Y-%m-%d")
expires_sec=$(date -d "$expires" "+%s")
tomorrow_sec=$(date -d "$tomorrow" "+%s")
warning_at_sec=$(date -d "$warning_at" "+%s")
if [ "$expires_sec" -le "$tomorrow_sec" ]; then
if [ "$expires_sec" -le "$warning_at_sec" ]; then
exit 1
else
exit 0

View file

@ -24,7 +24,7 @@ defaults:
shell: bash
env:
OSQUERY_VERSION: 5.12.0
OSQUERY_VERSION: 5.12.1
permissions:
contents: read

View file

@ -85,7 +85,7 @@ jobs:
- name: Install wine and wix
if: matrix.os == 'macos-latest'
run: |
./scripts/macos-install-wine.sh
./scripts/macos-install-wine.sh -n
wget https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip -nv -O wix.zip
mkdir wix
unzip wix.zip -d wix

View file

@ -12,3 +12,8 @@ CVE-2020-7753
# We feel like the risk of DoS using this technique, which requires being logged in, is low probability and low impact, as such we will not update glob-parent only for this CVE
CVE-2020-28469
# 2024/04/04 (github.com/goreleaser/nfpm/v2 should be updated)
# When packaging linux files, we do not use global permissions. Manually verified that packed fleet-osquery files do not have group/global write permissions.
CVE-2023-32698

View file

@ -1,3 +1,47 @@
## Fleet 4.48.0 (Apr 03, 2024)
### Endpoint operations
- Added integration with Google Calendar.
* Fleet admins can enable Google Calendar integration by using a Google service account with domain-wide delegation.
* Calendar integration is enabled at the team level for specific team policies.
* If the policy is failing, a calendar event will be put on the host user's calendar for the 3rd Tuesday of the month.
* During the event, Fleet will fire a webhook. IT admins should use this webhook to trigger a script or MDM command that will remediate the issue.
- Reduced the number of 'Deadlock found' errors seen by the server when multiple hosts share the same UUID.
- Removed outdated tooltips from UI.
- Added hover states to clickable elements.
- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.
### Device management (MDM)
- Added Windows MDM support to the `osquery-perf` host-simulation command.
- Added a missing database index to the MDM Windows enrollments table that will improve performance at scale.
- Migrate MDM-related endpoints to new paths, deprecating (but still supporting indefinitely) the old endpoints.
- Adds API functionality for creating DDM declarations, both individually and as a batch.
- Added DDM activities to the fleet UI.
- Added the `enable_release_device_manually` configuration setting for a team and no team. **Note** that the macOS automatic enrollment profile cannot set the `await_device_configured` option anymore, this setting is controlled by Fleet via the new `enable_release_device_manually` option.
- Automatically release a macOS DEP-enrolled device after enrollment commands and profiles have been delivered, unless `enable_release_device_manually` is set to `true`.
### Vulnerability management
- Added Visual Studio extensions to Fleet's software inventory.
### Bug fixes
- Fixed a bug where valid MDM enrollments would show up as unmanaged (EnrollmentState 3).
- Fixed flash message from closing when a modal closes.
- Fixed a bug where OS version information would not get detected on Windows Server 2019.
- Fixed issue where getting host details failed when attempting to read the host's BitLocker status from the datastore.
- Fixed false negative vulnerabilities on macOS Homebrew python packages.
- Fixed styling of live query disabled warning.
- Fixed issue where Windows MDM profile processing was skipping `<Add>` commands.
- Fixed UI's ability to bulk delete hosts when "All teams" is selected.
- Fixed error state rendering on the global Host status expiry settings page, fix error state alignment for tooltip-wrapper field labels across organization settings.
- Fixed `GET fleet/os_versions` and `GET fleet/os_versions/[id]` so team users no longer have access to os versions on hosts from other teams.
- `fleetctl gitops` now batch processes queries and policies.
- Fixed UI bug to render the query platform correctly for queries imported from the standard query library.
- Fixed issue where Microsoft Edge was not reporting vulnerabilities.
- Fixed a bug where all Windows MDM enrollments were detected as automatic.
- Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500.
- Fixed query reports so they reset when there is a change to the selected platform or selected minimum osquery version.
- Fixed live query sort of SQL result sort for both string and numerical columns.
## Fleet 4.47.3 (Mar 26, 2024)
### Bug fixes

View file

@ -74,7 +74,7 @@ go.mod @fleetdm/go
#
# (see website/config/custom.js for DRIs of other paths not listed here)
##############################################################################################
/handbook/company/pricing-features-table.yml @mikermcneil # « CEO is current DRI for features table
/handbook/company/pricing-features-table.yml @noahtalerman # « Head of Product Design is current DRI for features table
##############################################################################################
# 🦿 Repo automation and change control settings

View file

@ -71,7 +71,7 @@ The Fleet community is full of [kind and helpful people](https://fleetdm.com/han
The landscape of cybersecurity and IT is too complex. Let's open it up.
Contributions are welcome, whether you answer questions on [Slack](#chat) / [GitHub](https://github.com/fleetdm/fleet/issues) / [StackOverflow](https://stackoverflow.com/search?q=osquery) / [LinkedIn](https://linkedin.com/company/fleetdm) / [Twitter](https://twitter.com/fleetctl), improve the documentation or [website](./website), write a tutorial, give a talk at a conference or local meetup, give an [interview on a podcast](https://fleetdm.com/podcasts), troubleshoot reported issues, or [submit a patch](https://fleetdm.com/docs/contributing/contributing). The Fleet code of conduct is [on GitHub](https://github.com/fleetdm/fleet/blob/main/CODE_OF_CONDUCT.md).
Contributions are welcome, whether you answer questions on [Slack](https://fleetdm.com/slack) / [GitHub](https://github.com/fleetdm/fleet/issues) / [StackOverflow](https://stackoverflow.com/search?q=osquery) / [LinkedIn](https://linkedin.com/company/fleetdm) / [Twitter](https://twitter.com/fleetctl), improve the documentation or [website](./website), write a tutorial, give a talk at a conference or local meetup, give an [interview on a podcast](https://fleetdm.com/podcasts), troubleshoot reported issues, or [submit a patch](https://fleetdm.com/docs/contributing/contributing). The Fleet code of conduct is [on GitHub](https://github.com/fleetdm/fleet/blob/main/CODE_OF_CONDUCT.md).
<!-- - Great contributions are motivated by real-world use cases or learning.
- Some of the most valuable contributions might not touch any code at all.
@ -81,7 +81,7 @@ Contributions are welcome, whether you answer questions on [Slack](#chat) / [Git
To see what Fleet can do, head over to [fleetdm.com](https://fleetdm.com) and try it out for yourself, grab time with one of the maintainers to discuss, or visit the docs and roll it out to your organization.
#### Production deployment
Fleet is simple enough to [spin up for yourself](https://fleetdm.com/docs/using-fleet/learn-how-to-use-fleet). Or you can have us [host it for you](https://fleetdm.com/pricing). Premium features are [available](https://fleetdm.com/pricing) either way.
Fleet is simple enough to [spin up for yourself](https://fleetdm.com/docs/get-started/tutorials-and-guides). Or you can have us [host it for you](https://fleetdm.com/pricing). Premium features are [available](https://fleetdm.com/pricing) either way.
#### Documentation
Complete documentation for Fleet can be found at [https://fleetdm.com/docs](https://fleetdm.com/docs).

105
articles/fleet-4.48.0.md Normal file
View file

@ -0,0 +1,105 @@
# Fleet 4.48.0 | IdP local account creation, VS Code extensions.
![Fleet 4.48.0](../website/assets/images/articles/fleet-4.48.0-1600x900@2x.png)
Fleet 4.48.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.48.0) or continue reading to get the highlights.
For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs.
## Highlights
* IdP local account creation
* Software inventory includes VS Code extensions
### IdP local account creation
Local account creation with an Identity Provider (IdP) now prefills and locks local account creation with details sourced from the IdP. This feature streamlines the initial setup experience during the macOS out-of-the-box setup process by ensuring that the full name and account name fields are automatically populated with the user's IdP username. Additionally, it enforces password policies to be applied before the account creation is finalized, adding an extra layer of security and compliance to the enrollment process. This update simplifies the onboarding experience for end-users, allowing them to log into their Mac with their IdP credentials seamlessly. It reflects Fleet's commitment to enhancing device management efficiency and security. Fleet empowers IT administrators to ensure a consistent and secure user experience across their macOS fleet by focusing on automating and securing the setup process.
### VS Code extensions
In addressing the need for comprehensive software inventory management, Fleet has expanded its inventory capabilities to include Visual Studio Code (VS Code) extensions. This addition enables IT and security teams to gain visibility into the VS Code extensions installed across their device fleet, offering a clearer view of their environments' development tools and resources. By surfacing VS Code extensions in the software inventory, Fleet allows for a more detailed assessment of the software landscape, facilitating better compliance, security assessments, and software management practices. This feature aligns with Fleet's commitment to providing detailed and actionable insights into the software ecosystem, supporting informed decision-making and proactive management of digital assets.
## Changes
### Endpoint operations
- Added integration with Google Calendar.
* Fleet admins can enable Google Calendar integration by using a Google service account with domain-wide delegation.
* Calendar integration is enabled at the team level for specific team policies.
* If the policy is failing, a calendar event will be put on the host user's calendar for the 3rd Tuesday of the month.
* During the event, Fleet will fire a webhook. IT admins should use this webhook to trigger a script or MDM command that will remediate the issue.
- Reduced the number of 'Deadlock found' errors seen by the server when multiple hosts share the same UUID.
- Removed outdated tooltips from UI.
- Added hover states to clickable elements.
- Added cross-platform check for duplicate MDM profile names in batch set MDM profiles API.
### Device management (MDM)
- Added Windows MDM support to the `osquery-perf` host-simulation command.
- Added a missing database index to the MDM Windows enrollments table that will improve performance at scale.
- Migrate MDM-related endpoints to new paths, deprecating (but still supporting indefinitely) the old endpoints.
- Adds API functionality for creating DDM declarations, both individually and as a batch.
- Added DDM activities to the fleet UI.
- Added the `enable_release_device_manually` configuration setting for a team and no team. **Note** that the macOS automatic enrollment profile cannot set the `await_device_configured` option anymore, this setting is controlled by Fleet via the new `enable_release_device_manually` option.
- Automatically release a macOS DEP-enrolled device after enrollment commands and profiles have been delivered, unless `enable_release_device_manually` is set to `true`.
### Vulnerability management
- Added Visual Studio extensions to Fleet's software inventory.
### Bug fixes
- Fixed a bug where valid MDM enrollments would show up as unmanaged (EnrollmentState 3).
- Fixed flash message from closing when a modal closes.
- Fixed a bug where OS version information would not get detected on Windows Server 2019.
- Fixed issue where getting host details failed when attempting to read the host's BitLocker status from the datastore.
- Fixed false negative vulnerabilities on macOS Homebrew python packages.
- Fixed styling of live query disabled warning.
- Fixed issue where Windows MDM profile processing was skipping `<Add>` commands.
- Fixed UI's ability to bulk delete hosts when "All teams" is selected.
- Fixed error state rendering on the global Host status expiry settings page, fix error state alignment for tooltip-wrapper field labels across organization settings.
- Fixed `GET fleet/os_versions` and `GET fleet/os_versions/[id]` so team users no longer have access to os versions on hosts from other teams.
- `fleetctl gitops` now batch processes queries and policies.
- Fixed UI bug to render the query platform correctly for queries imported from the standard query library.
- Fixed issue where Microsoft Edge was not reporting vulnerabilities.
- Fixed a bug where all Windows MDM enrollments were detected as automatic.
- Fixed a bug where `null` or excluded `smtp_settings` caused a UI 500.
- Fixed query reports so they reset when there is a change to the selected platform or selected minimum osquery version.
- Fixed live query sort of SQL result sort for both string and numerical columns.
## Fleet 4.47.3 (Mar 26, 2024)
### Bug fixes
* Fixed a bug where valid Windows MDM enrollments would show up as unmanaged (EnrollmentState 3).
## Fleet 4.47.2 (Mar 22, 2024)
### Bug fixes
* Fixed false negative vulnerabilities on macOS Homebrew Python packages.
* Fixed policies to check "disable guest user".
* Resolved the issue where Microsoft Edge was not reporting vulnerabilities.
## Fleet 4.47.1 (Mar 18, 2024)
### Bug fixes
* Removed outdated tooltips from UI.
* Fixed an issue with Windows MDM profile processing where `<Add>` commands were being skipped.
* Team users no longer have access to OS versions on hosts from other teams for GET fleet/os_versions and GET fleet/os_versions/[id].
* Reduced the number of 'Deadlock found' errors seen by the server when multiple hosts share the same UUID.
## Ready to upgrade?
Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs for instructions on updating to Fleet 4.48.0.
<meta name="category" value="releases">
<meta name="authorFullName" value="JD Strong">
<meta name="authorGitHubUsername" value="spokanemac">
<meta name="publishedOn" value="2024-04-03">
<meta name="articleTitle" value="Fleet 4.48.0 | IdP local account creation, VS Code extensions.">
<meta name="articleImageUrl" value="../website/assets/images/articles/fleet-4.48.0-1600x900@2x.png">

View file

@ -0,0 +1,62 @@
# Sysadmin diaries: passcode profiles
![Sysadmin diaries: passcode profiles](../website/assets/images/articles/sysadmin-diaries-1600x900@2x.png)
Passcode MDM profiles do not work the way we might think they should. We recently onboarded a new Fleetie, which is always an opportunity to _eat our own dog food_ when using Fleet for device management.
This is the first in a series of things we encounter when managing our own devices (aka hosts). Fleet is an open-source and open-core company. Our [handbook](https://fleetdm.com/handbook) is public for everyone to view (and improve!). The [configuration policies](https://github.com/fleetdm/fleet-gitops) we apply to our devices reside in our public git repo. Today, we are looking at Fleet's password policy for macOS devices, which utilizes the [passcode policy payload](https://developer.apple.com/documentation/devicemanagement/passcode).
The user set up their new computer, created an account, and used a passcode that does not meet Fleet's passcode policy, namely a length of 10 characters. Sometime after that, the MDM profiles were delivered to the host. The expected behavior would be that the user would be prompted to enter a compliant passcode upon the next login.
What happens instead with this policy is that after login, the user is prompted with a "Password Policy Updated" notification.
![Password policy updated notification](../website/assets/images/articles/sysadmin-diaries-password-policy-updated-689x140@2x.png
"Password policy updated")
This notification comes with the ability just to ignore it: Change Later or just dismiss the dialog.
![Password policy options > change now… or change
later](../website/assets/images/articles/sysadmin-diaries-change-later-231x160@2x.png "Password
policy > change later")
A quick search of the [Mac Admins Slack](https://www.macadmins.org/) confirmed my suspicions. The non-compliant passcode will remain indefinitely, and the profile requirements are only enforced on the next reset or new account creation.
### Why did this happen, and how do we solve it?
We discovered that the policy was not applied because Fleet needed to lock out account creation before all the policies had been successfully applied to the host. We have corrected this in [Fleet 4.48.0](https://fleetdm.com/releases/fleet-4.48.0), but how do we resolve this issue with an existing enrolled device and a change in the organization's password policy?
Do we add the `changeAtNextAuth` key? A read of Apple's documentation means every user with this policy must reset their password on the next authentication. That could be highly disruptive. And, if the policy is redeployed for any reason, could institute a password reset to every host in that team.
<blockquote purpose="large-quote">
<code>changeAtNextAuth</code> (boolean)
If true, the system causes a password reset to occur the next time the user tries to authenticate. If this key is set in a device profile, the setting takes effect for all users, and admin authentications may fail until the admin user password is also reset. Available in macOS 10.13 and later.
</blockquote>
Another solution is to use Fleet's remote script execution capability to trigger a one-off password reset on the host.
```
pwpolicy -u "501" -setpolicy "newPasswordRequired=1"
```
This will require the user to reset their password upon the next login to the host. This is likely the best solution in this situation, as it can be applied on an individual host basis.
In wrapping up this exploration into the intricacies of passcode profiles and their challenges, Fleet's open-source nature allows us to share these experiences and collectively seek solutions that enhance our understanding and implementation of device management policies. Let's continue the conversation. [Join us on Slack](https://fleetdm.com/support) and let us know how you might solve this issue and what device management problems you want to solve.
<meta name="articleTitle" value="Sysadmin diaries: passcode profiles">
<meta name="authorFullName" value="JD Strong">
<meta name="authorGitHubUsername" value="spokanemac">
<meta name="category" value="guides">
<meta name="publishedOn" value="2024-04-01">
<meta name="articleImageUrl" value="../website/assets/images/articles/sysadmin-diaries-1600x900@2x.png">
<meta name="description" value="In this sysadmin diary, we explore a missapplied passcode policy.">

View file

@ -0,0 +1 @@
* Made block_id mismatch errors more informative as 400s instead of 500s.

View file

@ -0,0 +1 @@
Fleet UI: Fix edge cases of team ID being lost in various flows

View file

@ -0,0 +1 @@
- UI: Surface fleet desktop and orbit version to the host details page

View file

@ -0,0 +1 @@
Calendar webhook will retry if it receives response 429 Too Many Requests. Webhook request will retry for 30 minutes with a 1 minute max delay between retries.

View file

@ -0,0 +1 @@
Fixing potential server panic when events are created with calendar integration, but then global calendar integration is disabled.

View file

@ -0,0 +1 @@
- Fix a bug where values were not being rendered in host-specific query reports.

View file

@ -8,7 +8,7 @@ version: v6.0.2
home: https://github.com/fleetdm/fleet
sources:
- https://github.com/fleetdm/fleet.git
appVersion: v4.47.3
appVersion: v4.48.0
dependencies:
- name: mysql
condition: mysql.enabled

View file

@ -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.47.3 # Version of Fleet to deploy
imageTag: v4.48.0 # Version of Fleet to deploy
podAnnotations: {} # Additional annotations to add to the Fleet pod
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
resources:

View file

@ -28,6 +28,7 @@ import (
configpkg "github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
licensectx "github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/cron"
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysqlredis"
@ -773,9 +774,7 @@ the way that the Fleet server works.
if license.IsPremium() {
if err := cronSchedules.StartCronSchedule(
func() (fleet.CronSchedule, error) {
return newCalendarSchedule(
ctx, instanceID, ds, logger,
)
return cron.NewCalendarSchedule(ctx, instanceID, ds, 5*time.Minute, logger)
},
); err != nil {
initFatal(err, "failed to register calendar schedule")

View file

@ -1457,9 +1457,9 @@ func TestGetQuery(t *testing.T) {
Platform: "linux",
Logging: "differential",
}, nil
} else {
return nil, &notFoundError{}
}
return nil, &notFoundError{}
}
expectedYaml := `---

View file

@ -879,9 +879,9 @@ spec:
apiVersion: v1
kind: policy
spec:
name: No 1Password emergency kit stored on desktop or in downloads (macOS)
query: SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%%/Desktop/%%' OR path LIKE '/Users/%%/Documents/%%' OR path LIKE '/Users/%%/Downloads/%%' OR path LIKE '/Users/Shared'));
description: "Looks for PDF files with file names typically used by 1Password for emergency recovery kits."
name: No 1Password emergency kit stored in desktop, documents, or downloads folders (macOS)
query: SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%/Desktop/%' OR path LIKE '/Users/%/Documents/%' OR path LIKE '/Users/%/Downloads/%' OR path LIKE '/Users/Shared/%'));
description: "Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders."
resolution: "Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location."
platform: darwin
tags: compliance, built-in

View file

@ -906,7 +906,8 @@ None.
"macos_setup": {
"bootstrap_package": "",
"enable_end_user_authentication": false,
"macos_setup_assistant": "path/to/config.json"
"macos_setup_assistant": "path/to/config.json",
"enable_release_device_manually": true
}
},
"agent_options": {
@ -1509,7 +1510,7 @@ Delete all of a team's existing enroll secrets
| email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. |
| name | string | body | **Required.** The name of the invited user. |
| sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. |
| teams | list | body | _Available in Fleet Premium_ A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
#### Example
@ -1709,7 +1710,7 @@ Verify the specified invite.
| email | string | body | The email of the invited user. Updates on the email won't resend the invitation. |
| name | string | body | The name of the invited user. |
| sso_enabled | boolean | body | Whether or not SSO will be enabled for the invited user. |
| teams | list | body | _Available in Fleet Premium_ A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
#### Example
@ -1896,10 +1897,10 @@ the `software` table.
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| disable_failing_policies| boolean | query | If `true`, hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. |
| bootstrap_package | string | query | _Available in Fleet Premium_. Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. |
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| populate_software | boolean | query | If `true`, the response will include a list of installed software for each host, including vulnerability data. |
@ -2115,7 +2116,7 @@ Response payload with the `munki_issue_id` filter provided:
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. |
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
| query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). |
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. |
| policy_id | integer | query | The ID of the policy to filter hosts by. |
| policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. |
| software_version_id | integer | query | The ID of the software version to filter hosts by. |
@ -2130,9 +2131,9 @@ Response payload with the `munki_issue_id` filter provided:
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| bootstrap_package | string | query | _Available in Fleet Premium_. Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
@ -3289,7 +3290,7 @@ Retrieves MDM enrollment summary. Windows servers are excluded from the aggregat
| Name | Type | In | Description |
| -------- | ------- | ----- | -------------------------------------------------------------------------------- |
| team_id | integer | query | _Available in Fleet Premium_ Filter by team |
| team_id | integer | query | _Available in Fleet Premium_. Filter by team |
| platform | string | query | Filter by platform ("windows" or "darwin") |
A `team_id` of `0` returns the statistics for hosts that are not part of any team. A `null` or missing `team_id` returns statistics for all hosts regardless of the team.
@ -3399,7 +3400,7 @@ Retrieves aggregated host's MDM enrollment status and Munki versions.
| Name | Type | In | Description |
| ------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------- |
| team_id | integer | query | _Available in Fleet Premium_ Filters the aggregate host information to only include hosts in the specified team. | |
| team_id | integer | query | _Available in Fleet Premium_. Filters the aggregate host information to only include hosts in the specified team. | |
A `team_id` of `0` returns the statistics for hosts that are not part of any team. A `null` or missing `team_id` returns statistics for all hosts regardless of the team.
@ -3680,7 +3681,7 @@ requested by a web browser.
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. |
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
| query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). |
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. |
| policy_id | integer | query | The ID of the policy to filter hosts by. |
| policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** |
| software_version_id | integer | query | The ID of the software version to filter hosts by. |
@ -3694,9 +3695,9 @@ requested by a web browser.
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `team_id`. |
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| bootstrap_package | string | query | _Available in Fleet Premium_. Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| disable_failing_policies | boolean | query | If `true`, hosts will return failing policies as 0 (returned as the `issues` column) regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results.
@ -4424,15 +4425,15 @@ Returns a list of the hosts that belong to the specified label.
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. |
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
| query | string | query | Search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. |
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts in the specified team. |
| disable_failing_policies | boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
| mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). |
| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). |
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| bootstrap_package | string | query | _Available in Fleet Premium_. Filters the hosts by the status of the MDM bootstrap package on the host. Valid options are 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Valid options are 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
@ -4589,8 +4590,8 @@ Add a configuration profile to enforce custom settings on macOS and Windows host
| Name | Type | In | Description |
| ------------------------- | -------- | ---- | ------------------------------------------------------------------------------------------------------------- |
| profile | file | form | **Required.** The .mobileconfig (macOS) or XML (Windows) file containing the profile. |
| team_id | string | form | _Available in Fleet Premium_ The team ID for the profile. If specified, the profile is applied to only hosts that are assigned to the specified team. If not specified, the profile is applied to only to hosts that are not assigned to any team. |
| labels | array | form | _Available in Fleet Premium_ An array of labels to filter hosts in a team (or no team) that should get a profile. |
| team_id | string | form | _Available in Fleet Premium_. The team ID for the profile. If specified, the profile is applied to only hosts that are assigned to the specified team. If not specified, the profile is applied to only to hosts that are not assigned to any team. |
| labels | array | form | _Available in Fleet Premium_. An array of labels to filter hosts in a team (or no team) that should get a profile. |
#### Example
@ -4700,7 +4701,7 @@ results (i.e., only profiles that are associated with "No team" are listed).
| Name | Type | In | Description |
| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- |
| team_id | string | query | _Available in Fleet Premium_ The team id to filter profiles. |
| team_id | string | query | _Available in Fleet Premium_. The team id to filter profiles. |
| page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. |
@ -4900,7 +4901,7 @@ The summary can optionally be filtered by team ID.
| Name | Type | In | Description |
| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- |
| team_id | string | query | _Available in Fleet Premium_ The team ID to filter the summary. |
| team_id | string | query | _Available in Fleet Premium_. The team ID to filter the summary. |
#### Example
@ -4938,7 +4939,7 @@ optionally be filtered by `team_id`. If no `team_id` is specified, team profiles
| Name | Type | In | Description |
| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- |
| team_id | string | query | _Available in Fleet Premium_ The team ID to filter profiles. |
| team_id | string | query | _Available in Fleet Premium_. The team ID to filter profiles. |
#### Example
@ -5874,7 +5875,7 @@ Where `query_id` references an existing `query`.
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | _Available in Fleet Premium_ Mark policy as critical/high impact. |
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
#### Example
@ -6137,7 +6138,7 @@ The semantics for creating a team policy are the same as for global policies, se
| resolution | string | body | The resolution steps for the policy. |
| 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. |
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
Either `query` or `query_id` must be provided.
@ -6233,7 +6234,7 @@ Either `query` or `query_id` must be provided.
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | _Available in Fleet Premium_ Mark policy as critical/high impact. |
| critical | boolean | body | _Available in Fleet Premium_. Mark policy as critical/high impact. |
#### Example
@ -7732,7 +7733,7 @@ Get a list of all software.
| order_key | string | query | What to order results by. Allowed fields are `name` and `hosts_count`. Default is `hosts_count` (descending). |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `title` and `cve`. |
| team_id | integer | query | _Available in Fleet Premium_ Filters the software to only include the software installed on the hosts that are assigned to the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. |
| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
#### Example
@ -7841,7 +7842,7 @@ Get a list of all software versions.
| order_key | string | query | What to order results by. Allowed fields are `name`, `hosts_count`, `cve_published`, `cvss_score`, `epss_probability` and `cisa_known_exploit`. Default is `hosts_count` (descending). |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
| query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. |
| team_id | integer | query | _Available in Fleet Premium_ Filters the software to only include the software installed on the hosts that are assigned to the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. |
| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
#### Example
@ -8024,7 +8025,7 @@ Retrieves a list of all CVEs affecting software and/or OS versions.
| Name | Type | In | Description |
| --- | --- | --- | --- |
| team_id | integer | query | _Available in Fleet Premium_ Filters only include vulnerabilities affecting the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters only include vulnerabilities affecting the specified team. |
| page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. |
| order_key | string | query | What to order results by. Allowed fields are: `cve`, `cvss_score`, `epss_probability`, `cve_published`, `created_at`, and `host_count`. Default is `created_at` (descending). |
@ -9027,7 +9028,7 @@ Returns a list of all enabled users
| page | integer | query | Page number of the results to fetch. |
| query | string | query | Search query keywords. Searchable fields include `name` and `email`. |
| per_page | integer | query | Results per page. |
| team_id | integer | query | _Available in Fleet Premium_ Filters the users to only include users in the specified team. |
| team_id | integer | query | _Available in Fleet Premium_. Filters the users to only include users in the specified team. |
#### Example
@ -9101,7 +9102,7 @@ Creates a user account after an invited user provides registration information a
| password | string | body | The password chosen by the user (if not SSO user). |
| password_confirmation | string | body | Confirmation of the password chosen by the user. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `global_role` is specified, `teams` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
| teams | array | body | _Available in Fleet Premium_. The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
#### Example
@ -9219,7 +9220,7 @@ By default, the user will be forced to reset its password upon first login.
| api_only | boolean | body | User is an "API-only" user (cannot use web UI) if true. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `global_role` is specified, `teams` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
| admin_forced_password_reset | boolean | body | Sets whether the user will be forced to reset its password upon first login (default=true) |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
| teams | array | body | _Available in Fleet Premium_. The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). In Fleet 4.30.0 and 4.31.0, the `observer_plus` and `gitops` roles were introduced respectively. If `teams` is specified, `global_role` cannot be specified. For more information, see [manage access](https://fleetdm.com/docs/using-fleet/manage-access). |
#### Example
@ -9372,7 +9373,7 @@ Returns all information about a specific user.
| password | string | body | The user's current password, required to change the user's own email or password (not required for an admin to modify another user). |
| new_password| string | body | The user's new password. |
| global_role | string | body | The role assigned to the user. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `global_role` is specified, `teams` cannot be specified. |
| teams | array | body | _Available in Fleet Premium_ The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `teams` is specified, `global_role` cannot be specified. |
| teams | array | body | _Available in Fleet Premium_. The teams and respective roles assigned to the user. Should contain an array of objects in which each object includes the team's `id` and the user's `role` on each team. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). If `teams` is specified, `global_role` cannot be specified. |
#### Example

View file

@ -340,7 +340,7 @@ so:
```
If the provided path doesn't contain all 3 binaries, the command will fail.
>**Note:** Creating a fleetd agent for Windows (.msi) on macOS also requires Wine. To install Wine see the script [here](https://github.com/fleetdm/fleet/blob/fleet-v4.44.0/scripts/macos-install-wine.sh).
>**Note:** Creating a fleetd agent for Windows (.msi) on macOS also requires Wine. To install Wine see the script [here](https://fleetdm.com/install-wine).
### Experimental features

View file

@ -764,8 +764,8 @@ func (p *passphraseHandler) checkPassphrase(store tuf.LocalStore, role string) e
continue
} else if len(keys) == 0 {
return fmt.Errorf("%s key not found", role)
} else {
return nil
}
return nil
}
}

View file

@ -93,3 +93,14 @@ func ClearMockEvents() {
defer mu.Unlock()
mockEvents = make(map[string]*calendar.Event)
}
func SetMockEventsToNow() {
mu.Lock()
defer mu.Unlock()
now := time.Now()
for _, mockEvent := range mockEvents {
mockEvent.Start = &calendar.EventDateTime{DateTime: now.Format(time.RFC3339)}
mockEvent.End = &calendar.EventDateTime{DateTime: now.Add(30 * time.Minute).Format(time.RFC3339)}
}
}

View file

@ -11,7 +11,8 @@ const (
// These are just example keys generated locally with openssl. They are intentionally published
// and should never be used in production.
exampleKeyPassphrase = "password"
exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
// #nosec G101
exampleKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,5F4D77F29A9E2675

View file

@ -1,4 +1,4 @@
import { IHost } from "interfaces/host";
import { IHost, IHostResponse } from "interfaces/host";
import { IHostMdmProfile } from "interfaces/mdm";
const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = {
@ -34,6 +34,8 @@ const DEFAULT_HOST_MOCK: IHost = {
uuid: "09b244f8-0000-0000-b5cc-791a15f11073",
platform: "ubuntu",
osquery_version: "4.9.0",
orbit_version: "1.22.0",
fleet_desktop_version: "1.22.0",
os_version: "Ubuntu 18.4.0",
build: "",
platform_like: "debian",
@ -101,4 +103,6 @@ const createMockHost = (overrides?: Partial<IHost>): IHost => {
return { ...DEFAULT_HOST_MOCK, ...overrides };
};
export const createMockHostResponse = { host: createMockHost() };
export default createMockHost;

View file

@ -138,6 +138,24 @@ We tend to use explicit assignment of prop values, instead of object spread synt
```
<ExampleComponent prop1={pop1Val} prop2={prop2Val} prop3={prop3Val} />
```
### Naming handlers
When defining component props for handlers, we prefer naming with a more general `onAction`. When
naming the handler passed into that prop or used in the same component it's defined, we prefer
either the same `onAction` or, if useful, a more specific `onMoreSpecifiedAction`. E.g.:
```tsx
<BigSecretComponent
onSubmit={onSubmit}
/>
```
or
```tsx
<BigSecretComponent
onSubmit={onUpdateBigSecret}
/>
```
### Page component pattern
When creating a **top level page** (e.g. dashboard page, hosts page, policies page)

View file

@ -29,6 +29,8 @@ export default PropTypes.shape({
uuid: PropTypes.string,
platform: PropTypes.string,
osquery_version: PropTypes.string,
orbit_version: PropTypes.string,
fleet_desktop_version: PropTypes.string,
os_version: PropTypes.string,
build: PropTypes.string,
platform_like: PropTypes.string,
@ -267,6 +269,8 @@ export interface IHost {
uuid: string;
platform: string;
osquery_version: string;
orbit_version?: string;
fleet_desktop_version?: string;
os_version: string;
build: string;
platform_like: string; // TODO: replace with more specific union type

View file

@ -137,6 +137,9 @@ const ManageHostsPage = ({
isFreeTier,
isSandboxMode,
setFilteredHostsPath,
setFilteredPoliciesPath,
setFilteredQueriesPath,
setFilteredSoftwarePath,
} = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
@ -886,6 +889,11 @@ const ManageHostsPage = ({
// tableQueryData)
handleTeamChange(teamId);
handleResetPageIndex();
// Must clear other page paths or the team might accidentally switch
// When navigating from host details
setFilteredSoftwarePath("");
setFilteredQueriesPath("");
setFilteredPoliciesPath("");
},
[handleTeamChange]
);

View file

@ -52,7 +52,6 @@ import {
HOST_ABOUT_DATA,
HOST_OSQUERY_DATA,
} from "utilities/constants";
import { createMockHostMdmProfile } from "__mocks__/hostMock";
import Spinner from "components/Spinner";
import TabsWrapper from "components/TabsWrapper";
@ -147,6 +146,7 @@ const HostDetailsPage = ({
isSandboxMode,
isOnlyObserver,
filteredHostsPath,
currentTeam,
} = useContext(AppContext);
const { setSelectedQueryTargetsByType } = useContext(QueryContext);
const { renderFlash } = useContext(NotificationContext);
@ -567,7 +567,8 @@ const HostDetailsPage = ({
const onQueryHostCustom = () => {
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
PATHS.NEW_QUERY() + TAGGED_TEMPLATES.queryByHostRoute(host?.id)
PATHS.NEW_QUERY() +
TAGGED_TEMPLATES.queryByHostRoute(host?.id, currentTeam?.id)
);
};
@ -575,7 +576,7 @@ const HostDetailsPage = ({
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
router.push(
PATHS.EDIT_QUERY(selectedQuery.id) +
TAGGED_TEMPLATES.queryByHostRoute(host?.id)
TAGGED_TEMPLATES.queryByHostRoute(host?.id, currentTeam?.id)
);
};

View file

@ -141,8 +141,8 @@ const RunScriptModal = ({
{!isLoading && isError && <DataError />}
{!isLoading && !isError && (!tableData || tableData.length === 0) && (
<EmptyTable
header="No scripts are available for this host"
info="Expecting to see scripts? Try selecting “Refetch” to ask the host to report new vitals."
header="No scripts available for this host"
info="Expecting to see scripts? Close this modal and try again."
/>
)}
{!isLoading && !isError && tableData && tableData.length > 0 && (

View file

@ -0,0 +1,112 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import HQRTable, { IHQRTable } from "./HQRTable";
describe("HQRTable component", () => {
it("Renders results normally when they are present", () => {
const testData: IHQRTable[] = [
{
queryName: "testQuery0",
queryDescription: "testDescription0",
hostName: "testHost0",
rows: [
{
build_distro: "10.14",
build_platform: "darwin",
config_hash: "111111111111111111111111",
config_valid: "1",
extensions: "active",
instance_id: "2f7a7b8e-8f35-4fa8-9e8b-1111111111111111",
pid: "575",
platform_mask: "21",
start_time: "1711512878",
uuid: "gggggg-4568-5BD9-9F1C-6D2E701FAB5C",
version: "5.11.0",
watcher: "574",
},
],
reportClipped: false,
lastFetched: "2021-09-01T00:00:00Z",
onShowQuery: jest.fn(),
isLoading: false,
},
];
testData.forEach((tableProps) => {
render(<HQRTable {...tableProps} />);
expect(screen.getByText("1 result")).toBeInTheDocument();
expect(screen.getByText("Last fetched")).toBeInTheDocument();
tableProps.rows.forEach((row) => {
Object.entries(row).forEach(([col, val]) => {
expect(screen.getByText(col)).toBeInTheDocument();
expect(screen.getByText(val)).toBeInTheDocument();
});
});
});
});
it("Renders the 'collecting results' empty state when results have never been collected.", () => {
const testData: IHQRTable[] = [
{
queryName: "testQuery0",
queryDescription: "testDescription0",
hostName: "testHost0",
rows: [],
reportClipped: false,
lastFetched: null,
onShowQuery: jest.fn(),
isLoading: false,
},
];
testData.forEach((tableProps) => {
render(<HQRTable {...tableProps} />);
expect(screen.queryByText("Last fetched")).toBeNull();
expect(screen.getByText("Collecting results...")).toBeInTheDocument();
});
});
it("Renders the 'report clipped' empty state when reporting for this query has been paused and there are no existing results.", () => {
const testData: IHQRTable[] = [
{
queryName: "testQuery0",
queryDescription: "testDescription0",
hostName: "testHost0",
rows: [],
reportClipped: true,
lastFetched: "2021-09-01T00:00:00Z",
onShowQuery: jest.fn(),
isLoading: false,
},
];
testData.forEach((tableProps) => {
render(<HQRTable {...tableProps} />);
expect(screen.queryByText("Last fetched")).toBeNull();
expect(screen.getByText("Report clipped")).toBeInTheDocument();
});
});
it("Renders the 'nothing to report' empty state when the query has run and there are no results.", () => {
const testData: IHQRTable[] = [
{
queryName: "testQuery0",
queryDescription: "testDescription0",
hostName: "testHost0",
rows: [],
reportClipped: false,
lastFetched: "2021-09-01T00:00:00Z",
onShowQuery: jest.fn(),
isLoading: false,
},
];
testData.forEach((tableProps) => {
render(<HQRTable {...tableProps} />);
expect(screen.queryByText("Last fetched")).toBeNull();
expect(screen.getByText("Nothing to report")).toBeInTheDocument();
});
});
});

View file

@ -15,7 +15,7 @@ import generateColumnConfigs from "./HQRTableConfig";
const baseClass = "hqr-table";
interface IHQRTable {
export interface IHQRTable {
queryName?: string;
queryDescription?: string;
hostName?: string;

View file

@ -1,10 +1,6 @@
import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import {
IHeaderProps,
IStringCellProps,
IWebSocketData,
} from "interfaces/datatable_config";
import { IHeaderProps, IWebSocketData } from "interfaces/datatable_config";
import React from "react";
import { CellProps, Column } from "react-table";
@ -47,7 +43,7 @@ const generateColumnConfigs = (rows: IWebSocketData[]): IHQRTTableColumn[] =>
const val = cellProps?.cell?.value;
return !!val?.length && val.length > 300
? internallyTruncateText(val)
: <>val</> ?? null;
: <>{val}</> ?? null;
},
Filter: DefaultColumnFilter, // Component hides filter for last_fetched
filterType: "text",

View file

@ -317,6 +317,39 @@ const HostSummary = ({
);
};
const renderAgentSummary = () => {
if (platform === "chrome") {
return <DataSet title="Agent" value={summaryData.osquery_version} />;
}
if (summaryData.orbit_version) {
return (
<DataSet
title="Agent"
value={
<TooltipWrapper
tipContent={
<>
osquery: {summaryData.osquery_version}
<br />
Orbit: {summaryData.orbit_version}
{summaryData.fleet_desktop_version && (
<>
<br />
Fleet Desktop: {summaryData.fleet_desktop_version}
</>
)}
</>
}
>
{summaryData.orbit_version}
</TooltipWrapper>
}
/>
);
}
return <DataSet title="Osquery" value={summaryData.osquery_version} />;
};
const renderSummary = () => {
// for windows hosts we have to manually add a profile for disk encryption
// as this is not currently included in the `profiles` value from the API
@ -401,7 +434,8 @@ const HostSummary = ({
/>
<DataSet title="Processor type" value={summaryData.cpu_type} />
<DataSet title="Operating system" value={summaryData.os_version} />
<DataSet title="Osquery" value={summaryData.osquery_version} />
{renderAgentSummary()}
</Card>
);
};

View file

@ -22,6 +22,8 @@ import {
isTeamObserver,
} from "utilities/permissions/permissions";
import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants";
import { buildQueryStringFromParams } from "utilities/url";
import useTeamIdParam from "hooks/useTeamIdParam";
import Spinner from "components/Spinner/Spinner";
import Button from "components/buttons/Button";
@ -65,9 +67,13 @@ const QueryDetailsPage = ({
router.push(PATHS.MANAGE_QUERIES);
}
const queryParams = location.query;
const teamId = location.query.team_id
? parseInt(location.query.team_id, 10)
: undefined;
const { currentTeamId } = useTeamIdParam({
location,
router,
includeAllTeams: true,
includeNoTeam: false,
});
// Functions to avoid race conditions
const serverSortBy: ISortOption[] = (() => {
@ -203,7 +209,12 @@ const QueryDetailsPage = ({
// Function instead of constant eliminates race condition with filteredQueriesPath
const backToQueriesPath = () => {
return filteredQueriesPath || PATHS.MANAGE_QUERIES;
return (
filteredQueriesPath ||
`${PATHS.MANAGE_QUERIES}?${buildQueryStringFromParams({
team_id: currentTeamId,
})}`
);
};
return (
@ -233,7 +244,8 @@ const QueryDetailsPage = ({
{canEditQuery && (
<Button
onClick={() => {
queryId && router.push(PATHS.EDIT_QUERY(queryId, teamId));
queryId &&
router.push(PATHS.EDIT_QUERY(queryId, currentTeamId));
}}
className={`${baseClass}__manage-automations button`}
variant="brand"
@ -332,7 +344,7 @@ const QueryDetailsPage = ({
// Exclude below message for global and team observers/observer+s
!(
(currentUser && isGlobalObserver(currentUser)) ||
isTeamObserver(currentUser, teamId ?? null)
isTeamObserver(currentUser, currentTeamId ?? null)
) &&
" You can still use query automations to complete this report in your log destination."
}

View file

@ -30,6 +30,7 @@ import { NotificationContext } from "context/notification";
import PATHS from "router/paths";
import debounce from "utilities/debounce";
import deepDifference from "utilities/deep_difference";
import { buildQueryStringFromParams } from "utilities/url";
import EditQueryForm from "./components/EditQueryForm";
@ -38,7 +39,7 @@ interface IEditQueryPageProps {
params: Params;
location: {
pathname: string;
query: { host_ids: string; team_id?: string };
query: { host_id: string; team_id?: string };
search: string;
};
}
@ -51,9 +52,11 @@ const EditQueryPage = ({
location,
}: IEditQueryPageProps): JSX.Element => {
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
const {
currentTeamName: teamNameForQuery,
teamIdForApi: apiTeamIdForQuery,
currentTeamId,
} = useTeamIdParam({
location,
router,
@ -70,6 +73,7 @@ const EditQueryPage = ({
isObserverPlus,
isAnyTeamObserverPlus,
config,
filteredQueriesPath,
} = useContext(AppContext);
const {
editingExistingQuery,
@ -319,7 +323,15 @@ const EditQueryPage = ({
// Function instead of constant eliminates race condition
const backToQueriesPath = () => {
return queryId ? PATHS.QUERY_DETAILS(queryId) : PATHS.MANAGE_QUERIES;
const manageQueryPage =
filteredQueriesPath ||
`${PATHS.MANAGE_QUERIES}?${buildQueryStringFromParams({
team_id: currentTeamId,
})}`;
return queryId
? PATHS.QUERY_DETAILS(queryId, currentTeamId)
: manageQueryPage;
};
const showSidebar =
@ -348,6 +360,7 @@ const EditQueryPage = ({
storedQuery={storedQuery}
queryIdForEdit={queryId}
apiTeamIdForQuery={apiTeamIdForQuery}
currentTeamId={currentTeamId}
teamNameForQuery={teamNameForQuery}
isStoredQueryLoading={isStoredQueryLoading}
showOpenSchemaActionText={showOpenSchemaActionText}
@ -356,7 +369,7 @@ const EditQueryPage = ({
backendValidators={backendValidators}
isQuerySaving={isQuerySaving}
isQueryUpdating={isQueryUpdating}
hostId={parseInt(location.query.host_ids as string, 10)}
hostId={parseInt(location.query.host_id as string, 10)}
queryReportsDisabled={
appConfig?.server_settings.query_reports_disabled
}

View file

@ -63,6 +63,7 @@ interface IEditQueryFormProps {
router: InjectedRouter;
queryIdForEdit: number | null;
apiTeamIdForQuery?: number;
currentTeamId?: number;
teamNameForQuery?: string;
showOpenSchemaActionText: boolean;
storedQuery: ISchedulableQuery | undefined;
@ -97,6 +98,7 @@ const EditQueryForm = ({
router,
queryIdForEdit,
apiTeamIdForQuery,
currentTeamId,
teamNameForQuery,
showOpenSchemaActionText,
storedQuery,
@ -601,7 +603,7 @@ const EditQueryForm = ({
onClick={() => {
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
TAGGED_TEMPLATES.queryByHostRoute(hostId, apiTeamIdForQuery)
);
}}
disabled={disabledLiveQuery}
@ -680,7 +682,6 @@ const EditQueryForm = ({
const disableSaveFormErrors =
(lastEditedQueryName === "" && !!lastEditedQueryId) || !!size(errors);
console.log("lastEditedQueryPlatforms", lastEditedQueryPlatforms);
return (
<>
<form className={`${baseClass}`} autoComplete="off">
@ -846,7 +847,7 @@ const EditQueryForm = ({
setEditingExistingQuery(true); // Persists edited query data through live query flow
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
TAGGED_TEMPLATES.queryByHostRoute(hostId, currentTeamId)
);
}}
disabled={disabledLiveQuery}

View file

@ -27,7 +27,7 @@ interface IRunQueryPageProps {
params: Params;
location: {
pathname: string;
query: { host_ids: string; team_id?: string };
query: { host_id: string; team_id?: string };
search: string;
};
}
@ -109,9 +109,9 @@ const RunQueryPage = ({
useQuery<IHostResponse, Error, IHost>(
"hostFromURL",
() =>
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
hostAPI.loadHostDetails(parseInt(location.query.host_id as string, 10)),
{
enabled: !!location.query.host_ids && !queryParamHostsAdded,
enabled: !!location.query.host_id && !queryParamHostsAdded,
select: (data: IHostResponse) => data.host,
onSuccess: (host) => {
setTargetedHosts((prevHosts) =>

View file

@ -341,6 +341,8 @@ export const HOST_SUMMARY_DATA = [
"platform",
"os_version",
"osquery_version",
"orbit_version",
"fleet_desktop_version",
"enroll_secret_name",
"detail_updated_at",
"percent_disk_space_available",

View file

@ -853,8 +853,13 @@ export const getSoftwareBundleTooltipJSX = (bundle: string) => (
);
export const TAGGED_TEMPLATES = {
queryByHostRoute: (hostId: number | undefined | null) => {
return `${hostId ? `?host_ids=${hostId}` : ""}`;
queryByHostRoute: (hostId?: number | null, teamId?: number | null) => {
const queryString = buildQueryStringFromParams({
host_id: hostId || undefined,
team_id: teamId,
});
return queryString && `?${queryString}`;
},
};

10
go.mod
View file

@ -110,16 +110,17 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
go.opentelemetry.io/otel/sdk v1.19.0
golang.org/x/crypto v0.17.0
golang.org/x/crypto v0.22.0
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
golang.org/x/image v0.10.0
golang.org/x/mod v0.12.0
golang.org/x/net v0.19.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.12.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.15.0
golang.org/x/sys v0.19.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.13.0
google.golang.org/api v0.128.0
google.golang.org/grpc v1.58.3
gopkg.in/guregu/null.v3 v3.5.0
gopkg.in/ini.v1 v1.67.0
@ -304,10 +305,9 @@ require (
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
gocloud.dev v0.24.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.128.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a // indirect

16
go.sum
View file

@ -1313,8 +1313,8 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1425,8 +1425,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1576,8 +1576,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1587,8 +1587,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -15,7 +15,7 @@ This handbook page details processes specific to working [with](#what-we-do) and
## Responsibilities
The Business Operations department is directly responsible for the functions of all Finance, People, Legal, IT, and Revenue Operations (RevOps).
The Business Operations department is directly responsible for finance + invoicing, people operations, legal + deal desk, and corporate information technology (IT).
### Run payroll
Many of these processes are automated, but it's vital to check Gusto and Plane manually for accuracy.
@ -24,10 +24,10 @@ Many of these processes are automated, but it's vital to check Gusto and Plane m
| Payroll type | What to use | DRI |
|:-----------------------------|:-----------------------------|:-----------------------------|
| [Commissions and ramp](https://fleetdm.com/handbook/business-operations#run-us-commission-payroll) | "Off-cycle" payroll | Head of Revenue Operations
| [Commissions and ramp](https://fleetdm.com/handbook/business-operations#run-us-commission-payroll) | "Off-cycle" payroll | Head of Business Operations
| Sign-on bonus | "Bonus" payroll | Head of Business Operations
| Performance bonus | "Bonus" payroll | Head of Business Operations
| Accelerations (quarterly) | "Off-cycle" payroll | Head of Revenue Operations
| Accelerations (quarterly) | "Off-cycle" payroll | Head of Business Operations
| [US contractor payroll](https://fleetdm.com/handbook/business-operations#run-us-contractor-payroll) | "Off-cycle" payroll | Head of Business Operations
### Reconcile monthly recurring expenses
@ -109,7 +109,7 @@ For Fleet's US contractors, running payroll is a manual process:
- Sync hours and run contractor payroll.
### Grant role-specific license to a team member (RevOps)
### Grant role-specific license to a team member
Certain new team members, especially in go-to-market (GTM) roles, will need paid access to paid tools like Salesforce and LinkedIn Sales Navigator immediately on their first day with the company. Gong licenses that other departments need may [request them from BizOps](https://fleetdm.com/handbook/business-operations#contact-us) and we will make sure there is no license redundancy in that department. The table below can be used to determine which paid licenses they will need, based on their role:
| Role | Salesforce CRM | Salesforce "Inbox" | LinkedIn _(paid)_ | Gong _(paid)_ | Zoom _(paid)_|
@ -125,6 +125,18 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid
> **Warning:** Do NOT buy LinkedIn Recruiter. AEs and SDRs should use their personal Brex card to purchase the monthly [Core Sales Navigator](https://business.linkedin.com/sales-solutions/compare-plans) plan. Fleet does not use a company wide Sales Navigator account. The goal of Sales Navigator is to access to profile views and data, not InMail. Fleet does not send InMail.
### Communicate the status of customer financial actions
This reporting is performed to update the status of open or upcoming customer actions regarding the financial health of the opportunity. To complete the report:
- Go to this [report folder](https://fleetdm.lightning.force.com/lightning/r/Folder/00lUG000000DstpYAC/view?queryScope=userFolders) in SFDC. The three reports will provide the data used in the report.
- Copy the template below and paste it into the [#g-sales slack channel](https://fleetdm.slack.com/archives/C030A767HQV) and complete all "todos" using the data from Salesforce before sending.
```
Weekly revenue report - [@`todo: CRO` and @`todo: CEO`]
- Number accounts with outstanding balances = `todo`
- Number of customers awaiting invoices = `todo`
- Number of past-due renewals = `todo`
```
### Add a seat to Salesforce
Here are the steps we take to grant appropriate Salesforce licenses to a new hire:
- Go to ["My Account"](https://fleetdm.lightning.force.com/lightning/n/standard-OnlineSalesHome).
@ -170,6 +182,7 @@ When a Fleetie, consultant or advisor requests an update to their personnel deta
- If required, BizOps also makes changes to other core systems (e.g: creating a new email alias in google workspace; updating details in Carta; etc).
- The change is now actioned, notify the team member and close the issue.
> Note: if the Fleetie is US based and has a qualifying life event that impacts benefit coverage, they can [follow the Gusto steps](https://support.gusto.com/article/100895878100000/Change-your-benefits-with-a-qualifying-life-event) to update their coverage elections.
### Change a Fleetie's job title
When BizOps receives notification of a Fleetie's job title changing, follow these steps to ensure accurate recording of the change across our systems.

View file

@ -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. |
| 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 |
<meta name="maintainedBy" value="dherder">
<meta name="title" value="📃 Vendor questionnaires">

View file

@ -214,15 +214,15 @@ If you need to track content from a Slack channel (ie. #g-sales), you can automa
**Fleet Free:**
| Impact Level | Definition | Preferred Contact | Response Time |
| Impact level | Definition | Preferred contact | Response time |
|:---|:---|:---|:---|
| All Inquiries | Any request regardless of impact level or severity | Osquery #fleet Slack channel | No guaranteed resolution |
| All inquiries | Any request regardless of impact level or severity | Osquery #fleet Slack channel | No guaranteed resolution |
> **Note:** If you're using Fleet Free, you can also access community support by [opening a bug](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&projects=&template=bug-report.md&title=) in the [Fleet GitHub](https://github.com/fleetdm/fleet/) repository.
**Fleet Premium:**
| Impact Level | Definition | Preferred Contact | Response Time |
| Impact level | Definition | Preferred contact | Response time |
|:-----|:----|:----|:-----|
| Emergency (P0) | Your production instance of Fleet is unavailable or completely unusable. For example, if Fleet is showing 502 errors for all users. | Expedited phone/chat/email support during business hours. </br></br>Email the contact address provided in your Fleet contract or chat with us via your dedicated private Slack channel | **≤4 hours** |
| High (P1) | Fleet is highly degraded with significant business impact. | Expedited phone/chat/email support during business hours. </br></br>Email the contact address provided in your Fleet contract or chat with us via your dedicated private Slack channel | **≤4 business hours** |
@ -493,7 +493,7 @@ You can learn more about how Fleet approaches security in the [security handbook
## Vendor questionnaires
In responding to security questionnaires, Fleet endeavors to provide full transparency via our [security policies](https://fleetdm.com/handbook/security/security-policies#security-policies) and [application security](https://fleetdm.com/handbook/business-operations/application-security) documentation. In addition to this documentation, please refer to [the vendor questionnaires page](https://fleetdm.com/handbook/business-operations/vendor-questionnaires)
In responding to security questionnaires, Fleet endeavors to provide full transparency via our [security policies](https://fleetdm.com/handbook/security/security-policies#security-policies), [trust](https://trust.fleetdm.com/), and [application security](https://fleetdm.com/handbook/business-operations/application-security) documentation. In addition to this documentation, please refer to [the vendor questionnaires page](https://fleetdm.com/handbook/business-operations/vendor-questionnaires). [Contact the Sales department](https://fleetdm.com/handbook/sales#contact-us) to address any pending questionnaires.
## Getting a contract signed
If a contract is ready for signature and requires no review or revision, the requestor logins into DocuSign using hello@ from the 1Password vault and routes the agreement to the CEO for signature.

View file

@ -184,13 +184,7 @@ This section is about creating a core team member role, and the hiring process f
#### Creating a new position
Want to hire? Here's how to open up a new position on the core team:
> Use these steps to hire a [fleetie, not a consultant](https://fleetdm.com/handbook/business-operations#who-isnt-a-consultant).
<!--
> If you think this job posting may need to stay temporarily classified (¶¶) and not shared company-wide or publicly yet, for any reason, then stop here and send a Slack DM with your proposal to the CEO instead of modifying ["🧑‍🚀 Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit) (which is visible company-wide) or submitting a draft pull request to "Open positions" (which is public).
-->
Want to hire? Use these steps to hire a [fleetie, not a consultant](https://fleetdm.com/handbook/company/leadership#who-isnt-a-consultant). Here's how to open up a new position on the core team:
1. **Propose headcount:** Add the proposed position to ["🧑‍🚀 Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) in an empty row (but using one of the existing IDs. Unsure? Ask for help.) Be sure to include job title, manager, and department. Set the start date to the first Monday of the next month (This position is still only proposed (not approved), but would make it easier for the approver to have the date set).
2. **Propose job description:** Copy, personalize, and publish the job description:

View file

@ -61,3 +61,32 @@
- 🛠️ Technical: You understand the software development processes.
- 🟣 Openness: You are flexible and open to new ideas and ways of working
- Bonus: Cybersecurity or IT background
- jobTitle: 🐋 Customer Success Engineer
department: Customers
hiringManagerName: Jason Lewis
hiringManagerGithubUsername: Patagonia121
hiringManagerLinkedInUrl: https://www.linkedin.com/in/jlewis0451/
responsibilities: |
- 🎯 Strong attention to detail and can act as an encyclopedia of knowledge about how Fleet works - our customers represent a wide range of needs across many different use cases. Be adaptable to learning new things quickly and then share this knowledge with others.
- 📣 Manage multiple customer deployments and escalations simultaneously with the ability to stay organized.
- 🚀 Deploy Fleet on your own to have a better understanding of the customer experience and how the product works.
- 🪴 Promote product adoption, referencability, and customer advocacy with key customer stakeholders.
- 🥇 Be the first line of defense in customer Slack channels for any reported problems, how-to questions, feature request intake, and bug report filling.
- 🚀 Work collaboratively with product and engineering teams to facilitate bug resolution and feature development based on customer asks.
- ⏫ Work hand-in-hand with the customer success team by participating in ad-hoc calls with customers to discuss any support issues they may have.
- 💡 Excellent communication and collaboration skills, with the ability to work closely with customer success, engineering, and product teams.
experience: |
- 💭 Cybersecurity or IT background, experience with cloud environments like AWS and Azure or device management solutions like Fleet, Intune, Jamf Pro, Workspace One, etc.
- 💖 You know how to manage your time and priorities between customer support engagements, customer escalations, and other day-to-day responsibilities.
- 🧬 An excellent understanding of macOS, Windows, Linux and core services like Autopilot, ABM/ASM, MDM, ADE, APNs, syslog, etc.
- 🤝 Collaboration: You work best in a team-based environment. You are decisive with the ability to shift gears between thinking and doing.
- 👥 A customer-centric mindset, focusing on delivering value and a positive user experience.
- 🦉 2-3 years of work experience providing technical support to enterprise customers in the cybersecurity or device management space. Experience with executing and tracking results tied to customer escalations.
- 🛠️ You are personable, enjoy being customer facing, and have a passion for problem solving while assisting external and internal stakeholders.
- 🧪 Extensive experience with Slack, Google Suite, and GitHub.
- ✍️ Familiarity with shell scripting, Python, Powershell, and using Terminal to execute commands or run scripts, and other line of business applications.
- 🟣 Openness: Speak freely. Interrupt and be interrupted. Give pointed and respectful feedback, even when you disagree.
- 🔴 Empathy: You should demonstrate empathy by keenly understanding and addressing customer concerns with genuine compassion.
- Bonus: Familiarity with osquery, MYSql, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams.

View file

@ -8,6 +8,7 @@ This page details processes specific to working [with](#contact-us) and [within]
| Head of Design | [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_
| Software Engineer | [Eric Shaw](https://www.linkedin.com/in/eric-shaw-1423831a9/) _([@eashaw](https://github.com/eashaw))_
| Head of Revenue Operations | [Taylor Hughes](https://www.linkedin.com/in/taylorhughes834/) _([@hughestaylor](https://github.com/hughestaylor))_
| Apprentice | [Award Malisi](https://www.linkedin.com/in/award-malisi/) _([@Unearthlyglow](https://github.com/Unearthlyglow))_
## Contact us

View file

@ -5,8 +5,8 @@
task: "Check browser compatibility for fleetdm.com"
startedOn: "2024-03-06"
frequency: "Monthly"
description: "Run `npm audit --only=prod` to check for vulnerabilities on the production dependencies of fleetdm.com."
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#check-production-dependencies-of-fleetdm-com"
description: "Use Browserstack to manually QA pages on fleetdm.com in each of the earliest supported browser versions"
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#check-browser-compatibility-for-fleetdm-com"
dri: "eashaw"
autoIssue: # Enables automation of GitHub issues
labels: [ "#g-digital-experience" ] # label to be applied to issue

View file

@ -143,6 +143,12 @@ To close a deal with a new customer (non-self-service), create and complete a Gi
### Change customer credit card number
You can help a Premium license dispenser customers change their credit card by directing them to their [account dashboard](https://fleetdm.com/customers/dashboard). On that page, the customer can update their billing card by clicking the pencil icon next to their billing information.
### Process a security questionnaire
- The AE will [use the handbook](https://fleetdm.com/handbook/company/communications#vendor-questionnaires) to answer most of the questions with links to appropriate sections in the handbook. After this first pass has been completed, and if there are outstanding questions, the AE will [assign the issue to Business Operations (#g-business-operations)](https://fleetdm.com/handbook/business-operations#contact-us) with a requested timeline for completion defined.
- BizOps consults the handbook to validate that nothing was missed by the AE. After the second pass has been completed, and if there are outstanding questions, BizOps will [reassign the issue to Sales (#g-sales)](https://fleetdm.com/handbook/sales#contact-us) for intake.
- The issue will be assigned to the Solutions Consultant (SC) associated to the opportunity in order to complete any unanswered questions.
- The SC will search for unanswered questions and confirm again that nothing was missed from the handbook. Content missing from the handbook will need to be added via PR by the SC. Any unanswered questions after this pass has been completed by the SC will need to be [escalated to the Infrastructure team (#g-customer-success)](https://fleetdm.com/handbook/customer-success#contact-us) with the requested timeline for completion defined in the issue. Once complete, the infra team will assign the issue back to the #g-sales board.
- Any questions answered by the infra team will be added to the handbook by the SC.
## Rituals

View file

@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
default = "fleetdm/fleet:v4.47.3"
default = "fleetdm/fleet:v4.48.0"
}
variable "software_inventory" {

View file

@ -68,5 +68,5 @@ variable "redis_mem" {
}
variable "image" {
default = "fleet:v4.47.3"
default = "fleet:v4.48.0"
}

View file

@ -38,11 +38,11 @@ require (
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect

View file

@ -381,6 +381,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -453,6 +455,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -461,6 +464,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -472,6 +476,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View file

@ -53,9 +53,9 @@
description: This policy checks if maximum amount of time (in minutes) the device is allowed to sit idle before the screen is locked. End users can select any value less than the specified maximum.
resolution: An an IT admin, deploy a macOS, screen saver profile with the maxInactivity option set to 20 minutes.
platform: darwin
- name: macOS - No 1Password emergency kit stored on desktop or in downloads
query: SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%%/Desktop/%%' OR path LIKE '/Users/%%/Documents/%%' OR path LIKE '/Users/%%/Downloads/%%' OR path LIKE '/Users/Shared'));
- name: macOS - No 1Password emergency kit stored in desktop, documents, or downloads folders
query: SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM file WHERE filename LIKE '%Emergency Kit%.pdf' AND (path LIKE '/Users/%/Desktop/%' OR path LIKE '/Users/%/Documents/%' OR path LIKE '/Users/%/Downloads/%' OR path LIKE '/Users/Shared/%'));
critical: false
description: "Looks for PDF files with file names typically used by 1Password for emergency recovery kits."
description: "Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders."
resolution: "Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location."
platform: darwin

View file

@ -0,0 +1,16 @@
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
update_channels:
# We want to use these hosts to smoke test osquery releases.
osqueryd: edge

View file

@ -9,7 +9,7 @@ team_settings:
secrets:
- secret: $DOGFOOD_SERVERS_CANARY_ENROLL_SECRET
agent_options:
path: ../lib/servers.agent-options.yml
path: ../lib/servers-canary.agent-options.yml
controls:
enable_disk_encryption: false
macos_settings:

View file

@ -100,12 +100,6 @@ policies:
- path: ../lib/macos-device-health.policies.yml
- path: ../lib/windows-device-health.policies.yml
- path: ../lib/linux-device-health.policies.yml
- name: chromeOS/macOS - Screenlock enabled
query: SELECT 1 FROM screenlock WHERE enabled = 1;
critical: false
description: ""
resolution: ""
platform: darwin,chrome
queries:
- path: ../lib/collect-failed-login-attempts.queries.yml
- path: ../lib/collect-usb-devices.queries.yml

View file

@ -19,6 +19,6 @@ Following are the currently deployed versions of fleetd components on the `stabl
|--------------|--------|--------|---------|
| orbit | 1.22.0 | 1.22.0 | 1.22.0 |
| desktop | 1.22.0 | 1.22.0 | 1.22.0 |
| osqueryd | 5.12.0 | 5.12.0 | 5.12.0 |
| osqueryd | 5.12.1 | 5.12.1 | 5.12.1 |
| nudge | - | - | - |
| swiftDialog | - | - | - |

View file

@ -0,0 +1 @@
- Add `parse_json`, `parse_jsonl`, `parse_xml`, and `parse_ini` tables.

View file

@ -385,11 +385,20 @@ func createVersionInfo(vParts []string, manifestPath string) (*goversioninfo.Ver
// SanitizeVersion returns the version parts (Major, Minor, Patch and Build), filling the Build part
// with '0' if missing. Will error out if the version string is missing the Major, Minor or
// Patch part(s).
// It supports the version with a pre-release part (e.g. 1.2.3-1) and returns it as the Build number.
func SanitizeVersion(version string) ([]string, error) {
vParts := strings.Split(version, ".")
if len(vParts) < 3 {
return nil, errors.New("invalid version string")
}
if len(vParts) == 3 && strings.Contains(vParts[2], "-") {
parts := strings.SplitN(vParts[2], "-", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("invalid patch and pre-release version: %s", vParts[2])
}
patch, preRelease := parts[0], parts[1]
vParts = []string{vParts[0], vParts[1], patch, preRelease}
}
if len(vParts) < 4 {
vParts = append(vParts, "0")
@ -465,7 +474,7 @@ func downloadAndExtractZip(client *http.Client, urlPath string, destPath string)
}
defer zipReader.Close()
err = os.MkdirAll(filepath.Dir(destPath), 0755)
err = os.MkdirAll(filepath.Dir(destPath), 0o755)
if err != nil {
return fmt.Errorf("could not create directory %s: %w", filepath.Dir(destPath), err)
}
@ -479,7 +488,6 @@ func downloadAndExtractZip(client *http.Client, urlPath string, destPath string)
}
return nil
}
func extractZipFile(archiveReader *zip.File, destPath string) error {
@ -506,13 +514,13 @@ func extractZipFile(archiveReader *zip.File, destPath string) error {
// Check if the file to extract is just a directory
if archiveReader.FileInfo().IsDir() {
err = os.MkdirAll(finalPath, 0755)
err = os.MkdirAll(finalPath, 0o755)
if err != nil {
return fmt.Errorf("could not create directory %s: %w", finalPath, err)
}
} else {
// Create all needed directories
if os.MkdirAll(filepath.Dir(finalPath), 0755) != nil {
if os.MkdirAll(filepath.Dir(finalPath), 0o755) != nil {
return fmt.Errorf("could not create directory %s: %w", filepath.Dir(finalPath), err)
}

View file

@ -75,6 +75,14 @@ func TestSanitizeVersion(t *testing.T) {
}{
{Version: "4.13.0", Parts: []string{"4", "13", "0", "0"}},
{Version: "4.13.0.1", Parts: []string{"4", "13", "0", "1"}},
// We need to support this form of semantic versioning (with pre-releases)
// to comply with semantic versioning required by goreleaser to allow building
// orbit pre-releases.
{Version: "4.13.0-1", Parts: []string{"4", "13", "0", "1"}},
{Version: "4.13.0-alpha", Parts: []string{"4", "13", "0", "alpha"}},
{Version: "4.13.0-", ErrorsOut: true},
{Version: "4.13.0.1.2", Parts: []string{"4", "13", "0", "1"}},
{Version: "4", ErrorsOut: true},
{Version: "4.13", ErrorsOut: true},

View file

@ -9,6 +9,7 @@ import (
"time"
"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/sntp_request"
"github.com/macadmins/osquery-extension/tables/chromeuserprofiles"
@ -134,6 +135,13 @@ func OrbitDefaultTables() []osquery.OsqueryPlugin {
firefox_preferences.TablePlugin(osqueryLogger),
cryptoinfotable.TablePlugin(osqueryLogger),
// Additional data format tables
dataflattentable.TablePlugin(osqueryLogger, dataflattentable.JsonType), // table name is "parse_json"
dataflattentable.TablePlugin(osqueryLogger, dataflattentable.JsonlType), // table name is "parse_jsonl"
dataflattentable.TablePlugin(osqueryLogger, dataflattentable.XmlType), // table name is "parse_xml"
dataflattentable.TablePlugin(osqueryLogger, dataflattentable.IniType), // table name is "parse_ini"
}
return plugins
}

10
osv-scanner.toml Normal file
View file

@ -0,0 +1,10 @@
# Configure OSV-Scanner
# https://google.github.io/osv-scanner/configuration/
[[IgnoredVulns]]
id = "GO-2022-0646"
reason = "2024/04/02 - This project does not use github.com/aws/aws-sdk-go/service/s3/s3crypto. Reference: https://osv.dev/vulnerability/GO-2022-0646"
[[IgnoredVulns]]
id = "GO-2023-1788"
reason = "2024/04/02 - When packaging linux files, we do not use global permissions. Manually verified that packed fleet-osquery files do not have group/global write permissions. Reference: https://osv.dev/vulnerability/GO-2023-1788"

View file

@ -117,7 +117,7 @@
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
"autoprefixer": "9.8.8",
"autoprefixer": "10.4.19",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "9.0.0",
"babel-jest": "29.2.0",
@ -150,10 +150,9 @@
"json-loader": "0.5.7",
"mini-css-extract-plugin": "2.7.5",
"msw": "0.47.4",
"nock": "13.2.4",
"node-bourbon": "4.2.8",
"node-sass-glob-importer": "5.3.2",
"postcss-loader": "3.0.0",
"node-sass-glob-importer": "5.3.3",
"postcss-loader": "4.3.0",
"prettier": "2.2.1",
"react-docgen-typescript-plugin": "1.0.5",
"regenerator-runtime": "0.13.9",
@ -169,6 +168,12 @@
"webpack-cli": "5.0.1",
"webpack-notifier": "1.12.0"
},
"resolutions": {
"**/css-node-extract": "~3.0.4",
"**/css-node-extract/postcss": "^8.4.31",
"**/css-selector-extract": "~4.0.1",
"**/wait-on/axios": "^0.28.0"
},
"browserslist": [
"defaults"
],

File diff suppressed because it is too large Load diff

View file

@ -1715,8 +1715,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "List the SSH keys allowed to connect to this host.\n```\nSELECT key FROM authorized_keys;\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN authorized_keys USING (uid);\n```",
"columns": [
{
"name": "uid",
@ -1725,8 +1725,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "algorithm",
@ -2838,8 +2837,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "See classic browser plugins (C/NPAPI) installed by users. These plugins have been deprecated for a long time, so this query will usually not return anything.\n```\nSELECT bp.name, bp.identifier, bp.version FROM browser_plugins bp JOIN users u on bp.uid = u.uid ;\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "See classic browser plugins (C/NPAPI) installed by users. These plugins have been deprecated for a long time, so this query will usually not return anything.\n```\nSELECT * FROM users CROSS JOIN browser_plugins USING (uid);\n```",
"columns": [
{
"name": "uid",
@ -3691,8 +3690,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "```\nSELECT chrome_extension_content_scripts.* FROM users JOIN chrome_extension_content_scripts USING (uid) GROUP BY identifier, match\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN chrome_extension_content_scripts USING (uid);\n```",
"columns": [
{
"name": "browser_type",
@ -3710,8 +3709,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": true,
"requires_user_context": true
"index": true
},
{
"name": "identifier",
@ -3791,8 +3789,8 @@
],
"evented": false,
"cacheable": false,
"notes": "- On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos).\n",
"examples": "List Chrome extensions by user and profile which have full access to HTTPS browsing.\n```\nSELECT u.username, ce.name, ce.description, ce.version, ce.profile, ce.permissions FROM users u CROSS JOIN chrome_extensions ce USING (uid) WHERE ce.permissions LIKE '%%https://*/*%%';\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)\n\nOn ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos).\n",
"examples": "```\nSELECT * FROM users CROSS JOIN chrome_extensions USING (uid);\n```\nList Chrome extensions by user and profile which have full access to HTTPS browsing.\n```\nSELECT u.username, ce.name, ce.description, ce.version, ce.profile, ce.permissions FROM users u CROSS JOIN chrome_extensions ce USING (uid) WHERE ce.permissions LIKE '%%https://*/*%%';\n```",
"columns": [
{
"name": "browser_type",
@ -3815,8 +3813,7 @@
"macOS",
"Windows",
"Linux"
],
"requires_user_context": true
]
},
{
"name": "name",
@ -5067,8 +5064,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "See software responsible for crashes. This can be useful to detect what the most problematic software in your environment is.\n```\nSELECT crash_path, identifier, responsible, exception_type FROM crashes;\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN crashes USING (uid);\n```",
"columns": [
{
"name": "type",
@ -5149,8 +5146,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": true,
"requires_user_context": true
"index": true
},
{
"name": "datetime",
@ -10781,8 +10777,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "See Firefox extensions by user as well as information about their creator and automatic update status.\n```\nSELECT u.username, f.identifier, f.creator, f.description, f.version, f.autoupdate FROM users u CROSS JOIN firefox_addons f USING (uid) WHERE f.active='1';\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN firefox_addons USING (uid);\n```\nSee Firefox extensions by user as well as information about their creator and automatic update status.\n```\nSELECT u.username, f.identifier, f.creator, f.description, f.version, f.autoupdate FROM users u CROSS JOIN firefox_addons f USING (uid) WHERE f.active='1';\n```",
"columns": [
{
"name": "uid",
@ -10791,8 +10787,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "name",
@ -13306,8 +13301,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "```\nselect * from users join known_hosts using (uid)\n```",
"notes": "- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN known_hosts USING (uid);\n```",
"columns": [
{
"name": "uid",
@ -13316,8 +13311,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": true,
"requires_user_context": true
"index": true
},
{
"name": "key",
@ -18786,6 +18780,186 @@
],
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/package_receipts.yml"
},
{
"name": "parse_ini",
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"description": "Parse a file as INI configuration.",
"platforms": [
"darwin",
"windows",
"linux"
],
"evented": false,
"columns": [
{
"name": "path",
"description": "Path of the file to read.",
"required": true,
"type": "text"
},
{
"name": "fullkey",
"description": "Key including any parent keys.",
"type": "text",
"required": false
},
{
"name": "parent",
"description": "Parent key when keys are nested in the document.",
"required": false,
"type": "text"
},
{
"name": "key",
"description": "JSON key or array index.",
"required": false,
"type": "text"
},
{
"name": "value",
"description": "JSON value",
"required": false,
"type": "text"
}
],
"url": "https://fleetdm.com/tables/parse_ini",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/parse_ini.yml"
},
{
"name": "parse_json",
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"description": "Parses an entire file as JSON. See `parse_jsonl` where multiple JSON documents are supported.",
"platforms": [
"darwin",
"windows",
"linux"
],
"evented": false,
"columns": [
{
"name": "path",
"description": "Path of the file to read.",
"required": true,
"type": "text"
},
{
"name": "fullkey",
"description": "Same as `key` in this table. See `parse_jsonl` where multiple JSON documents are supported.",
"required": false,
"type": "text"
},
{
"name": "parent",
"description": "Parent key when keys are nested in the document.",
"required": false,
"type": "text"
},
{
"name": "key",
"description": "JSON key or array index.",
"required": false,
"type": "text"
},
{
"name": "value",
"description": "JSON value",
"required": false,
"type": "text"
}
],
"url": "https://fleetdm.com/tables/parse_json",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/parse_json.yml"
},
{
"name": "parse_jsonl",
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"description": "Parses each line of a file as a separate JSON document. See `parse_json` to treat an entire file as a single JSON document.",
"platforms": [
"darwin",
"windows",
"linux"
],
"evented": false,
"columns": [
{
"name": "path",
"description": "Path of the file to read.",
"required": true,
"type": "text"
},
{
"name": "fullkey",
"description": "Key including any parent keys or document indices.",
"required": false,
"type": "text"
},
{
"name": "parent",
"description": "Parent key when keys are nested in the document.",
"required": false,
"type": "text"
},
{
"name": "key",
"description": "INI key",
"required": false,
"type": "text"
},
{
"name": "value",
"description": "INI value",
"required": false,
"type": "text"
}
],
"url": "https://fleetdm.com/tables/parse_jsonl",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/parse_jsonl.yml"
},
{
"name": "parse_xml",
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"description": "Parses a file as an XML document.",
"platforms": [
"darwin",
"windows",
"linux"
],
"evented": false,
"columns": [
{
"name": "path",
"description": "Path of the file to read.",
"required": true,
"type": "text"
},
{
"name": "fullkey",
"description": "Key including any parent keys.",
"required": false,
"type": "text"
},
{
"name": "parent",
"description": "Parent key when keys are nested in the document.",
"required": false,
"type": "text"
},
{
"name": "key",
"description": "XML key",
"required": false,
"type": "text"
},
{
"name": "value",
"description": "XML value",
"required": false,
"type": "text"
}
],
"url": "https://fleetdm.com/tables/parse_xml",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/parse_xml.yml"
},
{
"name": "password_policy",
"description": "Password Policies for macOS.",
@ -19795,8 +19969,8 @@
],
"evented": false,
"cacheable": false,
"notes": "- The `value` column will be empty for keys that contain binary data.",
"examples": "This table reads a huge amount of preferences, including on third-party apps. This query will show how many users are enrolled to TouchID.\n```\nSELECT * FROM preferences WHERE subkey='dailyEvents/2/enrolledUserCount';\n```",
"notes": "- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)\n- The `value` column will be empty for keys that contain binary data.",
"examples": "This table reads a huge amount of preferences, including on third-party apps.\n```\nSELECT * FROM users CROSS JOIN preferences USING (username);\n```",
"columns": [
{
"name": "domain",
@ -19850,8 +20024,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "host",
@ -22842,8 +23015,8 @@
],
"evented": false,
"cacheable": false,
"notes": "- Includes installed extensions for all system users.",
"examples": "```\nselect count(*) from users JOIN safari_extensions using (uid)\n```",
"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.",
"examples": "```\nSELECT * FROM users CROSS JOIN safari_extensions USING (uid);\n```",
"columns": [
{
"name": "uid",
@ -22852,8 +23025,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": true,
"requires_user_context": true
"index": true
},
{
"name": "name",
@ -24342,8 +24514,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "See command line executions and related timestamps. Useful for threat hunting when a device is suspected of being compromised.\n```\nSELECT u.username, s.command, s.time FROM users u CROSS JOIN shell_history s USING (uid);\n```",
"notes": "- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN shell_history USING (uid);\n```\nSee command line executions and related timestamps. Useful for threat hunting when a device is suspected of being compromised.\n```\nSELECT u.username, s.command, s.time FROM users u CROSS JOIN shell_history s USING (uid);\n```",
"columns": [
{
"name": "uid",
@ -24352,8 +24524,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "time",
@ -25024,8 +25195,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "Identify SSH clients configured to send their locales to the server.\n```\nSELECT * FROM ssh_configs WHERE option='sendenv lang lc_*'; \n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN ssh_configs USING (uid);\n```\nIdentify SSH clients configured to send their locales to the server.\n```\nSELECT * FROM ssh_configs WHERE option='sendenv lang lc_*'; \n```",
"columns": [
{
"name": "uid",
@ -25034,8 +25205,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "block",
@ -26918,8 +27088,8 @@
],
"evented": false,
"cacheable": false,
"notes": "",
"examples": "Identify SSH keys stored in clear text in user directories\n```\nSELECT * FROM users JOIN user_ssh_keys USING (uid) WHERE encrypted = 0;\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN user_ssh_keys USING (uid);\n```\nIdentify SSH keys stored in clear text in user directories\n```\nSELECT * FROM users JOIN user_ssh_keys USING (uid) WHERE encrypted = 0;\n```",
"columns": [
{
"name": "uid",
@ -26928,8 +27098,7 @@
"notes": "",
"hidden": false,
"required": false,
"index": false,
"requires_user_context": true
"index": false
},
{
"name": "path",
@ -27509,8 +27678,8 @@
],
"evented": false,
"cacheable": false,
"notes": "Querying this table requires joining against the `users` table.",
"examples": "List the name, publisher, and version of the Visual Studio (VS) Code extensions installed on hosts.\n```\nSELECT extension.name, extension.publisher, extension.version FROM users JOIN vscode_extensions extension USING (uid);\n```",
"notes": "Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)",
"examples": "```\nSELECT * FROM users CROSS JOIN vscode_extensions USING (uid);\n```\n\nList the name, publisher, and version of the Visual Studio (VS) Code extensions installed on hosts.\n```\nSELECT extension.name, extension.publisher, extension.version FROM users JOIN vscode_extensions extension USING (uid);\n```",
"columns": [
{
"name": "name",

View file

@ -1,15 +1,13 @@
name: authorized_keys
examples: >-
List the SSH keys allowed to connect to this host.
```
SELECT key FROM authorized_keys;
SELECT * FROM users CROSS JOIN authorized_keys USING (uid);
```
columns:
- name: pid_with_namespace
platforms:
- linux
- name: uid
requires_user_context: true
notes: Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)

View file

@ -7,6 +7,8 @@ examples: >-
```
SELECT bp.name, bp.identifier, bp.version FROM browser_plugins bp JOIN users u on bp.uid = u.uid ;
SELECT * FROM users CROSS JOIN browser_plugins 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)

View file

@ -1,4 +1,10 @@
name: chrome_extension_content_scripts
columns:
- name: uid
requires_user_context: true
examples: >-
```
SELECT * FROM users CROSS JOIN chrome_extension_content_scripts 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)

View file

@ -6,6 +6,12 @@ platforms:
- chrome
description: Installed extensions (plugins) for [Chromium-based](https://en.wikipedia.org/wiki/Chromium_(web_browser)) browsers, including [Google Chrome](https://en.wikipedia.org/wiki/Google_Chrome), [Edge](https://en.wikipedia.org/wiki/Microsoft_Edge), [Brave](https://en.wikipedia.org/wiki/Brave_(web_browser)), [Opera](https://en.wikipedia.org/wiki/Opera_(web_browser)), and [Yandex](https://en.wikipedia.org/wiki/Yandex_Browser).
examples: >-
```
SELECT * FROM users CROSS JOIN chrome_extensions USING (uid);
```
List Chrome extensions by user and profile which have full access to HTTPS
browsing.
@ -14,9 +20,12 @@ examples: >-
SELECT u.username, ce.name, ce.description, ce.version, ce.profile, ce.permissions FROM users u CROSS JOIN chrome_extensions ce USING (uid) WHERE ce.permissions LIKE '%%https://*/*%%';
```
notes: |
Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos).
columns:
- name: uid
requires_user_context: true
platforms:
- darwin
- windows
@ -106,5 +115,3 @@ columns:
- darwin
- windows
- linux
notes: |
- On ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos).

View file

@ -1,13 +1,10 @@
name: crashes
examples: >-
See software responsible for crashes. This can be useful to detect what the
most problematic software in your environment is.
```
SELECT crash_path, identifier, responsible, exception_type FROM crashes;
SELECT * FROM users CROSS JOIN crashes 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)
columns:
- name: uid
requires_user_context: true

View file

@ -1,6 +1,12 @@
name: firefox_addons
description: Firefox browser [add-ons](https://addons.mozilla.org/en-US/firefox/) (plugins).
examples: >-
```
SELECT * FROM users CROSS JOIN firefox_addons USING (uid);
```
See Firefox extensions by user as well as information about their creator and
automatic update status.
@ -9,6 +15,6 @@ examples: >-
SELECT u.username, f.identifier, f.creator, f.description, f.version, f.autoupdate FROM users u CROSS JOIN firefox_addons f USING (uid) WHERE f.active='1';
```
notes: Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
columns:
- name: uid
requires_user_context: true

View file

@ -1,4 +1,12 @@
name: known_hosts
columns:
- name: uid
requires_user_context: true
examples: >-
```
SELECT * FROM users CROSS JOIN known_hosts 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)

View file

@ -0,0 +1,29 @@
name: parse_ini
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
description: Parse a file as INI configuration.
platforms:
- darwin
- windows
- linux
evented: false
columns:
- name: path
description: Path of the file to read.
required: true
type: text
- name: fullkey
description: Key including any parent keys.
type: text
required: false
- name: parent
description: Parent key when keys are nested in the document.
required: false
type: text
- name: key
description: JSON key or array index.
required: false
type: text
- name: value
description: JSON value
required: false
type: text

View file

@ -0,0 +1,29 @@
name: parse_json
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
description: Parses an entire file as JSON. See `parse_jsonl` where multiple JSON documents are supported.
platforms:
- darwin
- windows
- linux
evented: false
columns:
- name: path
description: Path of the file to read.
required: true
type: text
- name: fullkey
description: Same as `key` in this table. See `parse_jsonl` where multiple JSON documents are supported.
required: false
type: text
- name: parent
description: Parent key when keys are nested in the document.
required: false
type: text
- name: key
description: JSON key or array index.
required: false
type: text
- name: value
description: JSON value
required: false
type: text

View file

@ -0,0 +1,29 @@
name: parse_jsonl
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
description: Parses each line of a file as a separate JSON document. See `parse_json` to treat an entire file as a single JSON document.
platforms:
- darwin
- windows
- linux
evented: false
columns:
- name: path
description: Path of the file to read.
required: true
type: text
- name: fullkey
description: Key including any parent keys or document indices.
required: false
type: text
- name: parent
description: Parent key when keys are nested in the document.
required: false
type: text
- name: key
description: INI key
required: false
type: text
- name: value
description: INI value
required: false
type: text

View file

@ -0,0 +1,29 @@
name: parse_xml
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
description: Parses a file as an XML document.
platforms:
- darwin
- windows
- linux
evented: false
columns:
- name: path
description: Path of the file to read.
required: true
type: text
- name: fullkey
description: Key including any parent keys.
required: false
type: text
- name: parent
description: Parent key when keys are nested in the document.
required: false
type: text
- name: key
description: XML key
required: false
type: text
- name: value
description: XML value
required: false
type: text

View file

@ -1,15 +1,15 @@
name: preferences
examples: >-
This table reads a huge amount of preferences, including on third-party apps.
This query will show how many users are enrolled to TouchID.
```
SELECT * FROM preferences WHERE subkey='dailyEvents/2/enrolledUserCount';
SELECT * FROM users CROSS JOIN preferences USING (username);
```
notes: >-
- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
- The `value` column will be empty for keys that contain binary data.
columns:
- name: username
requires_user_context: true

View file

@ -2,6 +2,12 @@ name: safari_extensions
description: Installed Safari browser extensions (plugins).
columns:
- name: uid
requires_user_context: true
examples: >-
```
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.

View file

@ -1,5 +1,11 @@
name: shell_history
examples: >-
```
SELECT * FROM users CROSS JOIN shell_history USING (uid);
```
See command line executions and related timestamps. Useful for threat hunting
when a device is suspected of being compromised.
@ -10,4 +16,7 @@ examples: >-
```
columns:
- name: uid
requires_user_context: true
notes: >-
- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)

View file

@ -1,5 +1,11 @@
name: ssh_configs
examples: >-
```
SELECT * FROM users CROSS JOIN ssh_configs USING (uid);
```
Identify SSH clients configured to send their locales to the server.
```
@ -9,4 +15,4 @@ examples: >-
```
columns:
- name: uid
requires_user_context: true
notes: Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)

View file

@ -1,5 +1,11 @@
name: user_ssh_keys
examples: >-
```
SELECT * FROM users CROSS JOIN user_ssh_keys USING (uid);
```
Identify SSH keys stored in clear text in user directories
```
@ -12,4 +18,4 @@ columns:
platforms:
- linux
- name: uid
requires_user_context: true
notes: Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)

View file

@ -1,6 +1,13 @@
name: vscode_extensions
description: Installed extensions for [Visual Studio (VS) Code](https://code.visualstudio.com/).
examples: >-
```
SELECT * FROM users CROSS JOIN vscode_extensions USING (uid);
```
List the name, publisher, and version of the Visual Studio (VS) Code extensions installed on hosts.
```
@ -8,7 +15,7 @@ examples: >-
SELECT extension.name, extension.publisher, extension.version FROM users JOIN vscode_extensions extension USING (uid);
```
notes: Querying this table requires joining against the `users` table.
notes: Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
columns:
- name: name
description: Extension Name

View file

@ -1,16 +1,59 @@
#!/usr/bin/env bash
set -eo pipefail
# Run this script in user context (not root).
# Reference: https://wiki.winehq.org/MacOS
# Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0, or by building from source.
# Check if brew is installed
if ! command -v brew >/dev/null 2>&1 ; then
echo "Homebrew is not installed. Please install Homebrew first. For instructions, see https://brew.sh/"
exit 1
brew_wine(){
# Wine reference: https://wiki.winehq.org/MacOS
# Wine can be installed without brew via a distribution such as https://github.com/Gcenx/macOS_Wine_builds/releases/tag/9.0 or by building from source.
brew install --cask --no-quarantine https://raw.githubusercontent.com/Homebrew/homebrew-cask/1ecfe82f84e0f3c3c6b741d3ddc19a164c2cb18d/Casks/w/wine-stable.rb; exit 0
}
warn_wine(){
printf "\nWARNING: The Wine app developer has an Apple Developer certificate but the\napp bundle post-installation will not be code-signed or notarized.\n\nDo you wish to proceed?\n\n"
while true
do
read -r -p "install> " install
case "$install" in
y|yes|Y|YES) brew_wine ;;
n|no|N|NO) printf "\nExiting...\n\n"; exit 1 ;;
*) printf "\nPlease enter yes or no at the prompt...\n\n" ;;
esac
done
}
# option to execute script in non-interactive mode
while getopts 'n' option
do
case "$option" in
n) mode=auto ;;
*) : ;;
esac
done
# prevent root execution
if [ "$EUID" = 0 ]
then
printf "\nTo prevent unnecessary privilege elevation do not execute this script as the root user.\nExiting...\n\n"; exit 1
fi
# check if Homebrew is installed
if ! command -v brew > /dev/null 2>&1
then
printf "\nHomebrew is not installed.\nPlease install Homebrew.\nFor instructions, see https://brew.sh/\n\n"; exit 1
fi
# install Wine
if [ "$mode" = 'auto' ]
then
printf "\n%s executed in non-interactive mode.\n\n" "$0"; brew_wine
else
warn_wine
fi
# Install wine via brew
brew install --cask --no-quarantine https://raw.githubusercontent.com/Homebrew/homebrew-cask/1ecfe82f84e0f3c3c6b741d3ddc19a164c2cb18d/Casks/w/wine-stable.rb

View file

@ -1,4 +1,4 @@
package main
package cron
import (
"context"
@ -19,19 +19,19 @@ import (
const calendarConsumers = 18
func newCalendarSchedule(
func NewCalendarSchedule(
ctx context.Context,
instanceID string,
ds fleet.Datastore,
interval time.Duration,
logger kitlog.Logger,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronCalendar)
defaultInterval = 5 * time.Minute
name = string(fleet.CronCalendar)
)
logger = kitlog.With(logger, "cron", name)
s := schedule.New(
ctx, name, instanceID, defaultInterval, ds, ds,
ctx, name, instanceID, interval, ds, ds,
schedule.WithAltLockID("calendar"),
schedule.WithLogger(logger),
schedule.WithJob(
@ -222,7 +222,7 @@ func processCalendarFailingHosts(
// thus we skip this entry.
continue // continue with next host
}
if hostCalendarEvent.WebhookStatus == fleet.CalendarWebhookStatusPending {
if hostCalendarEvent.WebhookStatus == fleet.CalendarWebhookStatusPending || hostCalendarEvent.WebhookStatus == fleet.CalendarWebhookStatusRetry {
// This can happen if the host went offline (and never returned results)
// after setting the webhook as pending.
continue // continue with next host
@ -318,9 +318,6 @@ func processFailingHostExistingCalendarEvent(
}
// Even if fields haven't changed we want to update the calendar_events.updated_at below.
updated = true
//
// TODO(lucas): Check changing updatedEvent to UTC before consuming.
//
}
if updated {
@ -367,8 +364,6 @@ func processFailingHostExistingCalendarEvent(
return fmt.Errorf("update host calendar webhook status: %w", err)
}
// TODO(lucas): If this doesn't work at scale, then implement a special refetch
// for policies only.
if err := ds.UpdateHostRefetchRequested(ctx, host.HostID, true); err != nil {
return fmt.Errorf("refetch host: %w", err)
}
@ -676,7 +671,10 @@ func deleteCalendarEventsInParallel(
go func() {
defer wg.Done()
for calEvent := range calendarEventCh {
userCalendar := createUserCalendarFromConfig(ctx, calendarConfig, logger)
var userCalendar fleet.UserCalendar
if calendarConfig != nil {
userCalendar = createUserCalendarFromConfig(ctx, calendarConfig, logger)
}
if err := deleteCalendarEvent(ctx, ds, userCalendar, calEvent); err != nil {
level.Error(logger).Log("msg", "delete user calendar event", "err", err)
continue

View file

@ -1,11 +1,8 @@
package main
package cron
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
@ -17,7 +14,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
)
@ -207,16 +203,19 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
calendar.ClearMockEvents()
})
// TODO(lucas): Test!
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
requestBodyBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
t.Logf("webhook request: %s\n", requestBodyBytes)
}))
t.Cleanup(func() {
webhookServer.Close()
})
//
// Test setup
//
// team1:
//
// policyID1 (calendar)
// policyID2 (calendar)
//
// hostID1 has user1@example.com not passing policies.
// hostID2 has user2@example.com passing policies.
// hostID3 does not have example.com email and is not passing policies.
// hostID4 does not have example.com email and is passing policies.
//
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
@ -242,7 +241,7 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},
@ -268,12 +267,13 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
hostID1, userEmail1 := uint(100), "user1@example.com"
hostID2, userEmail2 := uint(101), "user2@example.com"
hostID3, userEmail3 := uint(102), "user3@other.com"
hostID4, userEmail4 := uint(103), "user4@other.com"
hostID3 := uint(102)
hostID4 := uint(103)
ds.GetTeamHostsPolicyMembershipsFunc = func(
ctx context.Context, domain string, teamID uint, policyIDs []uint,
) ([]fleet.HostPolicyMembershipData, error) {
require.Equal(t, "example.com", domain)
require.Equal(t, teamID1, teamID)
require.Equal(t, []uint{policyID1, policyID2}, policyIDs)
return []fleet.HostPolicyMembershipData{
@ -289,12 +289,12 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
},
{
HostID: hostID3,
Email: userEmail3,
Email: "", // because it does not belong to example.com
Passing: false,
},
{
HostID: hostID4,
Email: userEmail4,
Email: "", // because it does not belong to example.com
Passing: true,
},
}, nil
@ -304,6 +304,10 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
return nil, nil, notFoundErr{}
}
var eventsMu sync.Mutex
calendarEvents := make(map[string]*fleet.CalendarEvent)
hostCalendarEvents := make(map[uint]*fleet.HostCalendarEvent)
ds.CreateOrUpdateCalendarEventFunc = func(ctx context.Context,
email string,
startTime, endTime time.Time,
@ -311,26 +315,43 @@ func TestCalendarEventsMultipleHosts(t *testing.T) {
hostID uint,
webhookStatus fleet.CalendarWebhookStatus,
) (*fleet.CalendarEvent, error) {
switch email {
case userEmail1:
require.Equal(t, hostID1, hostID)
case userEmail2:
require.Equal(t, hostID2, hostID)
case userEmail3:
require.Equal(t, hostID3, hostID)
case userEmail4:
require.Equal(t, hostID4, hostID)
}
require.Equal(t, hostID1, hostID)
require.Equal(t, userEmail1, email)
require.Equal(t, fleet.CalendarWebhookStatusNone, webhookStatus)
require.NotEmpty(t, data)
require.NotZero(t, startTime)
require.NotZero(t, endTime)
// Currently, the returned calendar event is unused.
eventsMu.Lock()
calendarEventID := uint(len(calendarEvents) + 1)
calendarEvents[email] = &fleet.CalendarEvent{
ID: calendarEventID,
Email: email,
StartTime: startTime,
EndTime: endTime,
Data: data,
}
hostCalendarEventID := uint(len(hostCalendarEvents) + 1)
hostCalendarEvents[hostID] = &fleet.HostCalendarEvent{
ID: hostCalendarEventID,
HostID: hostID,
CalendarEventID: calendarEventID,
WebhookStatus: webhookStatus,
}
eventsMu.Unlock()
return nil, nil
}
err := cronCalendarEvents(ctx, ds, logger)
require.NoError(t, err)
eventsMu.Lock()
require.Len(t, calendarEvents, 1)
require.Len(t, hostCalendarEvents, 1)
eventsMu.Unlock()
createdCalendarEvents := calendar.ListGoogleMockEvents()
require.Len(t, createdCalendarEvents, 1)
}
type notFoundErr struct{}
@ -356,17 +377,6 @@ func TestCalendarEvents1KHosts(t *testing.T) {
calendar.ClearMockEvents()
})
// TODO(lucas): Use for the test.
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
requestBodyBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
t.Logf("webhook request: %s\n", requestBodyBytes)
}))
t.Cleanup(func() {
webhookServer.Close()
})
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
Integrations: fleet.Integrations{
@ -395,7 +405,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},
@ -406,7 +416,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},
@ -417,7 +427,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},
@ -428,7 +438,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},
@ -439,7 +449,7 @@ func TestCalendarEvents1KHosts(t *testing.T) {
Integrations: fleet.TeamIntegrations{
GoogleCalendar: &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: webhookServer.URL,
WebhookURL: "https://foo.example.com",
},
},
},

View file

@ -52,7 +52,7 @@ func (ds *Datastore) CreateOrUpdateCalendarEvent(
} else {
stmt := `SELECT id FROM calendar_events WHERE email = ?`
if err := sqlx.GetContext(ctx, tx, &id, stmt, email); err != nil {
return ctxerr.Wrap(ctx, err, "query mdm solution id")
return ctxerr.Wrap(ctx, err, "calendar event id")
}
}

View file

@ -1171,7 +1171,6 @@ func (ds *Datastore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fl
return policies, nil
}
// TODO(lucas): Must be tested at scale.
func (ds *Datastore) GetTeamHostsPolicyMemberships(
ctx context.Context,
domain string,

View file

@ -18,6 +18,8 @@ const (
CalendarWebhookStatusNone CalendarWebhookStatus = iota
CalendarWebhookStatusPending
CalendarWebhookStatusSent
CalendarWebhookStatusError
CalendarWebhookStatusRetry
)
type HostCalendarEvent struct {

View file

@ -594,6 +594,11 @@ type Datastore interface {
PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error)
// GetTeamHostsPolicyMembmerships returns the hosts that belong to the given team and their pass/fail statuses
// around the provided policyIDs.
// - Returns hosts of the team that are failing one or more of the provided policies.
// - Returns hosts of the team that are passing all the policies (or are not running any of the provided policies)
// and have a calendar event scheduled.
GetTeamHostsPolicyMemberships(ctx context.Context, domain string, teamID uint, policyIDs []uint) ([]HostPolicyMembershipData, error)
GetCalendarPolicies(ctx context.Context, teamID uint) ([]PolicyCalendarData, error)

View file

@ -7,7 +7,6 @@ import (
"crypto/x509"
"encoding/binary"
"encoding/pem"
"errors"
"fmt"
"math"
"net/url"
@ -36,18 +35,6 @@ func EncodeCertPEM(cert *x509.Certificate) []byte {
return pem.EncodeToMemory(&block)
}
func DecodeCertPEM(encoded []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(encoded)
if block == nil {
return nil, errors.New("no PEM-encoded data found")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("unexpected block type %s", block.Type)
}
return x509.ParseCertificate(block.Bytes)
}
func EncodeCertRequestPEM(cert *x509.CertificateRequest) []byte {
pemBlock := &pem.Block{
Type: "CERTIFICATE REQUEST",
@ -67,19 +54,6 @@ func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte {
return pem.EncodeToMemory(&block)
}
// DecodePrivateKeyPEM decodes PEM-encoded private key data.
func DecodePrivateKeyPEM(encoded []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(encoded)
if block == nil {
return nil, errors.New("no PEM-encoded data found")
}
if block.Type != "RSA PRIVATE KEY" {
return nil, fmt.Errorf("unexpected block type %s", block.Type)
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
// GenerateRandomPin generates a `lenght`-digit PIN number that takes into
// account the current time as described in rfc4226 (for one time passwords)
//

View file

@ -0,0 +1,67 @@
package commonmdm
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestResolveURL(t *testing.T) {
type testCase struct {
serverURL string
relPath string
cleanQuery bool
expected string
expectErr bool
}
testCases := []testCase{
{
serverURL: "http://example.com",
relPath: "path/to/resource",
cleanQuery: false,
expected: "http://example.com/path/to/resource",
expectErr: false,
},
{
serverURL: "http://example.com?query=string",
relPath: "path",
cleanQuery: true,
expected: "http://example.com/path",
expectErr: false,
},
{
serverURL: "http://example.com/base/",
relPath: "/path",
cleanQuery: false,
expected: "http://example.com/base/path",
expectErr: false,
},
{
serverURL: "http://example.com",
relPath: "path/to/resource",
cleanQuery: true,
expected: "http://example.com/path/to/resource",
expectErr: false,
},
{
serverURL: ":invalidurl",
relPath: "path",
cleanQuery: false,
expected: "",
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.serverURL+"_"+tc.relPath, func(t *testing.T) {
result, err := ResolveURL(tc.serverURL, tc.relPath, tc.cleanQuery)
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, result)
}
})
}
}

View file

@ -81,10 +81,6 @@ func ResolveWindowsMDMEnroll(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDE2EnrollPath, false)
}
func ResolveWindowsMDMAuth(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDE2AuthPath, false)
}
func ResolveWindowsMDMManagement(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDE2ManagementPath, false)
}

View file

@ -0,0 +1,201 @@
package microsoft_mdm
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"net"
"testing"
"github.com/stretchr/testify/require"
)
func TestGetPublicKeyAlgorithmFromOID(t *testing.T) {
testCases := []struct {
oid asn1.ObjectIdentifier
expected x509.PublicKeyAlgorithm
}{
{oidPublicKeyRSA, x509.RSA},
{oidPublicKeyDSA, x509.DSA},
{oidPublicKeyECDSA, x509.ECDSA},
{oidPublicKeyEd25519, x509.Ed25519},
{asn1.ObjectIdentifier{0, 0}, x509.UnknownPublicKeyAlgorithm},
}
for _, tc := range testCases {
t.Run(tc.oid.String(), func(t *testing.T) {
result := getPublicKeyAlgorithmFromOID(tc.oid)
require.Equal(t, tc.expected, result)
})
}
}
// The following tests were taken from the Go standard library (since the wstep
// code was taken from there as well)
// Copyright 2009 The Go Authors. All rights reserved.
var pemPrivateKey = testingKey(`
-----BEGIN RSA TESTING KEY-----
MIICXAIBAAKBgQCxoeCUW5KJxNPxMp+KmCxKLc1Zv9Ny+4CFqcUXVUYH69L3mQ7v
IWrJ9GBfcaA7BPQqUlWxWM+OCEQZH1EZNIuqRMNQVuIGCbz5UQ8w6tS0gcgdeGX7
J7jgCQ4RK3F/PuCM38QBLaHx988qG8NMc6VKErBjctCXFHQt14lerd5KpQIDAQAB
AoGAYrf6Hbk+mT5AI33k2Jt1kcweodBP7UkExkPxeuQzRVe0KVJw0EkcFhywKpr1
V5eLMrILWcJnpyHE5slWwtFHBG6a5fLaNtsBBtcAIfqTQ0Vfj5c6SzVaJv0Z5rOd
7gQF6isy3t3w9IF3We9wXQKzT6q5ypPGdm6fciKQ8RnzREkCQQDZwppKATqQ41/R
vhSj90fFifrGE6aVKC1hgSpxGQa4oIdsYYHwMzyhBmWW9Xv/R+fPyr8ZwPxp2c12
33QwOLPLAkEA0NNUb+z4ebVVHyvSwF5jhfJxigim+s49KuzJ1+A2RaSApGyBZiwS
rWvWkB471POAKUYt5ykIWVZ83zcceQiNTwJBAMJUFQZX5GDqWFc/zwGoKkeR49Yi
MTXIvf7Wmv6E++eFcnT461FlGAUHRV+bQQXGsItR/opIG7mGogIkVXa3E1MCQARX
AAA7eoZ9AEHflUeuLn9QJI/r0hyQQLEtrpwv6rDT1GCWaLII5HJ6NUFVf4TTcqxo
6vdM4QGKTJoO+SaCyP0CQFdpcxSAuzpFcKv0IlJ8XzS/cy+mweCMwyJ1PFEc4FX6
wg/HcAJWY60xZTJDFN+Qfx8ZQvBEin6c2/h+zZi5IVY=
-----END RSA TESTING KEY-----
`)
var testPrivateKey *rsa.PrivateKey
func init() {
block, _ := pem.Decode([]byte(pemPrivateKey))
var err error
if testPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
panic("Failed to parse private key: " + err.Error())
}
}
func TestCreateCertificateRequest(t *testing.T) {
random := rand.Reader
ecdsa256Priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate ECDSA key: %s", err)
}
ecdsa384Priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate ECDSA key: %s", err)
}
ecdsa521Priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate ECDSA key: %s", err)
}
_, ed25519Priv, err := ed25519.GenerateKey(random)
if err != nil {
t.Fatalf("Failed to generate Ed25519 key: %s", err)
}
tests := []struct {
name string
priv interface{}
sigAlgo x509.SignatureAlgorithm
}{
{"RSA", testPrivateKey, x509.SHA1WithRSA},
{"ECDSA-256", ecdsa256Priv, x509.ECDSAWithSHA1},
{"ECDSA-384", ecdsa384Priv, x509.ECDSAWithSHA1},
{"ECDSA-521", ecdsa521Priv, x509.ECDSAWithSHA1},
{"Ed25519", ed25519Priv, x509.PureEd25519},
}
for _, test := range tests {
template := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "test.example.com",
Organization: []string{"Σ Acme Co"},
},
SignatureAlgorithm: test.sigAlgo,
DNSNames: []string{"test.example.com"},
EmailAddresses: []string{"gopher@golang.org"},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
}
derBytes, err := x509.CreateCertificateRequest(random, &template, test.priv)
if err != nil {
t.Errorf("%s: failed to create certificate request: %s", test.name, err)
continue
}
out, err := ParseCertificateRequestFromWindowsDevice(derBytes)
if err != nil {
t.Errorf("%s: failed to create certificate request: %s", test.name, err)
continue
}
err = out.CheckSignature()
if err != nil {
t.Errorf("%s: failed to check certificate request signature: %s", test.name, err)
continue
}
if out.Subject.CommonName != template.Subject.CommonName {
t.Errorf("%s: output subject common name and template subject common name don't match", test.name)
} else if len(out.Subject.Organization) != len(template.Subject.Organization) {
t.Errorf("%s: output subject organisation and template subject organisation don't match", test.name)
} else if len(out.DNSNames) != len(template.DNSNames) {
t.Errorf("%s: output DNS names and template DNS names don't match", test.name)
} else if len(out.EmailAddresses) != len(template.EmailAddresses) {
t.Errorf("%s: output email addresses and template email addresses don't match", test.name)
} else if len(out.IPAddresses) != len(template.IPAddresses) {
t.Errorf("%s: output IP addresses and template IP addresses names don't match", test.name)
}
}
}
func fromBase64(in string) []byte {
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))
n, err := base64.StdEncoding.Decode(out, []byte(in))
if err != nil {
panic("failed to base64 decode")
}
return out[:n]
}
func TestParseCertificateRequestFromWindowsDevice(t *testing.T) {
for _, csrBase64 := range csrBase64Array {
csrBytes := fromBase64(csrBase64)
csr, err := ParseCertificateRequestFromWindowsDevice(csrBytes)
if err != nil {
t.Fatalf("failed to parse CSR: %s", err)
}
if len(csr.EmailAddresses) != 1 || csr.EmailAddresses[0] != "gopher@golang.org" {
t.Errorf("incorrect email addresses found: %v", csr.EmailAddresses)
}
if len(csr.DNSNames) != 1 || csr.DNSNames[0] != "test.example.com" {
t.Errorf("incorrect DNS names found: %v", csr.DNSNames)
}
if len(csr.Subject.Country) != 1 || csr.Subject.Country[0] != "AU" {
t.Errorf("incorrect Subject name: %v", csr.Subject)
}
}
}
// These CSR was generated with OpenSSL:
//
// openssl req -out CSR.csr -new -sha256 -nodes -keyout privateKey.key -config openssl.cnf
//
// With openssl.cnf containing the following sections:
//
// [ v3_req ]
// basicConstraints = CA:FALSE
// keyUsage = nonRepudiation, digitalSignature, keyEncipherment
// subjectAltName = email:gopher@golang.org,DNS:test.example.com
// [ req_attributes ]
// challengePassword = ignored challenge
// unstructuredName = ignored unstructured name
var csrBase64Array = [...]string{
// Just [ v3_req ]
"MIIDHDCCAgQCAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaBZMFcGCSqGSIb3DQEJDjFKMEgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLgYDVR0RBCcwJYERZ29waGVyQGdvbGFuZy5vcmeCEHRlc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAB6VPMRrchvNW61Tokyq3ZvO6/NoGIbuwUn54q6l5VZW0Ep5Nq8juhegSSnaJ0jrovmUgKDN9vEo2KxuAtwG6udS6Ami3zP+hRd4k9Q8djJPb78nrjzWiindLK5Fps9U5mMoi1ER8ViveyAOTfnZt/jsKUaRsscY2FzE9t9/o5moE6LTcHUS4Ap1eheR+J72WOnQYn3cifYaemsA9MJuLko+kQ6xseqttbh9zjqd9fiCSh/LNkzos9c+mg2yMADitaZinAh+HZi50ooEbjaT3erNq9O6RqwJlgD00g6MQdoz9bTAryCUhCQfkIaepmQ7BxS0pqWNW3MMwfDwx/Snz6g=",
// Both [ v3_req ] and [ req_attributes ]
"MIIDaTCCAlECAQAwfjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLQ29tbW9uIE5hbWUxITAfBgkqhkiG9w0BCQEWEnRlc3RAZW1haWwuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1GY4YFx2ujlZEOJxQVYmsjUnLsd5nFVnNpLE4cV+77sgv9NPNlB8uhn3MXt5leD34rm/2BisCHOifPucYlSrszo2beuKhvwn4+2FxDmWtBEMu/QA16L5IvoOfYZm/gJTsPwKDqvaR0tTU67a9OtxwNTBMI56YKtmwd/o8d3hYv9cg+9ZGAZ/gKONcg/OWYx/XRh6bd0g8DMbCikpWgXKDsvvK1Nk+VtkDO1JxuBaj4Lz/p/MifTfnHoqHxWOWl4EaTs4Ychxsv34/rSj1KD1tJqorIv5Xv2aqv4sjxfbrYzX4kvS5SC1goIovLnhj5UjmQ3Qy8u65eow/LLWw+YFcCAwEAAaCBpTAgBgkqhkiG9w0BCQcxEwwRaWdub3JlZCBjaGFsbGVuZ2UwKAYJKoZIhvcNAQkCMRsMGWlnbm9yZWQgdW5zdHJ1Y3R1cmVkIG5hbWUwVwYJKoZIhvcNAQkOMUowSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAuBgNVHREEJzAlgRFnb3BoZXJAZ29sYW5nLm9yZ4IQdGVzdC5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAgxe2N5O48EMsYE7o0rZBB0wi3Ov5/yYfnmmVI22Y3sP6VXbLDW0+UWIeSccOhzUCcZ/G4qcrfhhx6gTZTeA01nP7TdTJURvWAH5iFqj9sQ0qnLq6nEcVHij3sG6M5+BxAIVClQBk6lTCzgphc835Fjj6qSLuJ20XHdL5UfUbiJxx299CHgyBRL+hBUIPfz8p+ZgamyAuDLfnj54zzcRVyLlrmMLNPZNll1Q70RxoU6uWvLH8wB8vQe3Q/guSGubLyLRTUQVPh+dw1L4t8MKFWfX/48jwRM4gIRHFHPeAAE9D9YAoqdIvj/iFm/eQ++7DP8MDwOZWsXeB6jjwHuLmkQ==",
}

View file

@ -297,7 +297,7 @@ func (svc *Service) CarveBlock(ctx context.Context, payload fleet.CarveBlockPayl
logging.WithExtras(ctx, "validate_carve_error", errRecord, "carve_id", carve.ID)
}
return ctxerr.Wrap(ctx, err, "validate carve block")
return ctxerr.Wrap(ctx, badRequest("validate carve block"), err.Error())
}
if err := svc.carveStore.NewBlock(ctx, carve, payload.BlockId, payload.Data); err != nil {

View file

@ -477,7 +477,7 @@ func TestCarveCarveBlockBlockCountExceedError(t *testing.T) {
assert.Contains(t, err.Error(), "block_id exceeds expected max")
}
func TestCarveCarveBlockBlockCountMatchError(t *testing.T) {
func TestCarveBlockCountMatchError(t *testing.T) {
sessionId := "foobar"
metadata := &fleet.CarveMetadata{
ID: 2,
@ -509,7 +509,8 @@ func TestCarveCarveBlockBlockCountMatchError(t *testing.T) {
}
err := svc.CarveBlock(context.Background(), payload)
require.Error(t, err)
var be *fleet.BadRequestError
require.ErrorAs(t, err, &be)
assert.Contains(t, err.Error(), "block_id does not match")
}

View file

@ -5367,7 +5367,6 @@ func (s *integrationTestSuite) TestGoogleCalendarIntegrations() {
}`, domain,
)), http.StatusUnprocessableEntity,
)
}
func (s *integrationTestSuite) TestQueriesBadRequests() {
@ -6866,7 +6865,7 @@ func (s *integrationTestSuite) TestCarve() {
SessionId: sid,
RequestId: "r1",
Data: []byte("p1."),
}, http.StatusInternalServerError, &blockResp) // TODO: should be 400, see #4406
}, http.StatusBadRequest, &blockResp)
checkCarveError(1, "block_id does not match expected block (0): 1")
// sending a block with valid payload, block 0
@ -6895,7 +6894,7 @@ func (s *integrationTestSuite) TestCarve() {
SessionId: sid,
RequestId: "r1",
Data: []byte("p2."),
}, http.StatusInternalServerError, &blockResp) // TODO: should be 400, see #4406
}, http.StatusBadRequest, &blockResp)
checkCarveError(1, "block_id does not match expected block (2): 1")
// sending final block with too many bytes
@ -6905,7 +6904,7 @@ func (s *integrationTestSuite) TestCarve() {
SessionId: sid,
RequestId: "r1",
Data: []byte("p3extra"),
}, http.StatusInternalServerError, &blockResp) // TODO: should be 400, see #4406
}, http.StatusBadRequest, &blockResp)
checkCarveError(1, "exceeded declared block size 3: 7")
// sending actual final block
@ -6925,7 +6924,7 @@ func (s *integrationTestSuite) TestCarve() {
SessionId: sid,
RequestId: "r1",
Data: []byte("p4."),
}, http.StatusInternalServerError, &blockResp) // TODO: should be 400, see #4406
}, http.StatusBadRequest, &blockResp)
checkCarveError(1, "block_id exceeds expected max (2): 3")
}

View file

@ -16,18 +16,21 @@ import (
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/ee/server/calendar"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/cron"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/log"
@ -48,7 +51,8 @@ func TestIntegrationsEnterprise(t *testing.T) {
type integrationEnterpriseTestSuite struct {
withServer
suite.Suite
redisPool fleet.RedisPool
redisPool fleet.RedisPool
calendarSchedule *schedule.Schedule
lq *live_query_mock.MockLiveQuery
}
@ -58,6 +62,7 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
s.redisPool = redistest.SetupRedis(s.T(), "integration_enterprise", false, false, false)
s.lq = live_query_mock.New(s.T())
var calendarSchedule *schedule.Schedule
config := TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
@ -67,6 +72,16 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
Lq: s.lq,
Logger: log.NewLogfmtLogger(os.Stdout),
EnableCachedDS: true,
StartCronSchedules: []TestNewScheduleFunc{
func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
return func() (fleet.CronSchedule, error) {
// We set 24-hour interval so that it only runs when triggered.
var err error
calendarSchedule, err = cron.NewCalendarSchedule(ctx, s.T().Name(), s.ds, 24*time.Hour, log.NewJSONLogger(os.Stdout))
return calendarSchedule, err
}
},
},
}
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
config.Logger = kitlog.NewNopLogger()
@ -76,6 +91,7 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
s.users = users
s.token = s.getTestAdminToken()
s.cachedTokens = make(map[string]string)
s.calendarSchedule = calendarSchedule
}
func (s *integrationEnterpriseTestSuite) TearDownTest() {
@ -3605,7 +3621,6 @@ func (s *integrationEnterpriseTestSuite) TestOSVersions() {
"GET", fmt.Sprintf("/api/latest/fleet/os_versions/%d", osinfo.OSVersionID), nil, http.StatusForbidden, &osVersionResp, "team_id",
"99999",
)
}
func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() {
@ -7336,7 +7351,8 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareAuth() {
Description: "desc team1",
})
require.NoError(t, err)
require.NoError(t, s.ds.AddHostsToTeam(ctx, &team1.ID, []uint{tmHost.ID}))
err = s.ds.AddHostsToTeam(ctx, &team1.ID, []uint{tmHost.ID})
require.NoError(t, err)
team2, err := s.ds.NewTeam(ctx, &fleet.Team{
ID: 43,
Name: "team2",
@ -7653,3 +7669,653 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareAuth() {
// set the admin token again to avoid breaking other tests
s.token = s.getTestAdminToken()
}
func (s *integrationEnterpriseTestSuite) TestCalendarEvents() {
ctx := context.Background()
t := s.T()
t.Cleanup(func() {
calendar.ClearMockEvents()
})
currentAppCfg, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
t.Cleanup(func() {
err = s.ds.SaveAppConfig(ctx, currentAppCfg)
require.NoError(t, err)
})
team1, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "team1",
})
require.NoError(t, err)
team2, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "team2",
})
require.NoError(t, err)
newHost := func(name string, teamID *uint) *fleet.Host {
h, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + name),
NodeKey: ptr.String(t.Name() + name),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%s.%s.local", name, t.Name()),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
return h
}
host1Team1 := newHost("host1", &team1.ID)
host2Team1 := newHost("host2", &team1.ID)
host3Team2 := newHost("host3", &team2.ID)
host4Team2 := newHost("host4", &team2.ID)
_ = newHost("host5", nil) // global host
team1Policy1Calendar, err := s.ds.NewTeamPolicy(
ctx, team1.ID, nil, fleet.PolicyPayload{
Name: "team1Policy1Calendar",
Query: "SELECT 1;",
CalendarEventsEnabled: true,
},
)
require.NoError(t, err)
team1Policy2, err := s.ds.NewTeamPolicy(
ctx, team1.ID, nil, fleet.PolicyPayload{
Name: "team1Policy2",
Query: "SELECT 2;",
CalendarEventsEnabled: true,
},
)
require.NoError(t, err)
team2Policy1Calendar, err := s.ds.NewTeamPolicy(
ctx, team1.ID, nil, fleet.PolicyPayload{
Name: "team2Policy1Calendar",
Query: "SELECT 3;",
CalendarEventsEnabled: true,
},
)
require.NoError(t, err)
team2Policy2, err := s.ds.NewTeamPolicy(
ctx, team1.ID, nil, fleet.PolicyPayload{
Name: "team2Policy2",
Query: "SELECT 4;",
CalendarEventsEnabled: false,
},
)
require.NoError(t, err)
globalPolicy, err := s.ds.NewGlobalPolicy(
ctx, nil, fleet.PolicyPayload{
Name: "globalPolicy",
Query: "SELECT 5;",
CalendarEventsEnabled: false,
},
)
require.NoError(t, err)
genDistributedReqWithPolicyResults := func(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
var (
results = make(map[string]json.RawMessage)
statuses = make(map[string]interface{})
messages = make(map[string]string)
)
for policyID, policyResult := range policyResults {
distributedQueryName := hostPolicyQueryPrefix + fmt.Sprint(policyID)
switch {
case policyResult == nil:
results[distributedQueryName] = json.RawMessage(`[]`)
statuses[distributedQueryName] = 1
messages[distributedQueryName] = "policy failed execution"
case *policyResult:
results[distributedQueryName] = json.RawMessage(`[{"1": "1"}]`)
statuses[distributedQueryName] = 0
case !*policyResult:
results[distributedQueryName] = json.RawMessage(`[]`)
statuses[distributedQueryName] = 0
}
}
return submitDistributedQueryResultsRequestShim{
NodeKey: *host.NodeKey,
Results: results,
Statuses: statuses,
Messages: messages,
Stats: map[string]*fleet.Stats{},
}
}
// host1Team1 is failing a calendar policy and not a non-calendar policy (no results for global).
distributedResp := submitDistributedQueryResultsResponse{}
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
team1Policy1Calendar.ID: ptr.Bool(false),
team1Policy2.ID: ptr.Bool(true),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// host2Team1 is passing the calendar policy but not the non-calendar policy (no results for global).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host2Team1,
map[uint]*bool{
team2Policy1Calendar.ID: ptr.Bool(true),
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// host3Team2 is passing team2Policy1Calendar and failing the global policy
// (not results for team2Policy2).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host3Team2,
map[uint]*bool{
team2Policy1Calendar.ID: ptr.Bool(true),
team2Policy2.ID: nil,
globalPolicy.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
// host4Team2 is not returning results for the calendar policy, failing the non-calendar
// policy and passing the global policy.
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host4Team2,
map[uint]*bool{
team2Policy1Calendar.ID: nil,
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: ptr.Bool(true),
},
), http.StatusOK, &distributedResp)
// Trigger the calendar cron with the global feature is disabled.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// No calendar events were created.
allCalendarEvents, err := s.ds.ListCalendarEvents(ctx, nil)
require.NoError(t, err)
require.Empty(t, allCalendarEvents)
// Set global configuration for the calendar feature.
appCfg, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
appCfg.Integrations.GoogleCalendar = []*fleet.GoogleCalendarIntegration{
{
Domain: "example.com",
ApiKey: map[string]string{
fleet.GoogleCalendarEmail: "calendar-mock@example.com",
},
},
}
err = s.ds.SaveAppConfig(ctx, appCfg)
require.NoError(t, err)
time.Sleep(2 * time.Second) // Wait 2 seconds for the app config cache to clear.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// No calendar events were created because we are missing enabling it on the teams.
allCalendarEvents, err = s.ds.ListCalendarEvents(ctx, nil)
require.NoError(t, err)
require.Empty(t, allCalendarEvents)
// Run distributed/write for host4Team2 again, it should not attempt to trigger the webhook because
// it's disabled for the teams.
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host4Team2,
map[uint]*bool{
team2Policy1Calendar.ID: nil,
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: ptr.Bool(true),
},
), http.StatusOK, &distributedResp)
var (
team1Fired int
team1FiredMu sync.Mutex
)
team1WebhookFired := make(chan struct{})
team1WebhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
requestBodyBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
t.Logf("team1 webhook request: %s\n", requestBodyBytes)
team1FiredMu.Lock()
team1Fired++
team1WebhookFired <- struct{}{}
team1FiredMu.Unlock()
}))
t.Cleanup(func() {
team1WebhookServer.Close()
})
team1.Config.Integrations.GoogleCalendar = &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: team1WebhookServer.URL,
}
team1, err = s.ds.SaveTeam(ctx, team1)
require.NoError(t, err)
var (
team2Fired int
team2FiredMu sync.Mutex
)
team2WebhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method)
requestBodyBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
t.Logf("team2 webhook request: %s\n", requestBodyBytes)
team2FiredMu.Lock()
team2Fired++
team2FiredMu.Unlock()
}))
t.Cleanup(func() {
team2WebhookServer.Close()
})
team2.Config.Integrations.GoogleCalendar = &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: team2WebhookServer.URL,
}
team2, err = s.ds.SaveTeam(ctx, team2)
require.NoError(t, err)
//
// Same distributed/write as before but they should not fire yet.
//
// host1Team1 is failing a calendar policy and not a non-calendar policy (no results for global).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
team1Policy1Calendar.ID: ptr.Bool(false),
team1Policy2.ID: ptr.Bool(true),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// host2Team1 is passing the calendar policy but not the non-calendar policy (no results for global).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host2Team1,
map[uint]*bool{
team2Policy1Calendar.ID: ptr.Bool(true),
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// host3Team2 is passing team2Policy1Calendar and failing the global policy
// (not results for team2Policy2).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host3Team2,
map[uint]*bool{
team2Policy1Calendar.ID: ptr.Bool(true),
team2Policy2.ID: nil,
globalPolicy.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
// host4Team2 is not returning results for the calendar policy, failing the non-calendar
// policy and passing the global policy.
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host4Team2,
map[uint]*bool{
team2Policy1Calendar.ID: nil,
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: ptr.Bool(true),
},
), http.StatusOK, &distributedResp)
team1FiredMu.Lock()
require.Zero(t, team1Fired)
team1FiredMu.Unlock()
team2FiredMu.Lock()
require.Zero(t, team2Fired)
team2FiredMu.Unlock()
// Trigger the calendar cron, global feature enabled, team1 enabled, team2 not yet enabled
// and hosts do not have an associated email yet.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
team1CalendarEvents, err := s.ds.ListCalendarEvents(ctx, &team1.ID)
require.NoError(t, err)
require.Empty(t, team1CalendarEvents)
// Add an email but of another domain.
err = s.ds.ReplaceHostDeviceMapping(ctx, host1Team1.ID, []*fleet.HostDeviceMapping{
{
HostID: host1Team1.ID,
Email: "user@other.com",
Source: "google_chrome_profiles",
},
}, "google_chrome_profiles")
require.NoError(t, err)
// Trigger the calendar cron, global feature enabled, team1 enabled, team2 not yet enabled
// and hosts do not have an associated email for the domain yet.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
team1CalendarEvents, err = s.ds.ListCalendarEvents(ctx, &team1.ID)
require.NoError(t, err)
require.Empty(t, team1CalendarEvents)
err = s.ds.ReplaceHostDeviceMapping(ctx, host1Team1.ID, []*fleet.HostDeviceMapping{
{
HostID: host1Team1.ID,
Email: "user1@example.com",
Source: "google_chrome_profiles",
},
}, "google_chrome_profiles")
require.NoError(t, err)
// Trigger the calendar cron, global feature enabled, team1 enabled, team2 not yet enabled
// and host1Team1 has a domain email associated.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// An event should be generated for host1Team1
team1CalendarEvents, err = s.ds.ListCalendarEvents(ctx, &team1.ID)
require.NoError(t, err)
require.Len(t, team1CalendarEvents, 1)
require.NotZero(t, team1CalendarEvents[0].ID)
require.Equal(t, "user1@example.com", team1CalendarEvents[0].Email)
require.NotZero(t, team1CalendarEvents[0].StartTime)
require.NotZero(t, team1CalendarEvents[0].EndTime)
calendar.SetMockEventsToNow()
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
// Update updated_at so the event gets updated (the event is updated every 30 minutes)
_, err := db.ExecContext(ctx,
`UPDATE calendar_events SET updated_at = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 HOUR) WHERE id = ?`, team1CalendarEvents[0].ID)
if err != nil {
return err
}
// Set host1Team1 as online.
if _, err := db.ExecContext(ctx,
`UPDATE host_seen_times SET seen_time = CURRENT_TIMESTAMP WHERE host_id = ?`, host1Team1.ID); err != nil {
return err
}
return nil
})
// Trigger the calendar cron, global feature enabled, team1 enabled, team2 not yet enabled
// and host1Team1 has a domain email associated.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// Check that refetch on the host was set.
host, err := s.ds.Host(ctx, host1Team1.ID)
require.NoError(t, err)
require.True(t, host.RefetchRequested)
// host1Team1 is failing a calendar policy and not a non-calendar policy (no results for global).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
team1Policy1Calendar.ID: ptr.Bool(false),
team1Policy2.ID: ptr.Bool(true),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// host2Team1 is passing the calendar policy but not the non-calendar policy (no results for global).
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host2Team1,
map[uint]*bool{
team2Policy1Calendar.ID: ptr.Bool(true),
team2Policy2.ID: ptr.Bool(false),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
select {
case <-team1WebhookFired:
case <-time.After(5 * time.Second):
t.Error("timeout waiting for team1 webhook to fire")
}
// Trigger again, nothing should fire as webhook has already fired.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
team1FiredMu.Lock()
require.Equal(t, 1, team1Fired)
team1FiredMu.Unlock()
team2FiredMu.Lock()
require.Equal(t, 0, team2Fired)
team2FiredMu.Unlock()
// Make host1Team1 pass all policies.
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1Team1,
map[uint]*bool{
team1Policy1Calendar.ID: ptr.Bool(true),
team1Policy2.ID: ptr.Bool(true),
globalPolicy.ID: nil,
},
), http.StatusOK, &distributedResp)
// Trigger calendar should cleanup the events.
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// Events in the user calendar should not be cleaned up because they are not in the future.
mockEvents := calendar.ListGoogleMockEvents()
require.NotEmpty(t, mockEvents)
// Event should be cleaned up from our database.
team1CalendarEvents, err = s.ds.ListCalendarEvents(ctx, &team1.ID)
require.NoError(t, err)
require.Empty(t, team1CalendarEvents)
}
func (s *integrationEnterpriseTestSuite) TestCalendarEventsTransferringHosts() {
ctx := context.Background()
t := s.T()
t.Cleanup(func() {
calendar.ClearMockEvents()
})
currentAppCfg, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
t.Cleanup(func() {
err = s.ds.SaveAppConfig(ctx, currentAppCfg)
require.NoError(t, err)
})
// Set global configuration for the calendar feature.
appCfg, err := s.ds.AppConfig(ctx)
require.NoError(t, err)
appCfg.Integrations.GoogleCalendar = []*fleet.GoogleCalendarIntegration{
{
Domain: "example.com",
ApiKey: map[string]string{
fleet.GoogleCalendarEmail: "calendar-mock@example.com",
},
},
}
err = s.ds.SaveAppConfig(ctx, appCfg)
require.NoError(t, err)
time.Sleep(2 * time.Second) // Wait 2 seconds for the app config cache to clear.
team1, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "team1",
})
require.NoError(t, err)
team2, err := s.ds.NewTeam(ctx, &fleet.Team{
Name: "team2",
})
require.NoError(t, err)
team1.Config.Integrations.GoogleCalendar = &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: "https://foo.example.com",
}
team1, err = s.ds.SaveTeam(ctx, team1)
require.NoError(t, err)
team2.Config.Integrations.GoogleCalendar = &fleet.TeamGoogleCalendarIntegration{
Enable: true,
WebhookURL: "https://foo.example.com",
}
team2, err = s.ds.SaveTeam(ctx, team2)
require.NoError(t, err)
newHost := func(name string, teamID *uint) *fleet.Host {
h, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + name),
NodeKey: ptr.String(t.Name() + name),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%s.%s.local", name, t.Name()),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
return h
}
host1 := newHost("host1", &team1.ID)
err = s.ds.ReplaceHostDeviceMapping(ctx, host1.ID, []*fleet.HostDeviceMapping{
{
HostID: host1.ID,
Email: "user1@example.com",
Source: "google_chrome_profiles",
},
}, "google_chrome_profiles")
require.NoError(t, err)
team1Policy1, err := s.ds.NewTeamPolicy(
ctx, team1.ID, nil, fleet.PolicyPayload{
Name: "team1Policy1",
Query: "SELECT 1;",
CalendarEventsEnabled: true,
},
)
require.NoError(t, err)
team2Policy1, err := s.ds.NewTeamPolicy(
ctx, team2.ID, nil, fleet.PolicyPayload{
Name: "team2Policy1",
Query: "SELECT 2;",
CalendarEventsEnabled: true,
},
)
require.NoError(t, err)
distributedResp := submitDistributedQueryResultsResponse{}
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1,
map[uint]*bool{
team1Policy1.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
team1CalendarEvents, err := s.ds.ListCalendarEvents(ctx, &team1.ID)
require.NoError(t, err)
require.Len(t, team1CalendarEvents, 1)
// Check the calendar was created on the DB.
hostCalendarEvent, calendarEvent, err := s.ds.GetHostCalendarEventByEmail(ctx, "user1@example.com")
require.NoError(t, err)
// Transfer host to team2.
err = s.ds.AddHostsToTeam(ctx, &team2.ID, []uint{host1.ID})
require.NoError(t, err)
// host1 is failing team2's policy too.
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
host1,
map[uint]*bool{
team2Policy1.ID: ptr.Bool(false),
},
), http.StatusOK, &distributedResp)
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// Check the calendar event entry was reused.
hostCalendarEvent2, calendarEvent2, err := s.ds.GetHostCalendarEventByEmail(ctx, "user1@example.com")
require.NoError(t, err)
require.Equal(t, calendarEvent2.ID, calendarEvent.ID)
require.Equal(t, hostCalendarEvent2.CalendarEventID, hostCalendarEvent.CalendarEventID)
// Transfer host to global.
err = s.ds.AddHostsToTeam(ctx, nil, []uint{host1.ID})
require.NoError(t, err)
// Move event to two days ago (to clean up the calendar event)
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx,
`UPDATE calendar_events SET updated_at = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 49 HOUR) WHERE id = ?`, team1CalendarEvents[0].ID)
if err != nil {
return err
}
return nil
})
triggerAndWait(ctx, t, s.ds, s.calendarSchedule, 5*time.Second)
// Calendar event is cleaned up.
_, _, err = s.ds.GetHostCalendarEventByEmail(ctx, "user1@example.com")
require.True(t, fleet.IsNotFound(err))
}
func genDistributedReqWithPolicyResults(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
var (
results = make(map[string]json.RawMessage)
statuses = make(map[string]interface{})
messages = make(map[string]string)
)
for policyID, policyResult := range policyResults {
distributedQueryName := hostPolicyQueryPrefix + fmt.Sprint(policyID)
switch {
case policyResult == nil:
results[distributedQueryName] = json.RawMessage(`[]`)
statuses[distributedQueryName] = 1
messages[distributedQueryName] = "policy failed execution"
case *policyResult:
results[distributedQueryName] = json.RawMessage(`[{"1": "1"}]`)
statuses[distributedQueryName] = 0
case !*policyResult:
results[distributedQueryName] = json.RawMessage(`[]`)
statuses[distributedQueryName] = 0
}
}
return submitDistributedQueryResultsRequestShim{
NodeKey: *host.NodeKey,
Results: results,
Statuses: statuses,
Messages: messages,
Stats: map[string]*fleet.Stats{},
}
}
func triggerAndWait(ctx context.Context, t *testing.T, ds fleet.Datastore, s *schedule.Schedule, timeout time.Duration) {
// Following code assumes (for simplicity) only triggered runs.
stats, err := ds.GetLatestCronStats(ctx, s.Name())
require.NoError(t, err)
var previousRunID int
if len(stats) > 0 {
previousRunID = stats[0].ID
}
_, err = s.Trigger()
require.NoError(t, err)
timeoutCh := time.After(timeout)
for {
stats, err := ds.GetLatestCronStats(ctx, s.Name())
require.NoError(t, err)
if len(stats) > 0 && stats[0].ID > previousRunID && stats[0].Status == fleet.CronStatsStatusCompleted {
t.Logf("cron %s:%d done", s.Name(), stats[0].ID)
return
}
select {
case <-timeoutCh:
t.Fatalf("timeout waiting for schedule %s to complete", s.Name())
case <-time.After(250 * time.Millisecond):
}
}
}

Some files were not shown because too many files have changed in this diff Show more