= {
"Red Hat Linux": "platform",
"Ubuntu Linux": "platform",
chrome: "platform",
+} as const;
+
+export const PLATFORM_TYPE_ICONS: Record<
+ Extract<
+ PlatformLabelNameFromAPI,
+ "All Linux" | "macOS" | "MS Windows" | "chrome"
+ >,
+ IconNames
+> = {
+ "All Linux": "linux",
+ macOS: "darwin",
+ "MS Windows": "windows",
+ chrome: "chrome",
+} as const;
+
+export const hasPlatformTypeIcon = (
+ s: string
+): s is Extract<
+ PlatformLabelNameFromAPI,
+ "All Linux" | "macOS" | "MS Windows" | "chrome"
+> => {
+ return !!PLATFORM_TYPE_ICONS[s as keyof typeof PLATFORM_TYPE_ICONS];
};
interface IPlatformDropdownOptions {
diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx
index 14bfe56640..e18ee526ec 100644
--- a/frontend/utilities/helpers.tsx
+++ b/frontend/utilities/helpers.tsx
@@ -51,6 +51,7 @@ import {
DEFAULT_GRAVATAR_LINK_DARK_FALLBACK,
INITIAL_FLEET_DATE,
PLATFORM_LABEL_DISPLAY_TYPES,
+ isPlatformLabelNameFromAPI,
} from "utilities/constants";
import { IScheduledQueryStats } from "interfaces/scheduled_query_stats";
import { IDropdownOption } from "interfaces/dropdownOption";
@@ -220,10 +221,14 @@ export const formatFloatAsPercentage = (float?: number): string => {
const formatLabelResponse = (response: any): ILabel[] => {
const labels = response.labels.map((label: ILabel) => {
+ let labelType = "custom";
+ if (isPlatformLabelNameFromAPI(label.display_text)) {
+ labelType = PLATFORM_LABEL_DISPLAY_TYPES[label.display_text];
+ }
return {
...label,
slug: labelSlug(label),
- type: PLATFORM_LABEL_DISPLAY_TYPES[label.display_text] || "custom",
+ type: labelType,
target_type: "labels",
};
});
diff --git a/handbook/business-operations/README.md b/handbook/business-operations/README.md
index 4fc8c9bc5a..eeebe9fbc7 100644
--- a/handbook/business-operations/README.md
+++ b/handbook/business-operations/README.md
@@ -129,6 +129,7 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid
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`
@@ -376,6 +377,36 @@ Article creation begins with creation of an issue using the "Article request" te
Check the "📃 Planned articles" column in [#g-demand board](https://app.zenhub.com/workspaces/g-demand-64e6c8e2d35c7f001a457b7f/board) and continue to work through steps in each event's issue.
-->
+### Order SWAG
+
+**To order T-shirts:**
+
+ - Check [Postal](https://app.postal.io/items/postals) first and see if the warehouse has enough shirts.
+ - Navigate to the [approved items page](https://app.postal.io/items/postals).
+ - Hover over the shirt design and click on the airplane.
+ - Click bulk send and choose one shirt size and the expected quantity of that particular shirt size.
+ - Make sure the address matches the expected receiving address.
+ - If the Postal warehouse can't fulfill the order or To order swag quickly:
+ - Login to [https://www.rushordertees.com/my-account/login/) (saved in 1Password).
+ - Choose Fleet logo design t-shirt under [my designs](https://www.rushordertees.com/my-account/designs/).
+ - Order shirts based on the pre-determined number (~5% of total event attendees).
+ - Submit the order. Ensure the address matches the expected receiving address.
+
+**To order stickers:**
+
+ - Login to [StickerMule](https://www.stickermule.com/) (saved in 1Password).
+ - Find the [brand kit](https://www.stickermule.com/studio/brand-kits) after logging in.
+ - Click on the "Fleet Device Management" brand kit and order preapproved stickers from the templates.
+ - Total sticker quantity should be ~10% of total event attendees.
+ - Complete the checkout process. Ensure the address matches the expected receiving address.
+
+**To order pens and sticky note pads**
+
+ - Pens and sticky note pads are ordered through Everything Branded.
+ - Email our sales representative Jake William (saved in 1Password) to order any of the following:
+ - [Javalina™ Metallic Stylus Pen](https://www.everythingbranded.com/product/javalina-metallic-stylus-pen-us-pat-8847930-9092077-350220)
+ - [Sharpie Fine Point Markers](https://www.everythingbranded.com/product/sharpie-fine-point-332908)
+ - [Custom sticky note pads](https://www.everythingbranded.com/product/custom-sticky-notes-585601) (design is in the StickerMule [brand kit](https://www.stickermule.com/studio/brand-kits))
## Rituals
diff --git a/handbook/company/README.md b/handbook/company/README.md
index ed5b339d5f..05bbe2f238 100644
--- a/handbook/company/README.md
+++ b/handbook/company/README.md
@@ -2,7 +2,7 @@
## Purpose
-Fleet Device Management Inc is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community).
+Fleet is an [open-core company](https://fleetdm.com/handbook/company/why-this-way#why-open-source) that sells subscriptions that offer [more features and support](https://fleetdm.com/pricing) for Fleet and osquery, the leading open-source systems management platform and security agent. Today, Fleet enrolls millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linkedin.com/feed/update/urn:li:activity:7120880290859728897/).
Fleet is dedicated to a comprehensive strategy against [whatever this is](https://chat.openai.com/share/e44ba6f3-b3ed-488a-a15e-a5a723f20c98):
@@ -113,13 +113,13 @@ Ever wonder why there are 6 circles in the Fleet logo, but only 5 values? Behol
## History
### 2014: Origins of osquery
-In 2014, our Cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io).
+In 2014, our cofounder Zach Wasserman, together with [Mike Arpaia](https://twitter.com/mikearpaia/status/1357455391588839424) and the rest of their team at Facebook, created an open source project called [osquery](https://osquery.io).
### 2016: Origins of Fleet v1.0
A few years later, Zach, Mike Arpaia, and [Jason Meller](https://honest.security) founded [Kolide](https://kolide.com) and created Fleet: an open source platform that made it easier and more productive to use osquery in an enterprise setting.
### 2019: The growing community
-When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community took over maintenance of the open source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform.
+When Kolide's attention shifted away from Fleet, and towards their separate, user-focused SaaS offering, the Fleet community [took over maintenance](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community) of the open-source project. After his time at Kolide, Zach continued as lead maintainer of Fleet. He spent 2019 consulting and working with the growing open source community to support and extend the capabilities of the Fleet platform.
### 2020: Fleet was incorporated
Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/leadership#ceo-flaws), to found a new, independent company: Fleet Device Management Inc. In November 2020, we [announced](https://medium.com/fleetdm/a-new-fleet-d4096c7de978) the transition and kicked off the logistics of moving the GitHub repository.
@@ -127,8 +127,10 @@ Zach partnered with our [CEO, Mike McNeil](https://fleetdm.com/handbook/company/
### 2022: Millions of hosts
Fleet raised its Series A funding round. The world now has at least 1.65 million computers and virtual hosts enrolled in Fleet, including enterprises, governments, startups, families, and hobbyist racks all over the world.
-### 2024: Your last MDM migration
-Fleet announces [support for Windows and Linux devices](https://fleetdm.com/announcements/fleet-introduces-windows-mdm), enabling IT departments to consolidate tools and implement “zero trust” faster using a modern Mac-first MDM. Removing the need for proprietary alternatives like Jamf Pro, Jamf Protect, Microsoft Intune, Ivanti MobileIron, and Broadcom's recently acquired Workspace ONE (originally known as "Airwatch").
+### 2023: Your last MDM migration
+Fleet added support for [scripting and management capabilities](https://fleetdm.com/announcements/fleet-introduces-windows-mdm) on macOS, Windows, _and_ Linux devices, allowing IT departments to manage devices more consistently using modern tooling and best practices. This allowed many customers to simplify their management practices. In several cases, Fleet was also able to save customers several hundreds of thousands of dollars (USD) by cutting tool overlap across platforms such as Jamf, Airwatch, Intune, MobileIron, Nexthink, Tanium, Uptycs, and Rapid7.
+
+
> Still curious? Check out this [visualization of the Fleet repo over the years](https://www.linkedin.com/feed/update/urn:li:activity:7045068060168220672/) or listen to this [conversation between Zach and Mike Arpaia about the origin story of osquery](https://fleetdm.com/podcasts/the-future-of-device-management-ep1).
diff --git a/handbook/company/communications.md b/handbook/company/communications.md
index 72df3299c7..0285a14c3a 100644
--- a/handbook/company/communications.md
+++ b/handbook/company/communications.md
@@ -118,7 +118,7 @@ All invitations to meetings are welcomed, and quickly considering them is a top
> **Note:** Please do not add events to the CEO's calendar. **Events added directly to the CEO's calendar will be declined and removed.** Even if the CEO asks you to set up a meeting or add him to a call, please get scheduling help from the [Apprentice](https://www.fleetdm.com/handbook/digital-experience#team)).
-To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BNAME%7D%C2%BB______________________). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved.
+To request time with the CEO, [submit an issue](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-digital-experience&projects=&template=custom-request.md&title=%7BMeeting%20request%3A%20) at-mentioning the [Head of Digital Experience](https://www.fleetdm.com/handbook/digital-experience#team). Internal meetings can sometimes be moved to make room. External meetings, blocked time, travel, and personal commitments can rarely be moved.
- **Why the extra step?** There are not enough hours in the day for the CEO to accept every request to meet, so [we have to prioritize](https://www.fleetdm.com/handbook/digital-experience#process-the-ceos-calendar).
- **Self-service scheduling:** Unlike other team members, who you can schedule with by simply dropping an event on their calendar unless requested directly from Mike, please do not directly schedule a meeting onto the CEO's calendar without using this process to confirm with the Apprentice first.
@@ -130,16 +130,28 @@ This works because every Fleetie grants edit access to everyone else at Fleet as
### Shared calendars
Team calendars are the primary source for sprint rituals; they facilitate the execution of each sprint.
-
Looking to add, change, or remove a shared calendar? [Create an issue for the CEO](https://fleetdm.com/handbook/digital-experience#contact-us) and the appropriate DRI will reply with feedback.
+## Skip-level 1:1 meetings
+
+Fleet uses skip-level 1:1 meetings as a recurring pulse check to encourage [valuable personal and departmental feedback](https://fleetdm.com/handbook/company/communications#performance-feedback) across the org. This helps the leadership at Fleet run an effective company with a great team, good alignment, and quick decisions. To schedule a skip-Level 1:1:
+1. Create a copy of the ["Skip-level 1:1 agenda template"](https://docs.google.com/document/d/191wiy-_a9XBMndLlM97iOwUF6a-0PtkbboQ2FCUIy6w/copy) and rename the document "🧑🚀 YOUR_GITHUB_USER_NAME : SUPERVISOR_GITHUB_USER_NAME".
+2. [Schedule a meeting](https://fleetdm.com/handbook/company/communications#internal-meeting-scheduling) with your manager's supervisor and title the calendar event by copying your skip-level agenda title and appending "[no shadows]" to the end (this tells other team members that this is a private conversation).
+
+> **If you're scheduling with the CEO** please [get help](https://fleetdm.com/handbook/company/communications#schedule-time-with-the-ceo) before adding events to the calendar.
+
+3. Link the skip-level agenda in the calendar event description before saving.
+
+
### Zoom
+
We use [Zoom](https://zoom.us) for virtual meetings at Fleet, and it is important that every team member feels comfortable hosting, joining, and scheduling Zoom meetings.
By default, Zoom settings are the same for all Fleet team members, but you can change your personal settings on your [profile settings](https://zoom.us/profile/setting) page.
Settings that have a lock icon next to them have been locked by an administrator and cannot be changed. Zoom administrators can change settings for all team members on the [account settings page](https://zoom.us/account/setting) or for individual accounts on the [user management page](https://zoom.us/account/user#/).
### Recording meetings
+
Capturing video from meetings with customers, prospects, and community members outside the company is an important part of building world-class sales and customer success teams and is a widespread practice across the industry. At Fleet, we use Gong to capture Zoom meetings and share them company-wide. If a team member with a Gong license attends certain meetings, generally those with at least one person from outside of Fleet in attendance.
- While some Fleeties may have a Gong seat that is necessary in their work, the typical use case at Fleet is for employees on the company's sales, customer success, or customer support teams.
- You should be notified anytime you join a recorded call with an audio message announcing "this meeting is being recorded" or "recording in progress." To stop a recording, the host of the call can press "Stop."
diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md
index 7e333b583e..6daab45283 100644
--- a/handbook/company/leadership.md
+++ b/handbook/company/leadership.md
@@ -166,23 +166,25 @@ If the consultant is international, you will also provide:
> To update a consultant's fee, [submit an issue to BizOps](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-business-operations&projects=&title=Update%20consultant%20fee) with the consultant's name and new hourly rate.
-### Advisor
+### Adding an advisor
-#### Adding an advisor
-Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement"
-template.
-- Send the advisor agreement. To send a new advisor agreement, you'll need the new advisor's name and the number of shares they are offered.
-- Once you send the agreement, locate an existing empty row and available ID in ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) and enter the new advisor's information.
- >**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"*
+First:
-#### Finalizing a new advisor
-- Update the ["Advisors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm).
-- Update "Equity plan" to reflect updated status and equity grant for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual.
+Advisor agreements are sent through [DocuSign](https://www.docusign.com/), using the "Advisor Agreement" template.
+- Update the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483)
+ >**_Note:_** *Be sure to mark any columns that haven't been completed yet as "TODO"*
+- Update the "Equity plan" sheet (which should have been automatically updated after updating "Advisors" thanks to the global unique IDs next to each row which are used to connect the spreadsheets) to reflect the default number of shares for advisor equity grants.
+- Send the advisor agreement [through Docusign](https://apps.docusign.com/send/templates?view=shared&folder=0482b0fd-a752-41be-93a0-185e2fb7ef54) using the CEO's account, pulling the advisor's email address from a recent calendar event on the CEO's calendar.
+- Complete the first step of signing, which involves filling in the number of shares.
+- Then wait for the advisor to sign. (Fleet's CEO will sign after that.)
-### Core team member
-This section is about creating a core team member role, and the hiring process for a new core team member, or Fleetie.
+Then, to finalize a new advisor after signing is complete:
+- Schedule quarterly recurring 1h meeting between the CEO and the advisor, with 30m of recurring prep scheduled back to back ahead of the meeting.
+- Update the status columns in the ["Advisors" sheet](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1803674483) to show that the agreement has been signed, and ask the new advisor to add us on [LinkedIn](https://www.linkedin.com/company/71111416), [Crunchbase](https://www.crunchbase.com/organization/fleet-device-management), and [Angellist](https://angel.co/company/fleetdm).
+- Update "Equity plan" status columns to reflect updated status for this advisor, and to ensure the advisor's equity is queued up for the next quarterly equity grant ritual.
-#### Creating a new position
+
+### Creating a new position
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:
@@ -236,7 +238,7 @@ A completed open position entry should look something like this:
- _**Why bother with approvals?** We avoid cancelling or significantly changing a role after opening it. It hurts candidates too much. Instead, get the position approved first, before you start recruiting and interviewing. This gives you a sounding board and avoids misunderstandings._
-#### Approving a new position
+### Approving a new position
When review is requested on a proposal to open a new position, the 🐈⬛ CEO will complete the following steps when reviewing the pull request:
1. **Consider role and reporting structure:** Confirm the new row in "Fleeties" has a manager, job title, and department, that it doesn't have any corrupted spreadsheet formulas or formatting, and that the start date is set to the first Monday of the next month.
@@ -257,7 +259,7 @@ When review is requested on a proposal to open a new position, the 🐈⬛ CE
> _**Note:** Most columns of the "Equity plan" are updated automatically when "Fleeties" is, based on the unique identifier of each row, like `🧑🚀890`. (Advisors have their own flavor of unique IDs, such as `🦉755`, which are defined in ["Advisors and investors"](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit).)_
-#### Recruiting
+### Recruiting
Fleet accepts job applications, but the company does not list positions on general purpose job boards. This prevents us being overwhelmed with candidates so we can fulfill our goal of responding promptly to every applicant.
This means that outbound recruiting, 3rd party recruiters, and references from team members are important aspect of the company's hiring strategy. Fleet's CEO is happy to assist with outreach, intros, and recruiting strategy for candidates.
@@ -272,15 +274,16 @@ When a candidate clicks applies for a job at Fleet, they are taken to a generic
#### Candidate correspondence email templates
Fleet uses [certain email templates](https://docs.google.com/document/d/1E_gTunZBMNF4AhsOFuDVi9EnvsIGbAYrmmEzdGmnc9U) when responding to candidates. This helps us live our value of [🔴 empathy](https://fleetdm.com/handbook/company#empathy) and helps the company meet the aspiration of replying to all applications within one business day.
-#### Hiring restrictions
+### Hiring restrictions
-##### Incompatible former employers
+#### Incompatible former employers
Fleet maintains a list of companies with whom Fleet has do-not-solicit terms that prevents us from making offers to employees of these companies. The list is in the Do Not Solicit tab of the [BizOps spreadsheet](https://docs.google.com/spreadsheets/d/1lp3OugxfPfMjAgQWRi_rbyL_3opILq-duHmlng_pwyo/edit#gid=0).
-##### Incompatible locations
+#### Incompatible locations
Fleet is unable to hire team members in some countries. See [this internal document](https://docs.google.com/document/d/1jHHJqShIyvlVwzx1C-FB9GC74Di_Rfdgmhpai1SPC0g/edit) for the list.
-#### Interviewing
+
+### Interviewing
> TODO: Rewrite this section for the hiring manager as our audience.
We're glad you're interested in joining the team!
@@ -302,7 +305,7 @@ Here are the steps hiring managers follow to get an offer out to a candidate:
1. **Call references:** Before proceeding, make sure you have 2-5+ references. Ask the candidate for at least 2-5+ references and contact each reference in parallel using the instructions in [Fleet's reference check template](https://docs.google.com/document/d/1LMOUkLJlAohuFykdgxTPL0RjAQxWkypzEYP_AT-bUAw/edit?usp=sharing). Be respectful and keep these calls very short.
2. **Add to team database:** Update the [Fleeties](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) doc to accurately reflect the candidate's:
- Start date
- > _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._
+> _**Tip:** No need to check with the candidate if you haven't already. Just guess. First Mondays tend to make good start dates. When hiring an international employee, Pilot.co recommends starting the hiring process a month before the new employee's start date._
- First and last name
- Preferred pronoun _("them", "her", or "him")_
- LinkedIn URL _(If the fleetie does not have a LinkedIn account, enter `N/A`)_
@@ -311,11 +314,10 @@ Here are the steps hiring managers follow to get an offer out to a candidate:
4. **Confirm intent to offer:** Compile feedback about the candidate into a single document and share that document (the "interview packet") with the Head of Business Operations via Google Drive. _This will be interpreted as a signal that you are ready for them to make an offer to this candidate._
- _Compile feedback into a single doc:_ Include feedback from interviews, reference checks, and challenge submissions. Include any other notes you can think of offhand, and embed links to any supporting documents that were impactful in your final decision-making, such as portfolios or challenge submissions.
- _Share_ this single document with the Head of Business Operations via email.
- - Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line. For example, you could title it:
- >Why hire Jane Doe ("Train Conductor") - 2023-03-21
- - When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate.
+ - Share only _one, single Google Doc, please_; with a short, formulaic name that's easy to understand in an instant from just an email subject line (e.g. "_Why hire Jane Doe ("Train Conductor") - 2023-03-21_").
+ - When the Head of Business Operations receives this doc shared doc in their email with the compiled feedback about the candidate, they will understand that to mean that it is time for Fleet to make an offer to the candidate.
-#### Making an offer
+### Making an offer
After receiving the interview packet, the Head of Business Operations uses the following steps to make an offer:
@@ -399,12 +401,19 @@ From time to time, someone's job title changes. To do this, Business Operations
2. If there is a compensation change, update "Equity plan". Use the first day of a month as the date, and enter this in the corresponding column.
3. If applicable, schedule the change in the appropriate payroll system. (Don't worry about updating job titles in the payroll system.)
-## Performance feedback
+## Delivering performance feedback
When it comes to performance feedback, [speak freely](https://fleetdm.com/handbook/company#openness), sooner, and provide an explicit example of the behavior you observed and the impact it had.
1. Deliver negative feedback privately whenever possible, and be constructive not punitive. Celebrate positive feedback publicly.
-2. Performance mangement is a part of every 1:1 document. Start each 1:1 by delivering performance feedback.
-3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc.
+2. Performance management is a part of every 1:1 document. Start each 1:1 by delivering performance feedback.
+3. When you meet with your manager for your 1:1, periodically provide an update on how each of your direct reports is doing at the top of your own "Performance management" section in your 1:1 agenda doc.
+
+
+#### Stubs
+
+##### Performance feedback
+Please see 📖[handbook/company/leadership#delivering-performance-feedback](https://fleetdm.com/handbook/company/leadership#delivering-performance-feedback).
+
diff --git a/handbook/company/open-positions.yml b/handbook/company/open-positions.yml
index fcc3a2e43b..c10b8858be 100644
--- a/handbook/company/open-positions.yml
+++ b/handbook/company/open-positions.yml
@@ -87,6 +87,6 @@
- ✍️ 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.
+ - ➕ Bonus: Familiarity with osquery, MySQL, GitOps workflows, Terraform, Tines/Torq and open source projects. Experience working with IT, SRE, CPE, or SecOps teams.
diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md
index 506831d769..d85d80eeb7 100644
--- a/handbook/company/product-groups.md
+++ b/handbook/company/product-groups.md
@@ -273,7 +273,7 @@ All unreleased bugs are addressed before publishing a release. Released bugs tha
- Introduces a security vulnerability
### Notify the community about a critical bug
-We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the #help-product-design channel with the filed bug.
+We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the `#g-mdm` or `#g-endpoint-ops` channel with the filed bug.
If the bug finder is not a Fleetie (e.g., a member of the community), then whoever sees the critical bug should raise the alarm. Note that the bug finder here is NOT necessarily the **first** person who sees the bug. If you come across a bug you think is critical, but it has not been escalated, raise the alarm!
diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md
index 010680a7b3..5fe726ce66 100644
--- a/handbook/company/why-this-way.md
+++ b/handbook/company/why-this-way.md
@@ -117,6 +117,9 @@ The only exceptions are:
- _Confidential:_ [`fleetdm/confidential`](https://github.com/fleetdm/confidential)
- _Classified (¶¶):_ [`fleetdm/classified`](https://github.com/fleetdm/classified)
3. **GitHub Actions:** Since GitHub requires GitHub Actions to live in dedicated repositories in order to submit them to the marketplace, Fleet uses a separate repo for publishing [GitHub Actions designed for other people to deploy and use (and/or fork)](https://github.com/fleetdm/fleet-mdm-gitops).
+4. **Software vulnerabilities:** Since GitHub only allows one latest release per repository, we currently maintain two repositories to host our CVE/CPE database releases:
+ - _vulnerabilities:_ [`fleetdm/vulnerabilities`](https://github.com/fleetdm/vulnerabilities)
+ - _nvd:_ [`fleetdm/nvd`](https://github.com/fleetdm/nvd)
Besides the exceptions above, Fleet does not use any other repositories. Other GitHub repositories in `fleetdm` should be archived and made private.
diff --git a/handbook/customer-success/README.md b/handbook/customer-success/README.md
index 9f8a266164..2f96814de7 100644
--- a/handbook/customer-success/README.md
+++ b/handbook/customer-success/README.md
@@ -18,6 +18,7 @@ This handbook page details processes specific to working [with](#contact-us) and
## Responsibilities
The customer success department is directly responsible for ensuring that customers and community members of Fleet achieve their desired outcomes with Fleet products and services.
+
### Assign a customer codename
Occasionally, we will need to track public issues for customers who wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer.
@@ -26,7 +27,7 @@ Locate the relevant issue or create it if it doesn't already exist (to avoid dup
- Make sure the issue has a "customer request" label or "customer-codename" label.
- Occasionally, we will need to track public issues for customers that wish to remain anonymous on our public issue tracker. To do this, we choose an appropriate minor planet name from this [Wikipedia page](https://en.wikipedia.org/wiki/List_of_named_minor_planets_(alphabetical)) and create a label which we attach to the issue and any future issues for this customer.
- "+" prefixed labels (e.g., "+more info please") indicate we are waiting on an answer from an external community member who does not work at Fleet or that no further action is needed from the Fleet team until an external community member, who doesn't work at Fleet, replies with a comment. At this point, our bot will automatically remove the +-prefixed label.
-- 1. Required details that will help speed up time to resolution:
+1. Required details that will help speed up time to resolution:
- Fleet server version
- Agent version
- Osquery or fleetd?
diff --git a/handbook/customer-success/customer-success.rituals.yml b/handbook/customer-success/customer-success.rituals.yml
index 27046f38f2..017063ae96 100644
--- a/handbook/customer-success/customer-success.rituals.yml
+++ b/handbook/customer-success/customer-success.rituals.yml
@@ -49,14 +49,14 @@
frequency: "Triweekly"
description: "Check-in before the 🗣️ Product Feature Requests meeting to make sure that all information necessary has been gathered before presenting customer requests and feedback to the Product team."
moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more."
- dri: "patagonia121"
+ dri: "nonpunctual"
-
task: "Present customer requests at feature fest"
startedOn: "2024-02-15"
frequency: "Triweekly"
description: "Present and advocate for requests and ideas brought to Fleet's attention by customers that are interesting from a product perspective."
moreInfoUrl: "" # TODO: add responsibility on customer-success readme starting point == "Prepare and review the health and latest updates from Fleet's key customers and active proof of concepts (POCs), plus other active support items related to community support, community engagement efforts, contact form or chat requests, self-service customers, outages, and more."
- dri: "patagonia121"
+ dri: "nonpunctual"
-
task: "Communicate release notes to stakeholders"
startedOn: "2024-02-21"
diff --git a/handbook/demand/README.md b/handbook/demand/README.md
index 74744dbbf4..2360016984 100644
--- a/handbook/demand/README.md
+++ b/handbook/demand/README.md
@@ -23,7 +23,7 @@ The Demand department is directly responsible for growing awareness of Fleet and
1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions.
2. Mark submission as seen with the "👀" emoji.
3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks.
-4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in [#help-solutions-consulting](https://fleetdm.slack.com/archives/C05HZ2LHEL8). If an SC is unavailable, post in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5) or [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) and notify @on-call.
+4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call.
5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead.
6. Enrich each lead with company information and buying situation.
7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Revenue Operations](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC.
diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md
index 4463438ce7..f974034833 100644
--- a/handbook/digital-experience/README.md
+++ b/handbook/digital-experience/README.md
@@ -167,12 +167,50 @@ If the action fails, please complete the following steps:
3. Head to the fleetdm/fleet GitHub repository and re-run the Deploy Fleet Website action.
+### Communicate Fleet's potential energy to stakeholders
+On the first business day of every month, the Apprentice will send an update to the stakeholders of Fleet using the following steps:
+1. Copy the following template into an outgoing email with the subject line: "[Investor update] Fleet, YYYY-MM".
+
+```
+Hi investors and friends,
+
+Here’s a quick update on the numbers from last month:
+
+• Gross new ∆ARR (QTD): + TODO
+• Social media mentions (LinkedIn): 3.8 per day (Goal: 5) (Want to help?)
+• Current version: 4.48.0 (See what's new)
+• Next in-person event: Kansas City, (April 20) BSides KC
+• Next press release: 2024-04-30: "Stop nudging"
+"Stop installing updates and forcing restarts when your users are busy using their computers. Fleet finds time in the calendar for a reboot and uses AI to explain why."
+
+
+Thanks for your support,
+Mike and the Fleet team
+```
+
+2. Address the email to the executive team's Gmail.
+3. Using the [🌧️🦉 Investors + advisors](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit#gid=1068113636) spreadsheet, collect all of the investor emails from previous funding rounds and add them to bcc of the email and send.
+
+
### Refresh event calendar
Fleet's public relations firm is directly responsible for the accuracy of event locations, attendance dates, and CFP deadlines in the event strategy workbook. At the end of every quarter, the PR firm updates every event in the ["Event strategy workbook"](https://docs.google.com/spreadsheets/d/1YQXAX2Q_WnGkAwMYjMbQpV3nbCj7gOBbv7Y0u4twxzQ/edit) (private Google doc) by following these steps:
1. Visit the latest website for each event.
2. Update the workbook with the latest location, dates, and CFP deadlines from the website.
+### Schedule press release
+Fleet will occasionally release information to the press regarding upcoming initiatives before updating the functionality of the core product. This process sUse the following steps to schedule a press release:
+
+1. Add context for the next press release to the [e-group agenda](https://docs.google.com/document/d/13fjq3T0bZGOUah9cqHVxngckv0EB2R24A3gfl5cH7eo/edit) as a "DISCUSS:" to be reviewed by Fleet's executive team for alignment and finalization of date.
+2. Once a release date is set, at-mention our public relations firm in the [#help-public-relations-firm--mindshare-pr--brand-marketing](https://fleetdm.slack.com/archives/C04PC9H34LF) and schedule a 30m call for our CEO and to communicate the press release.
+
+> The above must be completed 6 weeks before the press release date.
+
+3. Schedule a 1.5h discussion between the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) and the CEO to review the first draft linked as "Agenda: LINK" to the calendar event description.
+4. Schedule a 60m call with the CEO and public relations firm to review the first draft linked as above to the calendar event (first draft provided by the PR firm)
+5. Schedule 2.5 hrs of async time for the CEO work on edits and a 60m followup postgame (solo) where CEO edits and then settles+sends final release.
+
+
### Archive a document
Follow these steps to archive any document:
1. Create a copy of the document prefixed with the date using the format "`YYYY-MM-DD` Backup of `DOCUMENT_NAME`" (e.g. "2024-03-22 Backup of 🪂🗞️ Customer voice").
@@ -391,7 +429,7 @@ Every two weeks, our CEO Mike has a meeting with Sid Sijbrandij. The CEO uses de
Follow these steps to process and backup the E-group agenda:
1. [Archive the E-group agenda](https://fleetdm.com/handbook/digital-experience#archive-a-document) after each meeting, moving it to the ["¶¶ E-group archive"](https://drive.google.com/drive/u/0/folders/1IsSGMgbt4pDcP8gSnLj8Z8NGY7_6UTt6) folder in Google Drive.
2. **In the backup copy**, leave Google Doc comments assigning all TODOs to the correct DRI.
-3. If the "All hands" meeting has happened today
+3. If the "All hands" meeting has happened today remove any spotlights covered in the current "All hands" presentation.
### Check LinkedIn for unread messages
Once a day the Apprentice will confirm check LinkedIn for unread messages.
diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md
index 835982d970..a560a0764e 100644
--- a/handbook/product-design/README.md
+++ b/handbook/product-design/README.md
@@ -11,7 +11,7 @@ This handbook page details processes specific to working [with](#contact-us) and
## Contact us
-- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5).
+- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in `#help-design`.
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none) for this department, including pending tasks and the status of new requests.
@@ -22,7 +22,7 @@ The Product Design department is responsible for reviewing and collecting feedba
- Once your designs are reviewed and approved, change the status on the cover page of the relevant Figma file and move the issue to the "Settled" column.
- After each release (every 3 weeks) make sure you change the status on the cover page of the relevant Figma files that you worked on during the sprint to "Released".
->**Questions and missing information:** Take a screenshot of the area in Figma and start a thread in the [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5) Slack channel and paste in the screenshot. Figma does have a commenting system, but it is not easy to search for outstanding concerns and is therefore not preferred.
+>**Questions and missing information:** Take a screenshot of the area in Figma and add a comment in the story's GitHub issue. Figma does have a commenting system, but it is not easy to search for outstanding concerns and is therefore not preferred.
>
>For external contributors: please consider opening an issue with reference screenshots if you have a Figma related question you need to resolve.
@@ -85,8 +85,8 @@ You'll know it's time for expedited drafting when:
- A user story on the drafting board won't reach "Settled" by the last estimation session in the current sprint and cannot wait until the next sprint. This can also happen when we decide to bring a user story in mid-sprint.
What happens during expedited drafting?
-1. If the user story wasn't "Settled" by the last estimation session, the product group's engineering manager (EM), [release DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris), and Head of Product Design are notified in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5). Decision to allow the user story to make it into the sprint is up to the release DRI.
-2. If the user story is already in the sprint, the EM, release DRI, and Head of Product Design are notified in #help-product-design. If there are significant changes to the requirements, then the user story might be pushed to the next sprint. Decision is up to the release DRI.
+1. If the user story wasn't "Settled" by the last estimation session, the product group's engineering manager (EM), [release DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris), and Head of Product Design are notified in `#g-mdm` or `#g-endpoint-ops`. Decision to allow the user story to make it into the sprint is up to the release DRI.
+2. If the user story is already in the sprint, the EM, release DRI, and Head of Product Design are notified in `#g-mdm` or `#g-endpoint-ops`. If there are significant changes to the requirements, then the user story might be pushed to the next sprint. Decision is up to the release DRI.
3. If the release DRI decides the user story will be worked on this sprint, drafts are updated or finished.
4. UI changes [are approved](https://fleetdm.com/handbook/company/development-groups#drafting-process), and the UI changes are brought back into the sprint or are estimated.
@@ -160,18 +160,6 @@ The following highlights should be considered when deciding if we promote a feat
explains why the feature is advertised as "beta" and tracking the feature's progress towards advertising the feature as "stable."
- The feature will be advertised as "beta" in the documentation on fleetdm.com/docs, release notes, release blog posts, and Twitter.
-### Maintain current versions
-Fleet's product depends on the capabilities of other platforms.
-
-Every week, a member of the product team looks up whether there is:
-1. a new major or minor version of [macOS](https://support.apple.com/en-us/HT201260)
-2. a new major or minor version of [CIS Benchmarks Windows 10 Enterprise](https://workbench.cisecurity.org/community/2/benchmarks?q=windows+10+enterprise&status=&sortBy=version&type=desc)
-3. a new major or minor version of [CIS Benchmarks macOS 13 Ventura](https://workbench.cisecurity.org/community/20/benchmarks?q=macos+13.0+Ventura&status=&sortBy=version&type=desc)
-4. a release of CIS Benchmarks for [macOS 14 Sonoma](https://workbench.cisecurity.org/community/20/benchmarks?q=sonoma&status=&sortBy=version&type=desc)
-5. a new major or minor version of [ChromeOS](https://chromereleases.googleblog.com/search/label/Chrome%20OS)
-
-The DRI should record the latest versions in the [maintenance tracker](https://docs.google.com/spreadsheets/d/1IWfQtSkOQgm_JIQZ0i2y3A8aaK5vQW1ayWRk6-4FOp0/edit#gid=0). If there are any changes, the DRI sends an update in the [#help-product-design Slack channel](https://fleetdm.slack.com/archives/C02A8BRABB5).
-
### View Fleet usage statistics
In order to understand the usage of the Fleet product, we [collect statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) from installations where this functionality is enabled.
@@ -190,7 +178,7 @@ Some of the data is forwarded to [Datadog](https://us5.datadoghq.com/dashboard/7
The following stubs are included only to make links backward compatible.
##### Maintenance
-Please see [handbook/product#maintain-current-versions](https://fleetdm.com/handbook/product#maintain-current-versions)
+Please see [handbook/product-design#rituals](https://fleetdm.com/handbook/product-design#rituals)
##### New CIS benchmarks
Please see [handbook/product#submit-a-new-cis-benchmark-set-for-certification](https://fleetdm.com/handbook/product#submit-a-new-cis-benchmark-set-for-certification)
diff --git a/handbook/product-design/product-design.rituals.yml b/handbook/product-design/product-design.rituals.yml
index bc710c5674..bea04e39bb 100644
--- a/handbook/product-design/product-design.rituals.yml
+++ b/handbook/product-design/product-design.rituals.yml
@@ -68,7 +68,7 @@
task: "Maintenance" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes
startedOn: "2024-03-01"
frequency: "Weekly"
- description: "Head of Product Design checks the latest versions of relevant platforms, updates the maintenance tracker, and notifies the #help-product-design Slack channel."
+ description: "Head of Product Design checks the latest versions of relevant platforms, updates the maintenance tracker, and notifies the #g-mdm and #g-endpoint-ops Slack channel."
moreInfoUrl:
dri: "noahtalerman"
-
diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf
index cb0d1593de..fdccef8481 100644
--- a/infrastructure/dogfood/terraform/aws/variables.tf
+++ b/infrastructure/dogfood/terraform/aws/variables.tf
@@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
- default = "fleetdm/fleet:v4.48.0"
+ default = "fleetdm/fleet:v4.48.2"
}
variable "software_inventory" {
diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf
index afe4e46243..88982208df 100644
--- a/infrastructure/dogfood/terraform/gcp/variables.tf
+++ b/infrastructure/dogfood/terraform/gcp/variables.tf
@@ -68,5 +68,5 @@ variable "redis_mem" {
}
variable "image" {
- default = "fleet:v4.48.0"
+ default = "fleet:v4.48.2"
}
diff --git a/it-and-security/default.yml b/it-and-security/default.yml
index a30c72224b..36edde9c74 100644
--- a/it-and-security/default.yml
+++ b/it-and-security/default.yml
@@ -31,6 +31,9 @@ org_settings:
host_expiry_settings:
host_expiry_enabled: false
integrations:
+ google_calendar:
+ - api_key_json: $DOGFOOD_CALENDAR_API_KEY
+ domain: fleetdm.com
jira: [ ]
zendesk: [ ]
mdm:
diff --git a/it-and-security/lib/macos-device-health.policies.yml b/it-and-security/lib/macos-device-health.policies.yml
index b706cd80c1..350b1f530f 100644
--- a/it-and-security/lib/macos-device-health.policies.yml
+++ b/it-and-security/lib/macos-device-health.policies.yml
@@ -44,7 +44,7 @@
username = ''
)
AND NOT EXISTS (
- SELECT 1 FROM managed_policies WHERE
+ SELECT 1 FROM managed_policies WHERE
domain='com.apple.screensaver' AND
name='idleTime' AND
CAST(value AS INT) > 1200
@@ -54,8 +54,24 @@
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 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/%'));
+ 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. 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."
+ description: Looks for PDF files with file names typically used by 1Password for emergency recovery kits. To protect the performance of your devices, the search is one level deep and limited to the Desktop, Documents, Downloads, and Shared folders.
+ resolution: Delete 1Password emergency kits from your computer, and empty the trash. 1Password emergency kits should only be printed and stored in a physically secure location.
platform: darwin
+- name: macOS - Check if latest version
+ query: SELECT 1 WHERE
+ EXISTS (
+ SELECT major, concat_ws(".", major, minor, patch) AS "macOS Version" FROM os_version --Sonoma WHERE
+ (major = "14" AND "macOS Version" < "14.4.1")
+ );
+ critical: false
+ description: This policy check if macOS version is most recent version available.
+ resolution: From the Apple menu, select System Settings. Navigate to General > Software Update.
+ platform: darwin
+ calendar_events_enabled: true
diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml
index 1ace8e43cf..7ec46d1f36 100644
--- a/it-and-security/teams/workstations-canary.yml
+++ b/it-and-security/teams/workstations-canary.yml
@@ -8,6 +8,10 @@ team_settings:
host_expiry_window: 0
secrets:
- secret: $DOGFOOD_WORKSTATIONS_CANARY_ENROLL_SECRET
+ integrations:
+ google_calendar:
+ enable_calendar_events: true
+ webhook_url: $DOGFOOD_WORKSTATIONS_CANARY_CALENDAR_WEBHOOK_URL
agent_options:
config:
decorators:
diff --git a/orbit/CHANGELOG.md b/orbit/CHANGELOG.md
index b62a34e6d5..90b43dc314 100644
--- a/orbit/CHANGELOG.md
+++ b/orbit/CHANGELOG.md
@@ -1,3 +1,9 @@
+## Orbit 1.23.0 (Apr 08, 2024)
+
+* Add `parse_json`, `parse_jsonl`, `parse_xml`, and `parse_ini` tables.
+
+* Add exponential backoff to orbit enroll retries.
+
## Orbit 1.22.0 (Feb 26, 2024)
* Reduce error logs when orbit cannot connect to Fleet.
diff --git a/orbit/TUF.md b/orbit/TUF.md
index fc164d0d15..11c44d4184 100644
--- a/orbit/TUF.md
+++ b/orbit/TUF.md
@@ -7,8 +7,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
| Component\OS | macOS | Linux | Windows |
|--------------|--------------|--------|---------|
-| orbit | 1.22.0 | 1.22.0 | 1.22.0 |
-| desktop | 1.22.0 | 1.22.0 | 1.22.0 |
+| orbit | 1.23.0 | 1.23.0 | 1.23.0 |
+| desktop | 1.23.0 | 1.23.0 | 1.23.0 |
| osqueryd | 5.11.0 | 5.11.0 | 5.11.0 |
| nudge | 1.1.10.81462 | - | - |
| swiftDialog | 2.1.0 | - | - |
@@ -17,8 +17,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
| Component\OS | macOS | Linux | Windows |
|--------------|--------|--------|---------|
-| orbit | 1.22.0 | 1.22.0 | 1.22.0 |
-| desktop | 1.22.0 | 1.22.0 | 1.22.0 |
+| orbit | 1.23.0 | 1.23.0 | 1.23.0 |
+| desktop | 1.23.0 | 1.23.0 | 1.23.0 |
| osqueryd | 5.12.1 | 5.12.1 | 5.12.1 |
| nudge | - | - | - |
| swiftDialog | - | - | - |
diff --git a/orbit/changes/16594-orbit-enroll-backoff b/orbit/changes/16594-orbit-enroll-backoff
deleted file mode 100644
index 01ef599803..0000000000
--- a/orbit/changes/16594-orbit-enroll-backoff
+++ /dev/null
@@ -1 +0,0 @@
-* Add exponential backoff to orbit enroll retries.
diff --git a/orbit/changes/17362-desktop-version-and-scripts-enabled b/orbit/changes/17362-desktop-version-and-scripts-enabled
new file mode 100644
index 0000000000..8c6104850f
--- /dev/null
+++ b/orbit/changes/17362-desktop-version-and-scripts-enabled
@@ -0,0 +1 @@
+In orbit_info table, added desktop_version and scripts_enabled fields.
diff --git a/orbit/changes/dataflatten-tables b/orbit/changes/dataflatten-tables
deleted file mode 100644
index d2ec646ff4..0000000000
--- a/orbit/changes/dataflatten-tables
+++ /dev/null
@@ -1 +0,0 @@
-- Add `parse_json`, `parse_jsonl`, `parse_xml`, and `parse_ini` tables.
diff --git a/orbit/cmd/desktop/desktop.go b/orbit/cmd/desktop/desktop.go
index c6d84be9eb..89b8e88475 100644
--- a/orbit/cmd/desktop/desktop.go
+++ b/orbit/cmd/desktop/desktop.go
@@ -59,6 +59,13 @@ func setupRunners() {
}
func main() {
+ // Orbits uses --version to get the fleet-desktop version. Logs do not need to be set up when running this.
+ if len(os.Args) > 1 && os.Args[1] == "--version" {
+ // Must work with update.GetVersion
+ fmt.Println("fleet-desktop", version)
+ return
+ }
+
setupLogs()
setupStderr()
diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go
index 1fd3d79ba2..87bd73a56c 100644
--- a/orbit/cmd/orbit/orbit.go
+++ b/orbit/cmd/orbit/orbit.go
@@ -495,6 +495,7 @@ func main() {
}
// Get current version of osquery
+ log.Info().Msgf("orbit version: %s", build.Version)
osquerydPath, err = updater.ExecutableLocalPath("osqueryd")
if err != nil {
log.Info().Err(err).Msg("Could not find local osqueryd executable")
@@ -803,7 +804,9 @@ func main() {
windowsMDMBitlockerCommandFrequency = time.Hour
)
configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL)
- configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient)
+ configFetcher, scriptsEnabledFn := update.ApplyRunScriptsConfigFetcherMiddleware(
+ configFetcher, c.Bool("enable-scripts"), orbitClient,
+ )
switch runtime.GOOS {
case "darwin":
@@ -1079,6 +1082,20 @@ func main() {
checkerClient.GetServerCapabilities().Copy(orbitClient.GetServerCapabilities())
g.Add(capabilitiesChecker.actor())
+ var desktopVersion string
+ if c.Bool("fleet-desktop") {
+ runPath := desktopPath
+ if runtime.GOOS == "darwin" {
+ runPath = filepath.Join(desktopPath, "Contents", "MacOS", constant.DesktopAppExecName)
+ }
+ desktopVersion, err = update.GetVersion(runPath)
+ if err == nil && desktopVersion != "" {
+ log.Info().Msgf("Found fleet-desktop version: %s", desktopVersion)
+ } else {
+ desktopVersion = "unknown"
+ }
+ }
+
registerExtensionRunner(
&g,
r.ExtensionSocketPath(),
@@ -1087,8 +1104,10 @@ func main() {
c.String("orbit-channel"),
c.String("osqueryd-channel"),
c.String("desktop-channel"),
+ desktopVersion,
trw,
startTime,
+ scriptsEnabledFn,
)),
)
diff --git a/orbit/docs/TUF-Update-Guide.md b/orbit/docs/TUF-Update-Guide.md
deleted file mode 100644
index e2f4946ecf..0000000000
--- a/orbit/docs/TUF-Update-Guide.md
+++ /dev/null
@@ -1,392 +0,0 @@
-# Pushing new releases to TUF
-
-This document is a walkthrough guide for:
-- A Fleet member to publish new updates to [Fleet's TUF service](tuf.fleetctl.com). See [Pushing updates](#pushing-updates).
-- A Fleet member to delete targets from [Fleet's TUF service](tuf.fleetctl.com). See [Removing unused targets](#removing-unused-targets).
-- A Fleet member to become a publisher of updates for [Fleet's TUF service](tuf.fleetctl.com). See [Becoming a new Fleet publisher](#becoming-a-new-fleet-publisher).
-
-Video walkthrough related to this process for additional [context](https://drive.google.com/file/d/1c_iukFEMne12Cxx9WVTt_j1Wp0sC1kQU/view?usp=drive_link).
-
-## Security
-
-- The TUF keys for `targets`, `snapshot` and `timestamp` should be stored on a USB stick (used solely for this purpose). Whenever you need to push updates to Fleet's TUF repository you can temporarily copy the encrypted keys to your workstation (under the `keys/` folder, more on this below).
-- The keys should be stored encrypted with its passphrase stored in 1Password (on a private vault).
-- Every `fleetctl updates` command will prompt for the passphrases to decrypt the encrypted keys. You can input the passphrases every time or can alternatively set the following environment variables: `FLEET_TIMESTAMP_PASSPHRASE`, `FLEET_SNAPSHOT_PASSPHRASE` and `FLEET_TARGETS_PASSPHRASE`. Make sure to not leave traces of the passphrases (scripts, history and/or environment) when you are done.
-
-## Syncing Fleet's TUF repository
-
-> The `fleetctl updates` commands assume the folders `keys/`, `staged/` and `repository/` exist on the current working directory.
-
-> IMPORTANT: When syncing the repository make sure to use `--exact-timestamps`. Otherwise `aws s3 sync` may not sync files that do not change in size, like `timestamp.json`.
-
-- The `keys/` folder contains the encrypted private keys.
-- The `staged/` folder contains uncommitted changes (usually empty because `fleetctl updates` commands automatically commit the changes).
-- The `repository/` folder contains the full TUF repository.
-
-Following are the commands to initialize the repository on your workstation:
-```sh
-mkdir /path/to/tuf.fleetctl.com
-
-cd /path/to/tuf.fleetctl.com
-mkdir -p ./repository
-cp /Volumes/YOUR-USB-NAME/keys ./keys
-mkdir -p ./staged
-
-export AWS_PROFILE=tuf
-aws sso login
-
-aws s3 sync s3://fleet-tuf-repo ./repository --exact-timestamps
-```
-
-## Building the components for releasing to `edge`
-
-### fleetd
-
-> Assuming we are releasing version 1.21.0 of fleetd.
-
-1. Create the fleetd changelog for the new release:
-```sh
-git checkout main
-git pull origin main
-git checkout -b release-fleetd-v1.21.0
-make changelog-orbit
-```
-2. Edit `orbit/CHANGELOG.md` accordingly
-3. Bump Fleet Desktop version in https://github.com/fleetdm/fleet/blob/9ca85411a16c504087d2793f8b9099f98054c93f/.github/workflows/generate-desktop-targets.yml#L27. This will trigger a github action to build the Fleet Desktop executables: https://github.com/fleetdm/fleet/actions/workflows/generate-desktop-targets.yml.
-4. Commit the changes, push the branch and create a PR.
-5. Add the following git tag with the following format: `orbit-v1.21.0`. Once pushed this will trigger a github action to build the orbit executables: https://github.com/fleetdm/fleet/blob/main/.github/workflows/goreleaser-orbit.yaml.
-```sh
-git tag orbit-v1.21.0
-git push origin --tags
-```
-6. Once the two github actions finish their runs, use the following scripts that will download the artifacts to a folder in your workstation (on this guide we assume you are using `$HOME/release-friday`).
-NOTE: The `goreleaser-macos` job is unstable and may need several re-runs until it works.
-```sh
-go run ./tools/tuf/download-artifacts desktop \
- --git-branch release-fleetd-v1.21.0 \
- --output-directory $HOME/release-friday/desktop \
- --github-username $GITHUB_USERNAME --github-api-token $GITHUB_TOKEN
-go run ./tools/tuf/download-artifacts orbit \
- --git-tag orbit-v1.21.0 \
- --output-directory $HOME/release-friday/orbit \
- --github-username $GITHUB_USERNAME --github-api-token $GITHUB_TOKEN
-tree $HOME/release-friday
-$HOME/release-friday
-├── desktop
-│ ├── linux
-│ │ └── desktop.tar.gz
-│ ├── macos
-│ │ └── desktop.app.tar.gz
-│ └── windows
-│ └── fleet-desktop.exe
-└── orbit
- ├── linux
- │ └── orbit
- ├── macos
- │ └── orbit
- └── windows
- └── orbit.exe
-```
-7. With the executables on your workstation, proceed to [Pushing updates](#pushing-updates) (`edge`).
-8. Manually run (`Run workflow`) this action that will update the released versions on our doc: https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml.
-
-### osqueryd
-
-> Assuming we are releasing version 5.12.0 of osqueryd.
-
-1. Bump osquery version in https://github.com/fleetdm/fleet/blob/30a36b0b3a1fd50e48d98a4c3c955595022f5277/.github/workflows/generate-osqueryd-targets.yml#L27.
-2. Commit the changes, push the branch (assuming branch name is `bump-osqueryd-5.12.0`) and create a PR.
-3. Once the Github action completes run the following (the [GitHub API token](https://github.com/settings/tokens?type=beta) does not need any special permissions -- public repository access is sufficient):
-```sh
-go run ./tools/tuf/download-artifacts osqueryd \
- --git-branch bump-osqueryd-5.12.0 \
- --output-directory $HOME/release-friday/osqueryd \
- --github-username $GITHUB_USERNAME \
- --github-api-token $GITHUB_TOKEN
-tree $HOME/release-friday/osqueryd
-$HOME/release-friday/osqueryd
-├── linux
-│ └── osqueryd
-├── macos
-│ └── osqueryd.app.tar.gz
-└── windows
- └── osqueryd.exe
-```
-4. With the executables on your workstation, proceed to [Pushing updates](#pushing-updates) (`edge`).
-5. Manually run (`Run workflow`) this action that will update the released versions on our docs: https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml.
-
-## Pushing updates
-
-> Before performing any actions on Fleet's TUF repository you must:
-> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository).
-> 2. Create a local backup in case we mess up with the repository:
-> ```sh
-> mkdir ~/tuf.fleetctl.com/backup
-> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup
-> ```
-> 3. Install fleetd on macOS, Linux and Windows VMs using the channel (`stable` or `edge`) you are about to release.
-> You can do this using the following flags in `fleetctl package`: `--orbit-channel`, `--desktop-channel`, `--osqueryd-channel`.
-
-### Releasing to the `edge` channel
-
-The commands shown here update the local repository. After you are done running the commands below for each component, see [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository) to push the updates to Fleet's TUF repository (https://tuf.fleetctl.com).
-
-#### Setup
-
-Make sure to install fleetd components using the `edge` channels in the three supported OSs (this is useful to smoke test the update).
-Here's how to generate the packages:
-```sh
-# (The same for --type=deb and --type=msi.)
-fleetctl package --type=pkg \
- --enable-scripts \
- --fleet-desktop \
- --fleet-url=... --enroll-secret=... \
- --update-interval 10s \
- --orbit-channel edge --desktop-channel edge --osqueryd-channel edge
-```
-
-#### orbit
-
-The `orbit` executables are downloaded from the [GoReleaser Orbit action](https://github.com/fleetdm/fleet/actions/workflows/goreleaser-orbit.yaml).
-Such action is triggered when git tagging a new orbit version with a tag of the form: `orbit-v1.21.0`.
-
-> IMPORTANT: If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped (even if there are no changes in it).
-> This is due to the fact that we want users to see the new version in the tray icon, e.g. `"Fleet Desktop v1.21.0"`.
-> Technical debt: We could improve this process to reduce the complexity of releasing fleetd when there are no Fleet Desktop changes.
-
-> The following commands assume you are pushing version `1.21.0`.
-
-```sh
-# macOS
-fleetctl updates add --target $HOME/release-friday/orbit/macos/orbit --platform macos --name orbit --version 1.21.0 -t edge
-# Linux
-fleetctl updates add --target $HOME/release-friday/orbit/linux/orbit --platform linux --name orbit --version 1.21.0 -t edge
-# Windows
-fleetctl updates add --target $HOME/release-friday/orbit/windows/orbit.exe --platform windows --name orbit --version 1.21.0 -t edge
-```
-
-#### desktop
-
-The Fleet Desktop executables are downloaded from the [Generate Fleet Desktop targets for Orbit action](https://github.com/fleetdm/fleet/actions/workflows/generate-desktop-targets.yml).
-Such action is triggered by submitting a PR with the [following version string](https://github.com/fleetdm/fleet/blob/4a6bf0d447a2080f994da1e2f36ce6d51db88109/.github/workflows/generate-desktop-targets.yml#L27) changed.
-
-> The following commands assume you are pushing version `1.21.0`.
-
-```sh
-# macOS
-fleetctl updates add --target $HOME/release-friday/desktop/macos/desktop.app.tar.gz --platform macos --name desktop --version 1.21.0 -t edge
-# Linux
-fleetctl updates add --target $HOME/release-friday/desktop/linux/desktop.tar.gz --platform linux --name desktop --version 1.21.0 -t edge
-# Windows
-fleetctl updates add --target $HOME/release-friday/desktop/windows/fleet-desktop.exe --platform windows --name desktop --version 1.21.0 -t edge
-```
-
-#### swiftDialog
-
-> macOS only component
-
-The `swiftDialog` executable can be generated from a macOS host by running:
-```sh
-make swift-dialog-app-tar-gz version=2.2.1 build=4591 out-path=.
-```
-
-```sh
-fleetctl updates add --target /path/to/macos/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version 2.2.1 -t edge
-```
-
-#### nudge
-
-> macOS only component
-
-The `nudge` executable can be generated from a macOS host by running:
-```sh
-make nudge-app-tar-gz version=1.1.10.81462 out-path=.
-```
-
-```sh
-fleetctl updates add --target /path/to/macos/nudge.app.tar.gz --platform macos --name nudge --version 1.1.10.81462 -t edge
-```
-
-#### osqueryd
-
-Osquery executables are downloaded from the [Generate osqueryd targets for Fleetd action](https://github.com/fleetdm/fleet/blob/main/.github/workflows/generate-osqueryd-targets.yml).
-Such action is triggered by submitting a PR with the [following version string](https://github.com/fleetdm/fleet/blob/7067ca586a4aa1a0377b387d4b4478a5958193ff/.github/workflows/generate-osqueryd-targets.yml#L27) changed.
-
-> The following commands assume you are pushing version `5.9.1`.
-
-```sh
-# macOS
-fleetctl updates add --target $HOME/release-friday/osqueryd/macos/osqueryd.app.tar.gz --platform macos-app --name osqueryd --version 5.9.1 -t edge
-# Linux
-fleetctl updates add --target $HOME/release-friday/osqueryd/linux/osqueryd --platform linux --name osqueryd --version 5.9.1 -t edge
-# Windows
-fleetctl updates add --target $HOME/release-friday/osqueryd/windows/osqueryd.exe --platform windows --name osqueryd --version 5.9.1 -t edge
-```
-
-#### Push updates
-
-Once all components are updated in your local repository we need to push the changes to the remote repository.
-See [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository).
-
-### Promoting `edge` to the `stable` channel
-
-> Make sure to install fleetd components using the `stable` channels in the three supported OSs (this is useful to smoke test the update).
-
-Following is the list of components and each command for each operating system.
-
-The commands show here update the local repository. After you are done running the commands below for each component, see [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository) to push the updates to Fleet's TUF repository (https://tuf.fleetctl.com).
-
-#### orbit
-
-> IMPORTANT: If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped (even if there are no changes in it).
-> This is due to the fact that we want users to see the new version in the tray icon, e.g. `"Fleet Desktop v1.21.0"`.
-> Technical debt: We could improve this process to reduce the complexity of releasing fleetd when there are no Fleet Desktop changes.
-
-> The following command assumes you are pushing version `1.21.0`:
-```sh
-/fleet/repo/tools/tuf/promote_edge_to_stable.sh orbit 1.21.0
-```
-
-#### desktop
-
-> The following command assumes you are pushing version `1.21.0`:
-```sh
-/fleet/repo/tools/tuf/promote_edge_to_stable.sh desktop 1.21.0
-```
-
-#### swiftDialog
-
-> The following command assumes you are pushing version `2.2.1`:
-```sh
-/fleet/repo/tools/tuf/promote_edge_to_stable.sh swiftDialog 2.2.1
-```
-
-#### nudge
-
-> The following command assumes you are pushing version `1.1.10.81462`:
-```sh
-/fleet/repo/tools/tuf/promote_edge_to_stable.sh nudge 1.1.10.81462
-```
-
-#### osqueryd
-
-> The following command assumes you are pushing version `5.9.1`.
-```sh
-/fleet/repo/tools/tuf/promote_edge_to_stable.sh osqueryd 5.9.1
-```
-
-#### Push updates
-
-Once all components are updated in your local repository we need to push the changes to the remote repository.
-See [Pushing releases to Fleet's TUF repository](#pushing-releases-to-fleets-tuf-repository).
-
-### Pushing releases to Fleet's TUF repository
-
-Once you are done with the changes on your local repository, you can use the following command to review the changes before pushing (`--dryrun` allows us to verify the upgrade before pushing):
-```sh
-AWS_PROFILE=tuf aws s3 sync ./repository s3://fleet-tuf-repo --dryrun
-(dryrun) upload: repository/snapshot.json to s3://fleet-tuf-repo/snapshot.json
-(dryrun) upload: repository/targets.json to s3://fleet-tuf-repo/targets.json
-[...]
-(dryrun) upload: repository/timestamp.json to s3://fleet-tuf-repo/timestamp.json
-```
-
-If all looks good, run the same command without the `--dryrun` flag.
-
-> NOTE: Some things to note after the changes are pushed:
-> - Once pushed you might see some clients failing to upgrade due to some sha256 mismatches. These temporary failures are expected because it takes some time for caches to be invalidated (these errors should go away after a few minutes).
-> - The auto-update routines in orbit runs every 15 minutes, so you might need to wait up to 15 minutes to verify online hosts are auto-updating properly.
-
-## Removing Unused Targets
-
-If you've inadvertently published a target that is no longer in use, follow these steps to remove it.
-
-> Before performing any actions on Fleet's TUF repository you must:
-> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository).
-> 2. Create a local backup in case we mess up with the repository:
-> ```sh
-> mkdir ~/tuf.fleetctl.com/backup
-> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup
-> ```
-
-1. You'll need the [`go-tuf`](https://github.com/theupdateframework/go-tuf) binary. The removal operations aren't integrated into `fleetctl` at the moment.
-2. Use `tuf remove` to remove the target and update `targets.json`. Substitute `desktop/windows/stable/desktop.exe` with the target you intend to delete.
-```sh
-tuf remove desktop/windows/stable/desktop.exe
-```
-3. Snapshot, timestamp, and commit the changes.
-```sh
-tuf snapshot
-tuf timestamp
-tuf commit
-```
-4. Run the following command to generate a timestamp that expires in two weeks (otherwise the default expiration when using `go-tuf` commands is 1 day)
-```sh
-fleetctl updates timestamp
-```
-5. Confirm that the version of the local `timestamp.json` file is more recent than that of the remote server.
-6. Verify the changes that will be synced by running a dry sync. Include the `--delete` flag as you're removing targets.
-```sh
-aws s3 sync ./repository s3://fleet-tuf-repo --delete --dryrun
-```
-7. `diff` the local `targets.json` file with its remote version.
-8. To upload the changes, perform a sync without the `--dryrun`:
-```sh
-aws s3 sync ./repository s3://fleet-tuf-repo --delete
-```
-
-## Becoming a New Fleet Publisher
-
-> Before performing any actions on Fleet's TUF repository you must:
-> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository).
-> 2. Create a local backup in case we mess up with the repository:
-> ```sh
-> mkdir ~/tuf.fleetctl.com/backup
-> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup
-> ```
-
-### Generate targets+snapshot+timestamp keys
-
-All commands shown in this guide are executed from `/path/to/tuf.fleetctl.com`:
-```sh
-cd /path/to/tuf.fleetctl.com
-```
-
-```sh
-tuf gen-key targets
-Enter targets keys passphrase:
-Repeat targets keys passphrase:
-Generated targets key with ID ae943cb8be8a849b37c66ed46bdd7e905ba3118c0c051a6ee3cd30625855a076
-```
-```sh
-tuf gen-key snapshot
-Enter snapshot keys passphrase:
-Repeat snapshot keys passphrase:
-Generated snapshot key with ID 1a4d9beb826d1ff4e036d757cfcd6e36d0f041e58d25f99ef3a20ae3f8dd71e3
-```
-```sh
-tuf gen-key timestamp
-Enter timestamp keys passphrase:
-Repeat timestamp keys passphrase:
-Generated timestamp key with ID d940df08b59b12c30f95622a05cc40164b78a11dd7d408395ee4f79773331b30
-```
-
-Share `staged/root.json` with Fleet member with the `root` role, who will sign with its root key and push to the repository.
-
-### Root role signs the `staged/root.json`
-
-Essentially the following commands are executed to sign the new keys:
-- `tuf sign`
-- `tuf snapshot`
-- `tuf timestamp`
-- `tuf commit`
-
-## Misc issues
-
-### Invalid timestamp.json version
-
-The following issue was solved by resigning the timestamp metadata `fleetctl updates timestamp` (executed three times to increase the version to `4175`)
-```sh
-2022-08-23T13:44:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174"
-2022-08-23T13:59:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174"
-```
diff --git a/orbit/pkg/table/orbit_info/orbit_info.go b/orbit/pkg/table/orbit_info/orbit_info.go
index 95b426a2ec..5ac71fe278 100644
--- a/orbit/pkg/table/orbit_info/orbit_info.go
+++ b/orbit/pkg/table/orbit_info/orbit_info.go
@@ -19,19 +19,26 @@ type Extension struct {
orbitChannel string
osquerydChannel string
desktopChannel string
+ dektopVersion string
trw *token.ReadWriter
+ scriptsEnabled func() bool
}
var _ orbit_table.Extension = (*Extension)(nil)
-func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter, startTime time.Time) *Extension {
+func New(
+ orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, desktopVersion string, trw *token.ReadWriter,
+ startTime time.Time, scriptsEnabled func() bool,
+) *Extension {
return &Extension{
startTime: startTime,
orbitClient: orbitClient,
orbitChannel: orbitChannel,
osquerydChannel: osquerydChannel,
desktopChannel: desktopChannel,
+ dektopVersion: desktopVersion,
trw: trw,
+ scriptsEnabled: scriptsEnabled,
}
}
@@ -50,7 +57,9 @@ func (o Extension) Columns() []table.ColumnDefinition {
table.TextColumn("orbit_channel"),
table.TextColumn("osqueryd_channel"),
table.TextColumn("desktop_channel"),
+ table.TextColumn("desktop_version"),
table.BigIntColumn("uptime"),
+ table.IntegerColumn("scripts_enabled"),
}
}
@@ -73,6 +82,17 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[
}
}
+ boolToInt := func(b bool) int64 {
+ // Fast implementation according to https://0x0f.me/blog/golang-compiler-optimization/
+ var i int64
+ if b {
+ i = 1
+ } else {
+ i = 0
+ }
+ return i
+ }
+
return []map[string]string{{
"version": v,
"device_auth_token": token,
@@ -81,6 +101,8 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[
"orbit_channel": o.orbitChannel,
"osqueryd_channel": o.osquerydChannel,
"desktop_channel": o.desktopChannel,
+ "desktop_version": o.dektopVersion,
"uptime": strconv.FormatInt(int64(time.Since(o.startTime).Seconds()), 10),
+ "scripts_enabled": strconv.FormatInt(boolToInt(o.scriptsEnabled()), 10),
}}, nil
}
diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go
index 174b2d76c3..febb8cc054 100644
--- a/orbit/pkg/update/notifications.go
+++ b/orbit/pkg/update/notifications.go
@@ -314,7 +314,9 @@ type runScriptsConfigFetcher struct {
mu sync.Mutex
}
-func ApplyRunScriptsConfigFetcherMiddleware(fetcher OrbitConfigFetcher, scriptsEnabled bool, scriptsClient scripts.Client) OrbitConfigFetcher {
+func ApplyRunScriptsConfigFetcherMiddleware(
+ fetcher OrbitConfigFetcher, scriptsEnabled bool, scriptsClient scripts.Client,
+) (OrbitConfigFetcher, func() bool) {
scriptsFetcher := &runScriptsConfigFetcher{
Fetcher: fetcher,
ScriptsExecutionEnabled: scriptsEnabled,
@@ -323,7 +325,7 @@ func ApplyRunScriptsConfigFetcherMiddleware(fetcher OrbitConfigFetcher, scriptsE
}
// start the dynamic check for scripts enabled if required
scriptsFetcher.runDynamicScriptsEnabledCheck()
- return scriptsFetcher
+ return scriptsFetcher, scriptsFetcher.scriptsEnabled
}
func (h *runScriptsConfigFetcher) runDynamicScriptsEnabledCheck() {
@@ -372,10 +374,7 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
log.Debug().Msgf("received request to run scripts %v", cfg.Notifications.PendingScriptExecutionIDs)
runner := &scripts.Runner{
- // scripts are always enabled if the agent is started with the
- // --scripts-enabled flag. If it is not started with this flag, then
- // scripts are enabled only if the mdm profile says so.
- ScriptExecutionEnabled: h.ScriptsExecutionEnabled || h.dynamicScriptsEnabled.Load(),
+ ScriptExecutionEnabled: h.scriptsEnabled(),
Client: h.ScriptsClient,
}
fn := runner.Run
@@ -399,6 +398,13 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
return cfg, err
}
+func (h *runScriptsConfigFetcher) scriptsEnabled() bool {
+ // scripts are always enabled if the agent is started with the
+ // --scripts-enabled flag. If it is not started with this flag, then
+ // scripts are enabled only if the mdm profile says so.
+ return h.ScriptsExecutionEnabled || h.dynamicScriptsEnabled.Load()
+}
+
type DiskEncryptionKeySetter interface {
SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error
}
diff --git a/orbit/pkg/update/runner_test.go b/orbit/pkg/update/runner_test.go
index 98781de717..035f9fd8b2 100644
--- a/orbit/pkg/update/runner_test.go
+++ b/orbit/pkg/update/runner_test.go
@@ -78,6 +78,10 @@ func TestGetVersion(t *testing.T) {
cmd: "#!/bin/bash\n/bin/echo orbit 4.5.6",
version: "4.5.6",
},
+ "42.0.0": {
+ cmd: "#!/bin/bash\n/bin/echo fleet-desktop 42.0.0",
+ version: "42.0.0",
+ },
"5.10.2-26-gc396d07b4-dirty": {
cmd: "#!/bin/bash\n/bin/echo osquery version 5.10.2-26-gc396d07b4-dirty",
version: "5.10.2-26-gc396d07b4-dirty",
diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json
index 6f88e524f6..bd13071fe1 100644
--- a/schema/osquery_fleet_schema.json
+++ b/schema/osquery_fleet_schema.json
@@ -17696,11 +17696,23 @@
"required": false,
"description": "The Update Framework update channel used for the Fleet Desktop executable."
},
+ {
+ "name": "desktop_version",
+ "type": "text",
+ "required": false,
+ "description": "The version of the fleet-desktop instance. Blank if fleet-desktop is not installed."
+ },
{
"name": "uptime",
"type": "bigint",
"required": false,
"description": "Uptime of the orbit process in seconds."
+ },
+ {
+ "name": "scripts_enabled",
+ "type": "integer",
+ "required": false,
+ "description": "1 if running scripts is enabled, 0 if disabled."
}
],
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
diff --git a/schema/tables/orbit_info.yml b/schema/tables/orbit_info.yml
index f626eb0c80..1dfa3a9ff0 100644
--- a/schema/tables/orbit_info.yml
+++ b/schema/tables/orbit_info.yml
@@ -33,9 +33,17 @@ columns:
type: text
required: false
description: The Update Framework update channel used for the Fleet Desktop executable.
+ - name: desktop_version
+ type: text
+ required: false
+ description: The version of the fleet-desktop instance. Blank if fleet-desktop is not installed.
- name: uptime
type: bigint
required: false
description: Uptime of the orbit process in seconds.
+ - name: scripts_enabled
+ type: integer
+ required: false
+ description: 1 if running scripts is enabled, 0 if disabled.
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
evented: false
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index 8c9c3cc153..f8b74da34f 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -985,7 +985,9 @@ func upsertHostDEPAssignmentsDB(ctx context.Context, tx sqlx.ExtContext, hosts [
stmt := `
INSERT INTO host_dep_assignments (host_id)
VALUES %s
- ON DUPLICATE KEY UPDATE added_at = CURRENT_TIMESTAMP, deleted_at = NULL`
+ ON DUPLICATE KEY UPDATE
+ added_at = CURRENT_TIMESTAMP,
+ deleted_at = NULL`
args := []interface{}{}
values := []string{}
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index c4489bcddc..031b7017ca 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -71,6 +71,7 @@ func TestMDMApple(t *testing.T) {
{"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown},
{"MDMAppleDDMDeclarationsToken", testMDMAppleDDMDeclarationsToken},
{"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs},
+ {"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates},
}
for _, c := range cases {
@@ -5311,6 +5312,46 @@ func TestRestorePendingDEPHost(t *testing.T) {
})
}
+func testMDMAppleDEPAssignmentUpdates(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ n := t.Name()
+ h, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: fmt.Sprintf("test-host%s-name", n),
+ OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%s", n)),
+ NodeKey: ptr.String(fmt.Sprintf("nodekey-%s", n)),
+ UUID: fmt.Sprintf("test-uuid-%s", n),
+ Platform: "darwin",
+ HardwareSerial: n,
+ })
+ require.NoError(t, err)
+
+ _, err = ds.GetHostDEPAssignment(ctx, h.ID)
+ require.ErrorIs(t, err, sql.ErrNoRows)
+
+ err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h})
+ require.NoError(t, err)
+
+ assignment, err := ds.GetHostDEPAssignment(ctx, h.ID)
+ require.NoError(t, err)
+ require.Equal(t, h.ID, assignment.HostID)
+ require.Nil(t, assignment.DeletedAt)
+
+ err = ds.DeleteHostDEPAssignments(ctx, []string{h.HardwareSerial})
+ require.NoError(t, err)
+
+ assignment, err = ds.GetHostDEPAssignment(ctx, h.ID)
+ require.NoError(t, err)
+ require.Equal(t, h.ID, assignment.HostID)
+ require.NotNil(t, assignment.DeletedAt)
+
+ err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h})
+ require.NoError(t, err)
+ assignment, err = ds.GetHostDEPAssignment(ctx, h.ID)
+ require.NoError(t, err)
+ require.Equal(t, h.ID, assignment.HostID)
+ require.Nil(t, assignment.DeletedAt)
+}
+
func createRawAppleCmd(reqType, cmdUUID string) string {
return fmt.Sprintf(`
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 28361f0a54..6daf64222b 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -16,8 +16,8 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/go-kit/kit/log"
- "github.com/go-kit/kit/log/level"
+ "github.com/go-kit/log"
+ "github.com/go-kit/log/level"
"github.com/jmoiron/sqlx"
)
@@ -630,7 +630,10 @@ SELECT
host_id = h.id
) AS additional,
COALESCE(failing_policies.count, 0) AS failing_policies_count,
- COALESCE(failing_policies.count, 0) AS total_issues_count
+ COALESCE(failing_policies.count, 0) AS total_issues_count,
+ hoi.version AS orbit_version,
+ hoi.desktop_version AS fleet_desktop_version,
+ hoi.scripts_enabled AS scripts_enabled
` + hostMDMSelect + `
FROM
hosts h
@@ -638,6 +641,7 @@ FROM
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
+ LEFT JOIN host_orbit_info hoi ON hoi.host_id = h.id
` + hostMDMJoin + `
JOIN (
SELECT
@@ -1092,7 +1096,7 @@ func (ds *Datastore) applyHostFilters(
if errors.Is(err, sql.ErrNoRows) {
return "", nil, ctxerr.Wrap(
ctx, &fleet.BadRequestError{
- Message: fmt.Sprintf("team is invalid"),
+ Message: "team is invalid",
InternalErr: err,
},
)
@@ -2090,6 +2094,7 @@ type hostWithMDMInfo struct {
MDMID *uint `db:"mdm_id"`
Name *string `db:"name"`
EncryptionKeyAvailable *bool `db:"encryption_key_available"`
+ DEPProfileAssignStatus *string `db:"dep_profile_assign_status"`
}
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
@@ -2148,7 +2153,8 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
COALESCE(hdek.decryptable, false) as encryption_key_available,
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
hd.encrypted as disk_encryption_enabled,
- t.name as team_name
+ t.name as team_name,
+ hdep.assign_profile_response AS dep_profile_assign_status
FROM
hosts h
LEFT OUTER JOIN
@@ -2158,7 +2164,7 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
LEFT OUTER JOIN
host_dep_assignments hdep
ON
- hdep.host_id = h.id
+ hdep.host_id = h.id AND hdep.deleted_at IS NULL
LEFT OUTER JOIN
mobile_device_management_solutions mdms
ON
@@ -2185,13 +2191,14 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
// leave MDMInfo nil unless it has mdm information
if hostWithMDM.HostID != nil {
host.MDMInfo = &fleet.HostMDM{
- HostID: *hostWithMDM.HostID,
- Enrolled: *hostWithMDM.Enrolled,
- ServerURL: *hostWithMDM.ServerURL,
- InstalledFromDep: *hostWithMDM.InstalledFromDep,
- IsServer: *hostWithMDM.IsServer,
- MDMID: hostWithMDM.MDMID,
- Name: *hostWithMDM.Name,
+ HostID: *hostWithMDM.HostID,
+ Enrolled: *hostWithMDM.Enrolled,
+ ServerURL: *hostWithMDM.ServerURL,
+ InstalledFromDep: *hostWithMDM.InstalledFromDep,
+ IsServer: *hostWithMDM.IsServer,
+ MDMID: hostWithMDM.MDMID,
+ Name: *hostWithMDM.Name,
+ DEPProfileAssignStatus: hostWithMDM.DEPProfileAssignStatus,
}
host.MDM = fleet.MDMHostData{
@@ -2260,7 +2267,8 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st
hm.mdm_id,
COALESCE(hm.is_server, false) AS is_server,
COALESCE(mdms.name, ?) AS name,
- IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet
+ IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
+ hdep.assign_profile_response AS dep_profile_assign_status
FROM
host_device_auth hda
INNER JOIN
@@ -2272,7 +2280,7 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st
LEFT OUTER JOIN
host_mdm hm ON hm.host_id = h.id
LEFT OUTER JOIN
- host_dep_assignments hdep ON hdep.host_id = h.id
+ host_dep_assignments hdep ON hdep.host_id = h.id AND hdep.deleted_at IS NULL
LEFT OUTER JOIN
mobile_device_management_solutions mdms ON hm.mdm_id = mdms.id
WHERE
@@ -2286,13 +2294,14 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st
// leave MDMInfo nil unless it has mdm information
if hostWithMDM.HostID != nil {
host.MDMInfo = &fleet.HostMDM{
- HostID: *hostWithMDM.HostID,
- Enrolled: *hostWithMDM.Enrolled,
- ServerURL: *hostWithMDM.ServerURL,
- InstalledFromDep: *hostWithMDM.InstalledFromDep,
- IsServer: *hostWithMDM.IsServer,
- MDMID: hostWithMDM.MDMID,
- Name: *hostWithMDM.Name,
+ HostID: *hostWithMDM.HostID,
+ Enrolled: *hostWithMDM.Enrolled,
+ ServerURL: *hostWithMDM.ServerURL,
+ InstalledFromDep: *hostWithMDM.InstalledFromDep,
+ IsServer: *hostWithMDM.IsServer,
+ MDMID: hostWithMDM.MDMID,
+ Name: *hostWithMDM.Name,
+ DEPProfileAssignStatus: hostWithMDM.DEPProfileAssignStatus,
}
}
return &host, nil
@@ -3688,15 +3697,36 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint)
return &key, nil
}
-func (ds *Datastore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
+func (ds *Datastore) SetOrUpdateHostOrbitInfo(
+ ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool,
+) error {
return ds.updateOrInsert(
ctx,
- `UPDATE host_orbit_info SET version = ? WHERE host_id = ?`,
- `INSERT INTO host_orbit_info (version, host_id) VALUES (?, ?)`,
- version, hostID,
+ `UPDATE host_orbit_info SET version = ?, desktop_version = ?, scripts_enabled = ? WHERE host_id = ?`,
+ `INSERT INTO host_orbit_info (version, desktop_version, scripts_enabled, host_id) VALUES (?, ?, ?, ?)`,
+ version, desktopVersion, scriptsEnabled, hostID,
)
}
+func (ds *Datastore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
+ var orbit fleet.HostOrbitInfo
+ err := sqlx.GetContext(
+ ctx, ds.reader(ctx), &orbit, `
+ SELECT
+ scripts_enabled
+ FROM
+ host_orbit_info
+ WHERE host_id = ?`, hostID,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ctxerr.Wrap(ctx, notFound("HostOrbitInfo").WithID(hostID))
+ }
+ return nil, ctxerr.Wrapf(ctx, err, "select host_orbit_info for host_id %d", hostID)
+ }
+ return &orbit, nil
+}
+
func (ds *Datastore) getOrInsertMDMSolution(ctx context.Context, serverURL string, mdmName string) (mdmID uint, err error) {
readStmt := ¶meterizedStmt{
Statement: `SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ?`,
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index 32e5507887..16c3d08bf5 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -163,6 +163,7 @@ func TestHosts(t *testing.T) {
{"ListHostsWithPagination", testListHostsWithPagination},
{"LastRestarted", testLastRestarted},
{"HostHealth", testHostHealth},
+ {"GetHostOrbitInfo", testGetHostOrbitInfo},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -211,16 +212,32 @@ func testUpdateHost(t *testing.T, ds *Datastore, updateHostFunc func(context.Con
assert.Equal(t, policyUpdatedAt.UTC(), host.PolicyUpdatedAt)
assert.NotNil(t, host.RefetchCriticalQueriesUntil)
assert.True(t, time.Now().Before(*host.RefetchCriticalQueriesUntil))
+ assert.Nil(t, host.OrbitVersion)
+ assert.Nil(t, host.DesktopVersion)
+ assert.Nil(t, host.ScriptsEnabled)
additionalJSON := json.RawMessage(`{"foobar": "bim"}`)
err = ds.SaveHostAdditional(context.Background(), host.ID, &additionalJSON)
require.NoError(t, err)
+ // set host orbit info
+ var (
+ orbitVersion = "1.1.0"
+ desktopVersion = "2.1.0"
+ )
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, orbitVersion, sql.NullString{String: desktopVersion, Valid: true},
+ sql.NullBool{Bool: true, Valid: true},
+ )
+ require.NoError(t, err)
host, err = ds.Host(context.Background(), host.ID)
require.NoError(t, err)
require.NotNil(t, host)
require.NotNil(t, host.Additional)
assert.Equal(t, additionalJSON, *host.Additional)
+ assert.Equal(t, orbitVersion, *host.OrbitVersion)
+ assert.Equal(t, desktopVersion, *host.DesktopVersion)
+ assert.True(t, *host.ScriptsEnabled)
err = updateHostFunc(context.Background(), host)
require.NoError(t, err)
@@ -229,10 +246,18 @@ func testUpdateHost(t *testing.T, ds *Datastore, updateHostFunc func(context.Con
err = updateHostFunc(context.Background(), host)
require.NoError(t, err)
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Valid: false},
+ )
+ require.NoError(t, err)
+
host, err = ds.Host(context.Background(), host.ID)
require.NoError(t, err)
require.NotNil(t, host)
require.Nil(t, host.RefetchCriticalQueriesUntil)
+ assert.Equal(t, orbitVersion, *host.OrbitVersion)
+ assert.Nil(t, host.DesktopVersion)
+ assert.Nil(t, host.ScriptsEnabled)
p, err := ds.NewPack(context.Background(), &fleet.Pack{
Name: t.Name(),
@@ -6515,7 +6540,9 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
err = ds.SetOrUpdateHostDisksSpace(context.Background(), host.ID, 12, 25, 40.0)
require.NoError(t, err)
// set host orbit info
- err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0")
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, "1.1.0", sql.NullString{String: "2.1.0", Valid: true}, sql.NullBool{Bool: true, Valid: true},
+ )
require.NoError(t, err)
// set an encryption key
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil)
@@ -7463,6 +7490,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.Equal(t, hSimple.ID, loadSimple.MDMInfo.HostID)
require.True(t, loadSimple.IsOsqueryEnrolled())
require.False(t, loadSimple.MDMInfo.IsPendingDEPFleetEnrollment())
+ require.False(t, loadSimple.IsEligibleForDEPMigration())
// create a host that will be pending enrollment in Fleet MDM
hFleet := createOrbitHost("fleet")
@@ -7477,6 +7505,8 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.True(t, loadFleet.IsOsqueryEnrolled())
require.True(t, loadFleet.MDMInfo.IsPendingDEPFleetEnrollment())
require.False(t, loadFleet.MDMInfo.IsServer)
+ require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus)
+ require.False(t, loadFleet.IsEligibleForDEPMigration())
// force its is_server mdm field to NULL, should be same as false
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@@ -7487,6 +7517,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, hFleet.ID, loadFleet.ID)
require.False(t, loadFleet.MDMInfo.IsServer)
+ require.False(t, loadFleet.IsEligibleForDEPMigration())
// fill in disk encryption information
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true))
@@ -7500,6 +7531,25 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
require.True(t, *loadFleet.DiskEncryptionEnabled)
+ require.False(t, loadFleet.IsEligibleForDEPMigration())
+ require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus)
+
+ // simulate the device being assigned to Fleet in ABM
+ err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*hFleet})
+ require.NoError(t, err)
+ loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
+ require.NoError(t, err)
+ require.Empty(t, loadFleet.MDMInfo.DEPProfileAssignStatus)
+
+ // simulate a failed JSON profile assignment
+ err = updateHostDEPAssignProfileResponses(
+ ctx, ds.writer(ctx), ds.logger,
+ "foo", []string{hFleet.HardwareSerial}, string(fleet.DEPAssignProfileResponseFailed),
+ )
+ require.NoError(t, err)
+ loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
+ require.NoError(t, err)
+ require.EqualValues(t, *loadFleet.MDMInfo.DEPProfileAssignStatus, fleet.DEPAssignProfileResponseFailed)
}
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) {
@@ -8707,3 +8757,41 @@ func testHostHealth(t *testing.T, ds *Datastore) {
require.Empty(t, hh.VulnerableSoftware)
require.Equal(t, h.TeamID, hh.TeamID)
}
+
+func testGetHostOrbitInfo(t *testing.T, ds *Datastore) {
+ host, err := ds.NewHost(
+ context.Background(), &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String("1"),
+ UUID: "1",
+ Hostname: "foo.local",
+ PrimaryIP: "192.168.1.1",
+ PrimaryMac: "30-65-EC-6F-C4-58",
+ },
+ )
+ require.NoError(t, err)
+ require.NotNil(t, host)
+
+ _, err = ds.GetHostOrbitInfo(context.Background(), host.ID)
+ require.True(t, fleet.IsNotFound(err))
+
+ orbitVersion := "1.1.0"
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Valid: false},
+ )
+ require.NoError(t, err)
+ hostOrbitInfo, err := ds.GetHostOrbitInfo(context.Background(), host.ID)
+ require.NoError(t, err)
+ assert.Nil(t, hostOrbitInfo.ScriptsEnabled)
+
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, orbitVersion, sql.NullString{Valid: false}, sql.NullBool{Bool: true, Valid: true},
+ )
+ require.NoError(t, err)
+ hostOrbitInfo, err = ds.GetHostOrbitInfo(context.Background(), host.ID)
+ require.NoError(t, err)
+ assert.True(t, *hostOrbitInfo.ScriptsEnabled)
+}
diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go
index c2075b3a5f..95cea4bc7d 100644
--- a/server/datastore/mysql/labels.go
+++ b/server/datastore/mysql/labels.go
@@ -15,6 +15,11 @@ import (
func (ds *Datastore) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpec) (err error) {
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ // TODO: do we want to allow on duplicate updating label_type or
+ // label_membership_type or should those always be immutable?
+ // are we ok depending solely on the caller to ensure that these fields
+ // are not changed?
+
sql := `
INSERT INTO labels (
name,
@@ -281,10 +286,15 @@ func labelDB(ctx context.Context, lid uint, q sqlx.QueryerContext) (*fleet.Label
}
// ListLabels returns all labels limited or sorted by fleet.ListOptions.
+// MatchQuery not supported
func (ds *Datastore) ListLabels(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Label, error) {
if opt.After != "" {
- return nil, &fleet.BadRequestError{Message: "after parameter is not supported"}
+ return nil, &fleet.BadRequestError{Message: "parameter 'after' is not supported"}
}
+ if opt.MatchQuery != "" {
+ return nil, &fleet.BadRequestError{Message: "parameter 'query' is not supported"}
+ }
+
query := fmt.Sprintf(`
SELECT *,
(SELECT COUNT(1) FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id) WHERE label_id = l.id AND %s) AS host_count
diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go
index 3380815902..2da15a65e3 100644
--- a/server/datastore/mysql/microsoft_mdm.go
+++ b/server/datastore/mysql/microsoft_mdm.go
@@ -1693,6 +1693,12 @@ ON DUPLICATE KEY UPDATE
keepNames = append(keepNames, p.Name)
}
}
+ for n := range mdm.FleetReservedProfileNames() {
+ if _, ok := incomingProfs[n]; !ok {
+ // always keep reserved profiles even if they're not incoming
+ keepNames = append(keepNames, n)
+ }
+ }
var (
stmt string
diff --git a/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go
index 2aa6de300b..e5595d7842 100644
--- a/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go
+++ b/server/datastore/mysql/migrations/tables/20240327115617_CreateTableNanoDDMRequests.go
@@ -14,9 +14,9 @@ func Up_20240327115617(tx *sql.Tx) error {
CREATE TABLE mdm_apple_declarative_requests (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- enrollment_id VARCHAR(255) NOT NULL,
+ enrollment_id VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
-- Should be one of "tokens", "declaration-items", "status", or "declaration/…/…" where the ellipses reference a declaration on the server
- message_type VARCHAR(255) NOT NULL,
+ message_type VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
-- json payload
raw_json TEXT,
PRIMARY KEY (id),
diff --git a/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go
new file mode 100644
index 0000000000..6ad62adc3d
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields.go
@@ -0,0 +1,27 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240408085837, Down_20240408085837)
+}
+
+func Up_20240408085837(tx *sql.Tx) error {
+ _, err := tx.Exec(
+ `ALTER TABLE host_orbit_info ADD COLUMN (
+ desktop_version VARCHAR(50) DEFAULT NULL,
+ scripts_enabled TINYINT(1) DEFAULT NULL
+ )`,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to add desktop_version and scripts_enabled to host_orbit_info: %w", err)
+ }
+ return nil
+}
+
+func Down_20240408085837(*sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go
new file mode 100644
index 0000000000..e62b00e7da
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240408085837_NewOrbitInfoFields_test.go
@@ -0,0 +1,56 @@
+package tables
+
+import (
+ "context"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestUp_20240408085837(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ // Insert data into orbit_info
+ id := 1
+ execNoErr(t, db, "INSERT INTO host_orbit_info (host_id, version) VALUES (?, ?)", id, "")
+
+ applyNext(t, db)
+
+ type orbitInfo struct {
+ HostID int64 `db:"host_id"`
+ Version string `db:"version"`
+ DesktopVersion *string `db:"desktop_version"`
+ ScriptsEnabled *bool `db:"scripts_enabled"`
+ }
+
+ var results []orbitInfo
+ err := db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id)
+ require.NoError(t, err)
+ assert.Len(t, results, 1)
+ assert.Nil(t, results[0].DesktopVersion)
+ assert.Nil(t, results[0].ScriptsEnabled)
+
+ id = 2
+ results = nil
+ execNoErr(t, db, "INSERT INTO host_orbit_info (host_id, version) VALUES (?, ?)", id, "")
+ err = db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id)
+ require.NoError(t, err)
+ assert.Len(t, results, 1)
+ assert.Nil(t, results[0].DesktopVersion)
+ assert.Nil(t, results[0].ScriptsEnabled)
+
+ id = 3
+ results = nil
+ const desktopVersion = "1.0.0"
+ const scriptsEnabled = true
+ execNoErr(
+ t, db, "INSERT INTO host_orbit_info (host_id, version, desktop_version, scripts_enabled) VALUES (?, ?, ?, ?)", id, "",
+ desktopVersion,
+ scriptsEnabled,
+ )
+ err = db.SelectContext(context.Background(), &results, `SELECT * FROM host_orbit_info WHERE host_id = ?`, id)
+ require.NoError(t, err)
+ assert.Len(t, results, 1)
+ assert.Equal(t, desktopVersion, *results[0].DesktopVersion)
+ assert.Equal(t, scriptsEnabled, *results[0].ScriptsEnabled)
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 1d8726bbfb..08da189437 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -421,6 +421,8 @@ CREATE TABLE `host_operating_system` (
CREATE TABLE `host_orbit_info` (
`host_id` int(10) unsigned NOT NULL,
`version` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `desktop_version` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `scripts_enabled` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`host_id`),
KEY `idx_host_orbit_info_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -883,9 +885,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=262 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) ENGINE=InnoDB AUTO_INCREMENT=263 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mobile_device_management_solutions` (
diff --git a/server/datastore/mysql/statistics_test.go b/server/datastore/mysql/statistics_test.go
index 0dd5a97b99..d7648c7859 100644
--- a/server/datastore/mysql/statistics_test.go
+++ b/server/datastore/mysql/statistics_test.go
@@ -2,6 +2,7 @@ package mysql
import (
"context"
+ "database/sql"
"encoding/json"
"testing"
"time"
@@ -97,7 +98,11 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// Create host_orbit_info record for test
- require.NoError(t, ds.SetOrUpdateHostOrbitInfo(ctx, h1.ID, "1.1.0"))
+ require.NoError(
+ t, ds.SetOrUpdateHostOrbitInfo(
+ ctx, h1.ID, "1.1.0", sql.NullString{String: "1.1.0", Valid: true}, sql.NullBool{Bool: true, Valid: true},
+ ),
+ )
// Create two new users for test
u1, err := ds.NewUser(ctx, &fleet.User{
diff --git a/server/datastore/mysql/vulnerabilities.go b/server/datastore/mysql/vulnerabilities.go
index 6b46da65ce..f9d57fc822 100644
--- a/server/datastore/mysql/vulnerabilities.go
+++ b/server/datastore/mysql/vulnerabilities.go
@@ -250,7 +250,7 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
selectStmt += " AND cm.cisa_known_exploit = 1"
}
- if match := opt.MatchQuery; match != "" {
+ if match := opt.ListOptions.MatchQuery; match != "" {
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
}
@@ -269,8 +269,8 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
// Prepare metadata
var metaData *fleet.PaginationMetadata
if opt.ListOptions.IncludeMetadata {
- metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0}
- if len(vulns) > int(opt.PerPage) {
+ metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0}
+ if len(vulns) > int(opt.ListOptions.PerPage) {
metaData.HasNextResults = true
vulns = vulns[:len(vulns)-1]
}
@@ -304,7 +304,7 @@ func (ds *Datastore) CountVulnerabilities(ctx context.Context, opt fleet.VulnLis
selectStmt = selectStmt + " AND cm.cisa_known_exploit = 1"
}
- if match := opt.MatchQuery; match != "" {
+ if match := opt.ListOptions.MatchQuery; match != "" {
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
}
diff --git a/server/datastore/mysql/vulnerabilities_test.go b/server/datastore/mysql/vulnerabilities_test.go
index 29236f4715..fca02a6bf7 100644
--- a/server/datastore/mysql/vulnerabilities_test.go
+++ b/server/datastore/mysql/vulnerabilities_test.go
@@ -342,7 +342,7 @@ func testVulnerabilitiesPagination(t *testing.T, ds *Datastore) {
require.False(t, meta.HasPreviousResults)
require.True(t, meta.HasNextResults)
- opts.Page = 1
+ opts.ListOptions.Page = 1
list, meta, err = ds.ListVulnerabilities(context.Background(), opts)
require.NoError(t, err)
require.Len(t, list, 2)
@@ -399,8 +399,8 @@ func testListVulnerabilitiesSort(t *testing.T, ds *Datastore) {
require.Equal(t, "CVE-2020-1237", list[3].CVE.CVE)
require.Equal(t, "CVE-2020-1236", list[4].CVE.CVE)
- opts.OrderKey = "published"
- opts.OrderDirection = fleet.OrderAscending
+ opts.ListOptions.OrderKey = "published"
+ opts.ListOptions.OrderDirection = fleet.OrderAscending
list, _, err = ds.ListVulnerabilities(context.Background(), opts)
require.NoError(t, err)
require.Len(t, list, 5)
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 048d9e48e6..0b6d83f228 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -3,6 +3,7 @@ package fleet
import (
"context"
"crypto/x509"
+ "database/sql"
"encoding/json"
"errors"
"io"
@@ -836,7 +837,11 @@ type Datastore interface {
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, cmdUUID string) (HostMDMProfileRetryCount, error)
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
- SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error
+ SetOrUpdateHostOrbitInfo(
+ ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool,
+ ) error
+
+ GetHostOrbitInfo(ctx context.Context, hostID uint) (*HostOrbitInfo, error)
ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*HostDeviceMapping, source string) error
diff --git a/server/fleet/errors.go b/server/fleet/errors.go
index ed519cf270..ed6bf27187 100644
--- a/server/fleet/errors.go
+++ b/server/fleet/errors.go
@@ -545,6 +545,7 @@ const (
RunScriptHostTimeoutErrMsg = "Fleet didn’t hear back from the host in under 5 minutes (timeout for live scripts). Fleet doesn’t know if the script ran because it didn’t receive the result. Please try again."
RunScriptScriptsDisabledGloballyErrMsg = "Running scripts is disabled in organization settings."
RunScriptDisabledErrMsg = "Scripts are disabled for this host. To run scripts, deploy the fleetd agent with scripts enabled."
+ RunScriptsOrbitDisabledErrMsg = "Couldn't run script. To run a script, deploy the fleetd agent with --enable-scripts."
RunScriptScriptTimeoutErrMsg = "Timeout. Fleet stopped the script after 5 minutes to protect host performance."
RunScriptAsyncScriptEnqueuedErrMsg = "Script is running or will run when the host comes online."
RunScripSavedMaxLenErrMsg = "Script is too large. It's limited to 500,000 characters (approximately 10,000 lines)."
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index f81cfbb5b4..809ab12d54 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -255,6 +255,9 @@ type Host struct {
// Platform is the host's platform as defined by osquery's os_version.platform.
Platform string `json:"platform" csv:"platform"`
OsqueryVersion string `json:"osquery_version" db:"osquery_version" csv:"osquery_version"`
+ OrbitVersion *string `json:"orbit_version" db:"orbit_version" csv:"orbit_version"`
+ DesktopVersion *string `json:"fleet_desktop_version" db:"fleet_desktop_version" csv:"fleet_desktop_version"`
+ ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled" csv:"scripts_enabled"`
OSVersion string `json:"os_version" db:"os_version" csv:"os_version"`
Build string `json:"build" csv:"build"`
PlatformLike string `json:"platform_like" db:"platform_like" csv:"platform_like"`
@@ -356,6 +359,11 @@ type Host struct {
Policies *[]*HostPolicy `json:"policies,omitempty" csv:"-"`
}
+// HostOrbitInfo maps to the host_orbit_info table in the database, which maps to the orbit_info agent table.
+type HostOrbitInfo struct {
+ ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"`
+}
+
// HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with
// the same name, see the comments/docs for the Host field above.
type HostHealth struct {
@@ -382,6 +390,8 @@ type MDMHostData struct {
// DEPProfileError is a boolean representing whether Fleet received a "FAILED" response when
// attempting to assign a DEP profile for the host.
// See https://developer.apple.com/documentation/devicemanagement/assignprofileresponse
+ //
+ // It is not filled in by all host-returning datastore methods.
DEPProfileError bool `json:"dep_profile_error" db:"dep_profile_error" csv:"mdm.dep_profile_error"`
// ServerURL is the server_url stored in the host_mdm table, loaded by
// JOIN in datastore
@@ -650,6 +660,7 @@ func (h *Host) IsDEPAssignedToFleet() bool {
func (h *Host) IsEligibleForDEPMigration() bool {
return h.IsOsqueryEnrolled() &&
h.IsDEPAssignedToFleet() &&
+ h.MDMInfo.HasJSONProfileAssigned() &&
h.MDMInfo.IsEnrolledInThirdPartyMDM()
}
@@ -899,13 +910,14 @@ type HostMunkiInfo struct {
// used by a host. Note that it uses a different JSON representation than its
// struct - it implements a custom JSON marshaler.
type HostMDM struct {
- HostID uint `db:"host_id" json:"-" csv:"-"`
- Enrolled bool `db:"enrolled" json:"-" csv:"-"`
- ServerURL string `db:"server_url" json:"-" csv:"-"`
- InstalledFromDep bool `db:"installed_from_dep" json:"-" csv:"-"`
- IsServer bool `db:"is_server" json:"-" csv:"-"`
- MDMID *uint `db:"mdm_id" json:"-" csv:"-"`
- Name string `db:"name" json:"-" csv:"-"`
+ HostID uint `db:"host_id" json:"-" csv:"-"`
+ Enrolled bool `db:"enrolled" json:"-" csv:"-"`
+ ServerURL string `db:"server_url" json:"-" csv:"-"`
+ InstalledFromDep bool `db:"installed_from_dep" json:"-" csv:"-"`
+ IsServer bool `db:"is_server" json:"-" csv:"-"`
+ MDMID *uint `db:"mdm_id" json:"-" csv:"-"`
+ Name string `db:"name" json:"-" csv:"-"`
+ DEPProfileAssignStatus *string `db:"dep_profile_assign_status" json:"-" csv:"-"`
}
// IsPendingDEPFleetEnrollment returns true if the host's MDM information
@@ -929,6 +941,17 @@ func (h *HostMDM) IsEnrolledInThirdPartyMDM() bool {
return h.Enrolled && h.Name != WellKnownMDMFleet
}
+// HasJSONProfileAssigned returns true if Fleet has assigned an ADE/DEP JSON
+// profile to the host, and it'll be enrolled into Fleet the next time the host
+// performs automatic enrollment.
+func (h *HostMDM) HasJSONProfileAssigned() bool {
+ // TODO: get rid of h != nil with a better solution once we stablish
+ // the pattern for dealing with a nil HostMDM
+ return h != nil &&
+ h.DEPProfileAssignStatus != nil &&
+ *h.DEPProfileAssignStatus == string(DEPAssignProfileResponseSuccess)
+}
+
// IsDEPCapable returns true if and only if the host's MDM information
// indicates that the device is capable of doing DEP/AEP enrollments.
func (h *HostMDM) IsDEPCapable() bool {
diff --git a/server/fleet/hosts_test.go b/server/fleet/hosts_test.go
index 18c4fc42b6..83c2e2519c 100644
--- a/server/fleet/hosts_test.go
+++ b/server/fleet/hosts_test.go
@@ -311,3 +311,137 @@ func TestIsEligibleForBitLockerEncryption(t *testing.T) {
hostThatNeedsEnforcement.MDMInfo.Enrolled = true
require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption())
}
+
+func TestIsEligibleForDEPMigration(t *testing.T) {
+ testCases := []struct {
+ name string
+ osqueryHostID *string
+ depAssignedToFleet *bool
+ depProfileResponse DEPAssignProfileResponseStatus
+ enrolledInThirdPartyMDM bool
+ expected bool
+ }{
+ {
+ name: "Eligible for DEP migration",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: DEPAssignProfileResponseSuccess,
+ enrolledInThirdPartyMDM: true,
+ expected: true,
+ },
+ {
+ name: "Not eligible - osqueryHostID nil",
+ osqueryHostID: nil,
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: DEPAssignProfileResponseSuccess,
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ {
+ name: "Not eligible - not DEP assigned to Fleet",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(false),
+ depProfileResponse: DEPAssignProfileResponseSuccess,
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ {
+ name: "Not eligible - not enrolled in third-party MDM",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: DEPAssignProfileResponseSuccess,
+ enrolledInThirdPartyMDM: false,
+ expected: false,
+ },
+ {
+ name: "Not eligible - not DEP assigned and DEP profile failed",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(false),
+ depProfileResponse: DEPAssignProfileResponseNotAccessible,
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ {
+ name: "Not eligible - DEP assigned and DEP profile failed",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: DEPAssignProfileResponseFailed,
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ {
+ name: "Not eligible - DEP assigned but not response yet",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: "",
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ {
+ name: "Not eligible - DEP assigned but not accessible",
+ osqueryHostID: ptr.String("some-id"),
+ depAssignedToFleet: ptr.Bool(true),
+ depProfileResponse: DEPAssignProfileResponseNotAccessible,
+ enrolledInThirdPartyMDM: true,
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ host := &Host{
+ OsqueryHostID: tc.osqueryHostID,
+ DEPAssignedToFleet: tc.depAssignedToFleet,
+ MDMInfo: &HostMDM{
+ Enrolled: tc.enrolledInThirdPartyMDM,
+ Name: "Some MDM",
+ DEPProfileAssignStatus: ptr.String(string(tc.depProfileResponse)),
+ },
+ }
+
+ require.Equal(t, tc.expected, host.IsEligibleForDEPMigration())
+ })
+ }
+}
+
+func TestHasJSONProfileAssigned(t *testing.T) {
+ testCases := []struct {
+ name string
+ hostMDM *HostMDM
+ expected bool
+ }{
+ {
+ name: "nil HostMDM",
+ hostMDM: nil,
+ expected: false,
+ },
+ {
+ name: "nil DEPProfileAssignStatus",
+ hostMDM: &HostMDM{
+ DEPProfileAssignStatus: nil,
+ },
+ expected: false,
+ },
+ {
+ name: "DEPProfileAssignStatus not successful",
+ hostMDM: &HostMDM{
+ DEPProfileAssignStatus: new(string),
+ },
+ expected: false,
+ },
+ {
+ name: "DEPProfileAssignStatus successful",
+ hostMDM: &HostMDM{
+ DEPProfileAssignStatus: ptr.String(string(DEPAssignProfileResponseSuccess)),
+ },
+ expected: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ result := tc.hostMDM.HasJSONProfileAssigned()
+ require.Equal(t, tc.expected, result)
+ })
+ }
+}
diff --git a/server/fleet/labels.go b/server/fleet/labels.go
index c754c80767..5925bf2c01 100644
--- a/server/fleet/labels.go
+++ b/server/fleet/labels.go
@@ -131,3 +131,29 @@ type LabelSpec struct {
LabelMembershipType LabelMembershipType `json:"label_membership_type" db:"label_membership_type"`
Hosts []string `json:"hosts,omitempty"`
}
+
+const (
+ BuiltinLabelNameAllHosts = "All Hosts"
+ BuiltinLabelNameMacOS = "macOS"
+ BuiltinLabelNameUbuntuLinux = "Ubuntu Linux"
+ BuiltinLabelNameCentOSLinux = "CentOS Linux"
+ BuiltinLabelNameWindows = "MS Windows"
+ BuiltinLabelNameRedHatLinux = "Red Hat Linux"
+ BuiltinLabelNameAllLinux = "All Linux"
+ BuiltinLabelNameChrome = "chrome"
+)
+
+// ReservedLabelNames returns a map of label name strings
+// that are reserved by Fleet.
+func ReservedLabelNames() map[string]struct{} {
+ return map[string]struct{}{
+ BuiltinLabelNameAllHosts: {},
+ BuiltinLabelNameMacOS: {},
+ BuiltinLabelNameUbuntuLinux: {},
+ BuiltinLabelNameCentOSLinux: {},
+ BuiltinLabelNameWindows: {},
+ BuiltinLabelNameRedHatLinux: {},
+ BuiltinLabelNameAllLinux: {},
+ BuiltinLabelNameChrome: {},
+ }
+}
diff --git a/server/fleet/vulnerabilities.go b/server/fleet/vulnerabilities.go
index b8980cb93a..a15468f31f 100644
--- a/server/fleet/vulnerabilities.go
+++ b/server/fleet/vulnerabilities.go
@@ -136,7 +136,8 @@ type VulnerabilityWithMetadata struct {
}
type VulnListOptions struct {
- ListOptions
+ // ListOptions cannot be embedded in order to unmarshall with validation.
+ ListOptions ListOptions `url:"list_options"`
IsEE bool
ValidSortColumns []string
TeamID uint `query:"team_id,optional"`
@@ -144,11 +145,11 @@ type VulnListOptions struct {
}
func (opt VulnListOptions) HasValidSortColumn() bool {
- if opt.OrderKey == "" || len(opt.ValidSortColumns) == 0 {
+ if opt.ListOptions.OrderKey == "" || len(opt.ValidSortColumns) == 0 {
return true
}
for _, c := range opt.ValidSortColumns {
- if c == opt.OrderKey {
+ if c == opt.ListOptions.OrderKey {
return true
}
}
diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go
index bc8b2bc6e4..40b669da93 100644
--- a/server/mdm/apple/util.go
+++ b/server/mdm/apple/util.go
@@ -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)
//
diff --git a/server/mdm/internal/commonmdm/commonmdm_test.go b/server/mdm/internal/commonmdm/commonmdm_test.go
new file mode 100644
index 0000000000..8423983163
--- /dev/null
+++ b/server/mdm/internal/commonmdm/commonmdm_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go
index e2888fbc50..9adc0c1988 100644
--- a/server/mdm/mdm.go
+++ b/server/mdm/mdm.go
@@ -108,3 +108,9 @@ func FleetReservedProfileNames() map[string]struct{} {
func ListFleetReservedWindowsProfileNames() []string {
return []string{FleetWindowsOSUpdatesProfileName}
}
+
+// ListFleetReservedMacOSProfileNames returns a list of PayloadDisplayName strings
+// that are reserved by Fleet for macOS.
+func ListFleetReservedMacOSProfileNames() []string {
+ return []string{FleetFileVaultProfileName, FleetdConfigProfileName}
+}
diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go
index 227f311ecc..a8a9254bd8 100644
--- a/server/mdm/microsoft/microsoft_mdm.go
+++ b/server/mdm/microsoft/microsoft_mdm.go
@@ -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)
}
diff --git a/server/mdm/microsoft/wstep_csr_test.go b/server/mdm/microsoft/wstep_csr_test.go
new file mode 100644
index 0000000000..f5d5e59800
--- /dev/null
+++ b/server/mdm/microsoft/wstep_csr_test.go
@@ -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==",
+}
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index bad684e93d..2fb532210e 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -5,6 +5,7 @@ package mock
import (
"context"
"crypto/x509"
+ "database/sql"
"encoding/json"
"math/big"
"sync"
@@ -586,7 +587,9 @@ type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, host *fleet.Hos
type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error)
-type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
+type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error
+
+type GetHostOrbitInfoFunc func(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error)
type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error
@@ -1758,6 +1761,9 @@ type DataStore struct {
SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc
SetOrUpdateHostOrbitInfoFuncInvoked bool
+ GetHostOrbitInfoFunc GetHostOrbitInfoFunc
+ GetHostOrbitInfoFuncInvoked bool
+
ReplaceHostDeviceMappingFunc ReplaceHostDeviceMappingFunc
ReplaceHostDeviceMappingFuncInvoked bool
@@ -4220,11 +4226,18 @@ func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context
return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, host, cmdUUID)
}
-func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
+func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool) error {
s.mu.Lock()
s.SetOrUpdateHostOrbitInfoFuncInvoked = true
s.mu.Unlock()
- return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version)
+ return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version, desktopVersion, scriptsEnabled)
+}
+
+func (s *DataStore) GetHostOrbitInfo(ctx context.Context, hostID uint) (*fleet.HostOrbitInfo, error) {
+ s.mu.Lock()
+ s.GetHostOrbitInfoFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetHostOrbitInfoFunc(ctx, hostID)
}
func (s *DataStore) ReplaceHostDeviceMapping(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping, source string) error {
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index b2457d772d..d54e0882c1 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -821,7 +821,7 @@ func (svc *Service) validateMDM(
// TODO: Should we validate MDM configured on here too?
if mdm.MacOSMigration.Enable {
- if license.Tier != fleet.TierPremium {
+ if !license.IsPremium() {
invalid.Append("macos_migration.enable", ErrMissingLicense.Error())
return nil
}
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index ddbd5e1e4a..2d6e847f6a 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -3053,10 +3053,7 @@ func (svc *Service) maybeRestorePendingDEPHost(ctx context.Context, host *fleet.
return nil
}
- license, ok := license.FromContext(ctx)
- if !ok {
- return ctxerr.New(ctx, "maybe restore pending DEP host: missing license")
- } else if license.Tier != fleet.TierPremium {
+ if !license.IsPremium(ctx) {
// only premium tier supports DEP so nothing more to do
return nil
}
diff --git a/server/service/devices.go b/server/service/devices.go
index a96b3bb155..ea8069efc3 100644
--- a/server/service/devices.go
+++ b/server/service/devices.go
@@ -11,7 +11,6 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server/contexts/authz"
- authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
@@ -176,7 +175,7 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S
}
func (svc *Service) GetHostDEPAssignment(ctx context.Context, host *fleet.Host) (*fleet.HostDEPAssignment, error) {
- alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken)
+ alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken)
if !alreadyAuthd {
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
diff --git a/server/service/devices_test.go b/server/service/devices_test.go
index e937695996..3fc3ba127c 100644
--- a/server/service/devices_test.go
+++ b/server/service/devices_test.go
@@ -114,10 +114,11 @@ func TestGetFleetDesktopSummary(t *testing.T) {
OsqueryHostID: ptr.String("test"),
DEPAssignedToFleet: &c.depAssigned,
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: true,
- Name: fleet.WellKnownMDMIntune,
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
}})
sum, err := svc.GetFleetDesktopSummary(ctx)
require.NoError(t, err)
@@ -206,10 +207,11 @@ func TestGetFleetDesktopSummary(t *testing.T) {
OsqueryHostID: ptr.String("test"),
DEPAssignedToFleet: &c.depAssigned,
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: false,
- Name: fleet.WellKnownMDMFleet,
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: false,
+ Name: fleet.WellKnownMDMFleet,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
}})
sum, err := svc.GetFleetDesktopSummary(ctx)
require.NoError(t, err)
@@ -262,10 +264,11 @@ func TestGetFleetDesktopSummary(t *testing.T) {
OsqueryHostID: ptr.String("test"),
DEPAssignedToFleet: ptr.Bool(false),
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: false,
- Enrolled: true,
- Name: fleet.WellKnownMDMIntune,
+ IsServer: false,
+ InstalledFromDep: false,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
}},
err: nil,
out: fleet.DesktopNotifications{
@@ -279,10 +282,11 @@ func TestGetFleetDesktopSummary(t *testing.T) {
DEPAssignedToFleet: ptr.Bool(true),
OsqueryHostID: ptr.String("test"),
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: false,
- Name: fleet.WellKnownMDMFleet,
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: false,
+ Name: fleet.WellKnownMDMFleet,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
}},
err: nil,
out: fleet.DesktopNotifications{
@@ -296,10 +300,83 @@ func TestGetFleetDesktopSummary(t *testing.T) {
DEPAssignedToFleet: ptr.Bool(true),
OsqueryHostID: ptr.String("test"),
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: true,
- Name: fleet.WellKnownMDMFleet,
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMFleet,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
+ }},
+ err: nil,
+ out: fleet.DesktopNotifications{
+ NeedsMDMMigration: false,
+ RenewEnrollmentProfile: false,
+ },
+ },
+ {
+ name: "failed ADE assignment status",
+ host: &fleet.Host{
+ DEPAssignedToFleet: ptr.Bool(true),
+ OsqueryHostID: ptr.String("test"),
+ MDMInfo: &fleet.HostMDM{
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseFailed)),
+ }},
+ err: nil,
+ out: fleet.DesktopNotifications{
+ NeedsMDMMigration: false,
+ RenewEnrollmentProfile: false,
+ },
+ },
+ {
+ name: "not accessible ADE assignment status",
+ host: &fleet.Host{
+ DEPAssignedToFleet: ptr.Bool(true),
+ OsqueryHostID: ptr.String("test"),
+ MDMInfo: &fleet.HostMDM{
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseNotAccessible)),
+ }},
+ err: nil,
+ out: fleet.DesktopNotifications{
+ NeedsMDMMigration: false,
+ RenewEnrollmentProfile: false,
+ },
+ },
+ {
+ name: "empty ADE assignment status",
+ host: &fleet.Host{
+ DEPAssignedToFleet: ptr.Bool(true),
+ OsqueryHostID: ptr.String("test"),
+ MDMInfo: &fleet.HostMDM{
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(""),
+ }},
+ err: nil,
+ out: fleet.DesktopNotifications{
+ NeedsMDMMigration: false,
+ RenewEnrollmentProfile: false,
+ },
+ },
+ {
+ name: "nil ADE assignment status",
+ host: &fleet.Host{
+ DEPAssignedToFleet: ptr.Bool(true),
+ OsqueryHostID: ptr.String("test"),
+ MDMInfo: &fleet.HostMDM{
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: nil,
}},
err: nil,
out: fleet.DesktopNotifications{
@@ -313,10 +390,11 @@ func TestGetFleetDesktopSummary(t *testing.T) {
DEPAssignedToFleet: ptr.Bool(true),
OsqueryHostID: ptr.String("test"),
MDMInfo: &fleet.HostMDM{
- IsServer: false,
- InstalledFromDep: true,
- Enrolled: true,
- Name: fleet.WellKnownMDMIntune,
+ IsServer: false,
+ InstalledFromDep: true,
+ Enrolled: true,
+ Name: fleet.WellKnownMDMIntune,
+ DEPProfileAssignStatus: ptr.String(string(fleet.DEPAssignProfileResponseSuccess)),
}},
err: nil,
out: fleet.DesktopNotifications{
diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go
index 712f4519f5..97ca541b87 100644
--- a/server/service/endpoint_utils.go
+++ b/server/service/endpoint_utils.go
@@ -94,7 +94,7 @@ type bodyDecoder interface {
// struct has at least 1 json tag it'll unmarshall the body. If the struct has
// a `url` tag with value list_options it'll gather fleet.ListOptions from the
// URL (similarly for host_options, carve_options, user_options that derive
-// from the common list_options).
+// from the common list_options). Note that these behaviors do not work for embedded structs.
//
// Finally, any other `url` tag will be treated as a path variable (of the form
// /path/{name} in the route's path) from the URL path pattern, and it'll be
@@ -172,7 +172,6 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc {
if err != nil {
return nil, err
}
-
switch urlTagValue {
case "list_options":
opts, err := listOptionsFromRequest(r)
diff --git a/server/service/global_policies.go b/server/service/global_policies.go
index a0d3afff15..2cecd274e9 100644
--- a/server/service/global_policies.go
+++ b/server/service/global_policies.go
@@ -157,7 +157,7 @@ func (svc Service) GetPolicyByIDQueries(ctx context.Context, policyID uint) (*fl
// ///////////////////////////////////////////////////////////////////////////////
type countGlobalPoliciesRequest struct {
- fleet.ListOptions `url:"list_options"`
+ ListOptions fleet.ListOptions `url:"list_options"`
}
type countGlobalPoliciesResponse struct {
Count int `json:"count"`
@@ -168,7 +168,7 @@ func (r countGlobalPoliciesResponse) error() error { return r.Err }
func countGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*countGlobalPoliciesRequest)
- resp, err := svc.CountGlobalPolicies(ctx, req.MatchQuery)
+ resp, err := svc.CountGlobalPolicies(ctx, req.ListOptions.MatchQuery)
if err != nil {
return countGlobalPoliciesResponse{Err: err}, nil
}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 87640f3ed1..273f4ec513 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -690,6 +690,11 @@ func (s *integrationTestSuite) TestTranslator() {
require.Len(t, payload.List, 1)
assert.Equal(t, s.users[payload.List[0].Payload.Identifier].ID, payload.List[0].Payload.ID)
+
+ // empty body
+ s.DoJSON("POST", "/api/latest/fleet/translate", &translatorRequest{}, http.StatusBadRequest, &payload)
+
+ s.DoJSON("POST", "/api/latest/fleet/translate", &translatorRequest{List: []fleet.TranslatePayload{{Type: "notavalidtype", Payload: fleet.StringIdentifierToIDPayload{}}}}, http.StatusBadRequest, &payload)
}
func (s *integrationTestSuite) TestVulnerableSoftware() {
@@ -895,6 +900,7 @@ func (s *integrationTestSuite) TestVulnerableSoftware() {
func (s *integrationTestSuite) TestGlobalPolicies() {
t := s.T()
+ // create 3 hosts
for i := 0; i < 3; i++ {
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
@@ -918,6 +924,7 @@ func (s *integrationTestSuite) TestGlobalPolicies() {
})
require.NoError(t, err)
+ // create a global policy
gpParams := globalPolicyRequest{
QueryID: &qr.ID,
Resolution: "some global resolution",
@@ -931,6 +938,7 @@ func (s *integrationTestSuite) TestGlobalPolicies() {
require.NotNil(t, gpResp.Policy.Resolution)
assert.Equal(t, "some global resolution", *gpResp.Policy.Resolution)
+ // list global policies
policiesResponse := listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 1)
@@ -969,6 +977,27 @@ func (s *integrationTestSuite) TestGlobalPolicies() {
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
require.Len(t, listHostsResp.Hosts, 1)
+ // count global policies
+ cGPRes := countGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes)
+ assert.Equal(t, 1, cGPRes.Count)
+
+ // count global policies with matching search query
+ cGPRes = countGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", "estQue")
+ assert.Equal(t, 1, cGPRes.Count)
+
+ // count global policies with matching search query containing leading/trailing whitespace
+ cGPRes = countGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", " estQue ")
+ assert.Equal(t, 1, cGPRes.Count)
+
+ // count global policies with non-matching search query
+ cGPRes = countGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &cGPRes, "query", "Query4")
+ assert.Equal(t, 0, cGPRes.Count)
+
+ // delete the policy
deletePolicyParams := deleteGlobalPoliciesRequest{IDs: []uint{policiesResponse.Policies[0].ID}}
deletePolicyResp := deleteGlobalPoliciesResponse{}
s.DoJSON("POST", "/api/latest/fleet/policies/delete", deletePolicyParams, http.StatusOK, &deletePolicyResp)
@@ -1208,6 +1237,62 @@ func (s *integrationTestSuite) TestHostsCount() {
)
assert.Equal(t, 1, resp.Count)
+ // there are 3 hosts, whos names end with ...local0, ...local1, ...local2
+ // query by host name
+
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "query", "local0",
+ )
+ assert.Equal(t, 1, resp.Count)
+
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "query", "local",
+ )
+ assert.Equal(t, 3, resp.Count)
+
+ // query by host name with leading/trailing whitespace
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "query", " local0 ",
+ )
+ assert.Equal(t, 1, resp.Count)
+
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "query", " local ",
+ )
+ assert.Equal(t, 3, resp.Count)
+
+ // query by host name leading/trailing whitespace and label
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "label_id", fmt.Sprint(label.ID),
+ "query", " local0 ",
+ )
+ assert.Equal(t, 1, resp.Count)
+
+ req = countHostsRequest{}
+ resp = countHostsResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/hosts/count", req, http.StatusOK, &resp,
+ "label_id", fmt.Sprint(label.ID),
+ // only host 0 has the label
+ "query", " local1 ",
+ )
+ assert.Equal(t, 0, resp.Count)
+
// filter by low_disk_space criteria is ignored (premium-only filter)
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &resp, "low_disk_space", "32")
require.Equal(t, len(hosts), resp.Count)
@@ -1773,6 +1858,17 @@ func (s *integrationTestSuite) TestListHosts() {
assert.Equal(t, "pass", policies[1].Response)
}
}
+
+ // there are 3 hosts, whos names end with ...local0, ...local1, ...local2
+ resp = listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", "local0")
+ require.Len(t, resp.Hosts, 1)
+ require.Contains(t, resp.Hosts[0].Hostname, "local0")
+ resp = listHostsResponse{}
+ // now with leading/trailing whitespace
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "query", " local0 ")
+ require.Len(t, resp.Hosts, 1)
+ require.Contains(t, resp.Hosts[0].Hostname, "local0")
}
func (s *integrationTestSuite) TestInvites() {
@@ -1849,6 +1945,26 @@ func (s *integrationTestSuite) TestInvites() {
require.Len(t, listResp.Invites, 1)
require.Equal(t, validInvite.ID, listResp.Invites[0].ID)
+ // list invites filtered by search query with leading/trailing whitespace
+ // matches name
+ listResp = listInvitesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " some name ")
+ require.Len(t, listResp.Invites, 1)
+ require.Equal(t, validInvite.ID, listResp.Invites[0].ID)
+
+ // list invites filtered by search query with leading/trailing whitespace
+ // matches email
+ listResp = listInvitesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " some email ")
+ require.Len(t, listResp.Invites, 1)
+ require.Equal(t, validInvite.ID, listResp.Invites[0].ID)
+
+ // list invites filtered by search query with leading/trailing whitespace
+ // matches nothing
+ listResp = listInvitesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "query", " no match ")
+ require.Len(t, listResp.Invites, 0)
+
// list invites, next page is empty
listResp = listInvitesResponse{}
s.DoJSON("GET", "/api/latest/fleet/invites", nil, http.StatusOK, &listResp, "page", "1", "per_page", "2")
@@ -2349,8 +2465,9 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
err = s.ds.AddHostsToTeam(context.Background(), &team1.ID, hosts)
require.NoError(t, err)
+ tpName := "TestPolicy3"
tpParams := teamPolicyRequest{
- Name: "TestQuery3",
+ Name: tpName,
Query: "select * from osquery;",
Description: "Some description",
Resolution: "some team resolution",
@@ -2360,7 +2477,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp)
require.NotNil(t, tpResp.Policy)
require.NotEmpty(t, tpResp.Policy.ID)
- assert.Equal(t, "TestQuery3", tpResp.Policy.Name)
+ assert.Equal(t, tpName, tpResp.Policy.Name)
assert.Equal(t, "select * from osquery;", tpResp.Policy.Query)
assert.Equal(t, "Some description", tpResp.Policy.Description)
require.NotNil(t, tpResp.Policy.Resolution)
@@ -2369,9 +2486,10 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
assert.Equal(t, "Test Name admin1@example.com", tpResp.Policy.AuthorName)
assert.Equal(t, "admin1@example.com", tpResp.Policy.AuthorEmail)
+ tpNameNew := "TestPolicy4"
mtpParams := modifyTeamPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
- Name: ptr.String("TestQuery4"),
+ Name: ptr.String(tpNameNew),
Query: ptr.String("select * from osquery_info;"),
Description: ptr.String("Some description updated"),
Resolution: ptr.String("some team resolution updated"),
@@ -2380,7 +2498,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
mtpResp := modifyTeamPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), mtpParams, http.StatusOK, &mtpResp)
require.NotNil(t, mtpResp.Policy)
- assert.Equal(t, "TestQuery4", mtpResp.Policy.Name)
+ assert.Equal(t, tpNameNew, mtpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", mtpResp.Policy.Query)
assert.Equal(t, "Some description updated", mtpResp.Policy.Description)
require.NotNil(t, mtpResp.Policy.Resolution)
@@ -2390,7 +2508,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
gtpResp := getPolicyByIDResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, >pResp)
require.NotNil(t, gtpResp.Policy)
- assert.Equal(t, "TestQuery4", gtpResp.Policy.Name)
+ assert.Equal(t, tpNameNew, gtpResp.Policy.Name)
assert.Equal(t, "select * from osquery_info;", gtpResp.Policy.Query)
assert.Equal(t, "Some description updated", gtpResp.Policy.Description)
require.NotNil(t, gtpResp.Policy.Resolution)
@@ -2400,7 +2518,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
policiesResponse := listTeamPoliciesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &policiesResponse)
require.Len(t, policiesResponse.Policies, 1)
- assert.Equal(t, "TestQuery4", policiesResponse.Policies[0].Name)
+ assert.Equal(t, tpNameNew, policiesResponse.Policies[0].Name)
assert.Equal(t, "select * from osquery_info;", policiesResponse.Policies[0].Query)
assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description)
require.NotNil(t, policiesResponse.Policies[0].Resolution)
@@ -2408,6 +2526,23 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
assert.Equal(t, "darwin", policiesResponse.Policies[0].Platform)
require.Len(t, policiesResponse.InheritedPolicies, 0)
+ // test team policy count endpoint
+ tpCountResp := countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp)
+ assert.Equal(t, 1, tpCountResp.Count)
+
+ tpCountResp = countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", tpNameNew)
+ assert.Equal(t, 1, tpCountResp.Count)
+
+ tpCountResp = countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", " "+tpNameNew+" ")
+ assert.Equal(t, 1, tpCountResp.Count)
+
+ tpCountResp = countTeamPoliciesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tpCountResp, "query", " nomatch")
+ assert.Equal(t, 0, tpCountResp.Count)
+
listHostsURL := fmt.Sprintf("/api/latest/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
listHostsResp := listHostsResponse{}
s.DoJSON("GET", listHostsURL, nil, http.StatusOK, &listHostsResp)
@@ -2930,6 +3065,20 @@ func (s *integrationTestSuite) TestScheduledQueries() {
require.Len(t, listQryResp.Queries, 1)
assert.Equal(t, query.Name, listQryResp.Queries[0].Name)
+ // listing with matching name returns that query
+ s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", query.Name)
+ require.Len(t, listQryResp.Queries, 1)
+ assert.Equal(t, query.Name, listQryResp.Queries[0].Name)
+
+ // listing with matching name plus whitespace returns that query
+ s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", " "+query.Name+" ")
+ require.Len(t, listQryResp.Queries, 1)
+ assert.Equal(t, query.Name, listQryResp.Queries[0].Name)
+
+ // listing with non-matching name returns nothing
+ s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", " nomatch")
+ require.Len(t, listQryResp.Queries, 0)
+
// Return that query by name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries?query=%s", query.Name), nil, http.StatusOK, &listQryResp)
require.Len(t, listQryResp.Queries, 1)
@@ -3581,19 +3730,25 @@ func (s *integrationTestSuite) TestLabels() {
t := s.T()
// list labels, has the built-in ones
+ builtinsMap := fleet.ReservedLabelNames()
var listResp listLabelsResponse
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp)
assert.True(t, len(listResp.Labels) > 0)
for _, lbl := range listResp.Labels {
+ _, ok := builtinsMap[lbl.Name]
+ assert.True(t, ok)
assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType)
}
builtInsCount := len(listResp.Labels)
+ require.Equal(t, builtInsCount, len(builtinsMap))
// labels summary has the built-in ones
var summaryResp getLabelsSummaryResponse
s.DoJSON("GET", "/api/latest/fleet/labels/summary", nil, http.StatusOK, &summaryResp)
assert.Len(t, summaryResp.Labels, builtInsCount)
for _, lbl := range summaryResp.Labels {
+ _, ok := builtinsMap[lbl.Name]
+ assert.True(t, ok)
assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType)
}
@@ -3601,6 +3756,11 @@ func (s *integrationTestSuite) TestLabels() {
var createResp createLabelResponse
s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Query: ptr.String("select 1")}, http.StatusUnprocessableEntity, &createResp)
+ // create invalid label, conflicts with builtin name
+ for n := range builtinsMap {
+ s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(n), Query: ptr.String("select 1")}, http.StatusUnprocessableEntity, &createResp)
+ }
+
// create a valid label
s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(t.Name()), Query: ptr.String("select 1")}, http.StatusOK, &createResp)
assert.NotZero(t, createResp.Label.ID)
@@ -3621,6 +3781,11 @@ func (s *integrationTestSuite) TestLabels() {
assert.Equal(t, lbl1.ID, modResp.Label.ID)
assert.NotEqual(t, lbl1.Name, modResp.Label.Name)
+ // attempt to modify a label to a reserved name
+ for n := range builtinsMap {
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", lbl1.ID), &fleet.ModifyLabelPayload{Name: ptr.String(n)}, http.StatusUnprocessableEntity, &modResp)
+ }
+
// modify a non-existing label
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", lbl1.ID+1), &fleet.ModifyLabelPayload{Name: ptr.String("zzz")}, http.StatusNotFound, &modResp)
@@ -3633,9 +3798,13 @@ func (s *integrationTestSuite) TestLabels() {
assert.Len(t, summaryResp.Labels, builtInsCount+1)
// next page is empty
- s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", "2", "page", "1", "query", t.Name())
+ s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "page", "1")
assert.Len(t, listResp.Labels, 0)
+ // list labels with invalid query params
+ s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "order_key", "id", "after", "1")
+ s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, &listResp, "per_page", strconv.Itoa(builtInsCount+1), "query", "no match query for this endpoint")
+
// create another label
s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: ptr.String(strings.ReplaceAll(t.Name(), "/", "_")), Query: ptr.String("select 1")}, http.StatusOK, &createResp)
assert.NotZero(t, createResp.Label.ID)
@@ -3658,7 +3827,7 @@ func (s *integrationTestSuite) TestLabels() {
assert.Equal(t, hosts[1].ID, listHostsResp.Hosts[0].ID)
assert.Equal(t, hosts[2].ID, listHostsResp.Hosts[1].ID)
- // list hosts in label searching by display_name
+ // list hosts in label ordered by display_name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp, "order_key", "display_name", "order_direction", "desc")
assert.Len(t, listHostsResp.Hosts, len(hosts))
// first in the list is the last one, as the names are ordered with the index
@@ -3680,6 +3849,11 @@ func (s *integrationTestSuite) TestLabels() {
assert.Len(t, listHostsResp.Hosts, 1)
assert.Equal(t, hosts[0].ID, listHostsResp.Hosts[0].ID)
+ // list hosts in label searching by email address with leading/trailing whitespace
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp, "query", " a@b.c ")
+ assert.Len(t, listHostsResp.Hosts, 1)
+ assert.Equal(t, hosts[0].ID, listHostsResp.Hosts[0].ID)
+
// count hosts in label order by display_name
var countResp countHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "label_id", fmt.Sprint(lbl2.ID), "order_key", "display_name", "order_direction", "desc")
@@ -3734,15 +3908,22 @@ func (s *integrationTestSuite) TestLabels() {
// list labels, only the built-ins remain
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "per_page", strconv.Itoa(builtInsCount+1))
assert.Len(t, listResp.Labels, builtInsCount)
+ idsByName := make(map[string]uint, len(listResp.Labels))
for _, lbl := range listResp.Labels {
+ _, ok := builtinsMap[lbl.Name]
+ assert.True(t, ok)
assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType)
+ idsByName[lbl.Name] = lbl.ID
}
// labels summary, only the built-ins remains
s.DoJSON("GET", "/api/latest/fleet/labels/summary", nil, http.StatusOK, &summaryResp)
assert.Len(t, summaryResp.Labels, builtInsCount)
for _, lbl := range summaryResp.Labels {
+ _, ok := builtinsMap[lbl.Name]
+ assert.True(t, ok)
assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType)
+ assert.Equal(t, idsByName[lbl.Name], lbl.ID)
}
// host summary matches built-ins count
@@ -3750,7 +3931,22 @@ func (s *integrationTestSuite) TestLabels() {
s.DoJSON("GET", "/api/latest/fleet/host_summary", nil, http.StatusOK, &hostSummaryResp)
assert.Len(t, hostSummaryResp.BuiltinLabels, builtInsCount)
for _, lbl := range hostSummaryResp.BuiltinLabels {
+ _, ok := builtinsMap[lbl.Name]
+ assert.True(t, ok)
assert.Equal(t, fleet.LabelTypeBuiltIn, lbl.LabelType)
+ assert.Equal(t, idsByName[lbl.Name], lbl.ID)
+ }
+
+ require.Len(t, idsByName, len(builtinsMap))
+ for name := range builtinsMap {
+ id, ok := idsByName[name]
+ require.True(t, ok)
+
+ // attempt to delete by name
+ s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/%s", url.PathEscape(name)), nil, http.StatusUnprocessableEntity, &delResp)
+
+ // attempt to delete by id
+ s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/id/%d", id), nil, http.StatusUnprocessableEntity, &delIDResp)
}
}
@@ -3923,6 +4119,33 @@ func (s *integrationTestSuite) TestLabelSpecs() {
},
}, http.StatusInternalServerError, &applyResp)
+ // apply an invalid label spec - builtin label type
+ s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{
+ Specs: []*fleet.LabelSpec{
+ {
+ Name: name,
+ Query: "select 1",
+ Platform: "linux",
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ LabelType: fleet.LabelTypeBuiltIn,
+ },
+ },
+ }, http.StatusUnprocessableEntity, &applyResp)
+
+ // apply an invalid label spec - builtin label name
+ for n := range fleet.ReservedLabelNames() {
+ s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{
+ Specs: []*fleet.LabelSpec{
+ {
+ Name: n,
+ Query: "select 1",
+ Platform: "linux",
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ },
+ }, http.StatusUnprocessableEntity, &applyResp)
+ }
+
// apply a valid label spec
s.DoJSON("POST", "/api/latest/fleet/spec/labels", applyLabelSpecsRequest{
Specs: []*fleet.LabelSpec{
@@ -3955,11 +4178,26 @@ func (s *integrationTestSuite) TestUsers() {
t := s.T()
+ // existing users:
+ // {ID: 1, Name: "Test Name admin1@example.com", Email: "admin1@example.com", ...}
+ // {ID: 2, Name: "Test Name user1@example.com", Email: "user1@example.com", ...}
+ // {ID: 3, Name: "Test Name user2@example.com", Email: "user2@example.com", ...}
+
// list existing users
var listResp listUsersResponse
s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp)
assert.Len(t, listResp.Users, len(s.users))
+ // with non-matching query
+ s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp, "query", "noone")
+ assert.Len(t, listResp.Users, 0)
+
+ // with matching query containing leading/trailing whitespaces
+ s.DoJSON("GET", "/api/latest/fleet/users", nil, http.StatusOK, &listResp, "query", " user ")
+ assert.Len(t, listResp.Users, 2)
+ assert.Equal(t, uint(2), listResp.Users[0].ID)
+ assert.Equal(t, uint(3), listResp.Users[1].ID)
+
// test available teams returned by `/me` endpoint for existing user
var getMeResp getUserResponse
ssn := createSession(t, 1, s.ds)
@@ -6179,10 +6417,11 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
require.NoError(t, s.ds.LoadHostSoftware(context.Background(), hosts[0], false))
// add CVEs for the first 10 software, which are the least used (lower hosts_count)
+ testCvePrefix := "cve-123-123"
for i, sw := range hosts[0].Software[:10] {
inserted, err := s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
SoftwareID: sw.ID,
- CVE: fmt.Sprintf("cve-123-123-%03d", i),
+ CVE: fmt.Sprintf(testCvePrefix+"-%03d", i),
}, fleet.NVDSource)
require.NoError(t, err)
require.True(t, inserted)
@@ -6396,6 +6635,35 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "vulnerable", "true", "per_page", "5", "page", "2", "order_key", "hosts_count", "order_direction", "desc")
assertVersionsResp(versResp, nil, time.Time{}, "", expectedVulnVersionsCount)
+ // /software/versions filtered by name, version, cve (`/software` is deprecated)
+ // TODO(jacob) use `assertVersionsResp`
+ versionsResp := listSoftwareVersionsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Name)
+ assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1)
+ // with whitespace
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", " "+sws[0].Name+"\n")
+ assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1)
+
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Version)
+ assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1)
+ // with whitespace
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", "\n"+sws[0].Version+" ")
+ assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1)
+
+ // All 10 CVEs added to the first 10 software have the same cvePrefix, so should return all
+ // 10 vulnerable software versions
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", testCvePrefix)
+ require.Len(t, versionsResp.Software, 10)
+ require.Equal(t, 10, versionsResp.Count)
+ // TODO(jacob) use `assertVersionsResp`
+ // assertVersionsResp(versionsResp, sws[:10], hostsCountTs, "", 10, 1)
+ // with whitespace
+ s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", " "+testCvePrefix+"\n")
+ require.Len(t, versionsResp.Software, 10)
+ require.Equal(t, 10, versionsResp.Count)
+ // TODO(jacob) use `assertVersionsResp`
+ // assertVersionsResp(versionsResp, sws[:10], hostsCountTs, "", 10, 1)
+
// filter by the team, 2 by page
lsResp = listSoftwareResponse{}
s.DoJSON(
@@ -6477,16 +6745,20 @@ func (s *integrationTestSuite) TestSearchTargets() {
hosts := s.createHosts(t)
- lblMap, err := s.ds.LabelIDsByName(context.Background(), []string{"All Hosts"})
+ var builtinNames []string
+ for name := range fleet.ReservedLabelNames() {
+ builtinNames = append(builtinNames, name)
+ }
+ lblMap, err := s.ds.LabelIDsByName(context.Background(), builtinNames)
require.NoError(t, err)
- require.Len(t, lblMap, 1)
+ require.Len(t, lblMap, len(builtinNames))
// no search criteria
var searchResp searchTargetsResponse
s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{}, http.StatusOK, &searchResp)
require.Equal(t, uint(0), searchResp.TargetsCount)
require.Len(t, searchResp.Targets.Hosts, len(hosts)) // the HostTargets.HostIDs are actually host IDs to *omit* from the search
- require.Len(t, searchResp.Targets.Labels, 1)
+ require.Len(t, searchResp.Targets.Labels, len(lblMap))
require.Len(t, searchResp.Targets.Teams, 0)
var lblIDs []uint
@@ -6505,7 +6777,7 @@ func (s *integrationTestSuite) TestSearchTargets() {
s.DoJSON("POST", "/api/latest/fleet/targets", searchTargetsRequest{Selected: fleet.HostTargets{HostIDs: []uint{hosts[1].ID}}}, http.StatusOK, &searchResp)
require.Equal(t, uint(1), searchResp.TargetsCount)
require.Len(t, searchResp.Targets.Hosts, len(hosts)-1) // one omitted host id
- require.Len(t, searchResp.Targets.Labels, 1) // labels have not been omitted
+ require.Len(t, searchResp.Targets.Labels, len(lblMap)) // labels have not been omitted
require.Len(t, searchResp.Targets.Teams, 0)
searchResp = searchTargetsResponse{}
@@ -7329,6 +7601,7 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
t := s.T()
ctx := context.Background()
+ // create 3 hosts (deb, rhel, linux)
hosts := s.createHosts(t)
err := s.ds.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{
{Name: t.Name(), LabelMembershipType: fleet.LabelMembershipTypeManual, Query: "select 1", Hosts: []string{hosts[2].Hostname}},
@@ -7395,14 +7668,14 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows, len(hosts)+1) // all hosts + header row
- assert.Len(t, rows[0], 51) // total number of cols
+ assert.Len(t, rows[0], 54) // total number of cols
const (
idCol = 3
- issuesCol = 42
- gigsDiskCol = 39
- pctDiskCol = 40
- gigsTotalCol = 41
+ issuesCol = 45
+ gigsDiskCol = 42
+ pctDiskCol = 43
+ gigsTotalCol = 44
)
// find the row for hosts[1], it should have issues=1 (1 failing policy) and the expected disk space
@@ -7449,6 +7722,14 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
require.Len(t, rows, 2) // headers + matching host
require.Contains(t, rows[1], hosts[0].Hostname)
+ // search criteria including search query with leading/trailing whitespace are applied
+ res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "query", " local0 ", "columns", "hostname")
+ rows, err = csv.NewReader(res.Body).ReadAll()
+ res.Body.Close()
+ require.NoError(t, err)
+ require.Len(t, rows, 2) // headers + matching host
+ require.Contains(t, rows[1], hosts[0].Hostname)
+
// with device mapping results
res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "id,hostname,device_mapping")
rawCSV, err := io.ReadAll(res.Body)
@@ -7474,6 +7755,15 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
require.Len(t, rows, 2) // headers + member host
require.Contains(t, rows[1], hosts[2].Hostname)
+ // with a label id and a search query with leading/trailing whitespace
+ res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "hostname", "label_id", fmt.Sprintf("%d", customLabelID), "query", " local2 ")
+ rows, err = csv.NewReader(res.Body).ReadAll()
+ res.Body.Close()
+ require.NoError(t, err)
+ require.Len(t, rows, 2) // headers + member host
+ // hosts[2] is both matched by the trimmed query and in the provided label
+ require.Contains(t, rows[1], hosts[2].Hostname)
+
// with a software version id
res = s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusOK, "format", "csv", "columns", "hostname", "software_version_id", fmt.Sprint(fooV1ID))
rows, err = csv.NewReader(res.Body).ReadAll()
@@ -7855,7 +8145,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
// insert software vuln outside of host scope
_, err = s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
SoftwareID: sw2.ID,
- CVE: "CVE-2021-1236",
+ CVE: "CVE-2021-1246",
}, fleet.NVDSource)
require.NoError(t, err)
@@ -7879,12 +8169,12 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
Description: "Test CVE 2021-1235",
},
{
- CVE: "CVE-2021-1236",
+ CVE: "CVE-2021-1246",
CVSSScore: ptr.Float64(5.4),
EPSSProbability: ptr.Float64(0.6),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(mockTime),
- Description: "Test CVE 2021-1236",
+ Description: "Test CVE 2021-1246",
},
})
require.NoError(t, err)
@@ -7892,6 +8182,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
err = s.ds.UpdateVulnerabilityHostCounts(context.Background())
require.NoError(t, err)
+ // test list
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
require.Empty(t, resp.Err)
require.Len(s.T(), resp.Vulnerabilities, 3)
@@ -7913,9 +8204,9 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
HostCount: 1,
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235",
},
- "CVE-2021-1236": {
+ "CVE-2021-1246": {
HostCount: 1,
- DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1236",
+ DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1246",
},
}
@@ -7927,6 +8218,48 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.Empty(t, vuln.CVSSScore)
}
+ // test list with matching query containing leading/trailing whitespace
+ // TODO(jacob) - this may be another parsing bug
+ s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", " 123 ")
+ require.Empty(t, resp.Err)
+ require.Len(s.T(), resp.Vulnerabilities, 2)
+ require.Equal(t, resp.Count, uint(2))
+ require.False(t, resp.Meta.HasPreviousResults)
+ require.False(t, resp.Meta.HasNextResults)
+
+ expected = map[string]struct {
+ fleet.CVEMeta
+ HostCount uint
+ DetailsLink string
+ Source fleet.VulnerabilitySource
+ }{
+ "CVE-2021-1234": {
+ HostCount: 1,
+ DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234",
+ },
+ "CVE-2021-1235": {
+ HostCount: 1,
+ DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235",
+ },
+ // ...1246 should not match the query
+ }
+
+ for _, vuln := range resp.Vulnerabilities {
+ expectedVuln, ok := expected[vuln.CVE.CVE]
+ require.True(t, ok)
+ require.Equal(t, expectedVuln.HostCount, vuln.HostsCount)
+ require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
+ require.Empty(t, vuln.CVSSScore)
+ }
+
+ // test list with non-matching query
+ s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", "CVB")
+ require.Empty(t, resp.Err)
+ require.Len(s.T(), resp.Vulnerabilities, 0)
+ require.Equal(t, resp.Count, uint(0))
+ require.False(t, resp.Meta.HasPreviousResults)
+ require.False(t, resp.Meta.HasNextResults)
+
// Test Team Filter
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1")
require.Len(s.T(), resp.Vulnerabilities, 0)
@@ -7959,7 +8292,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/foobar", nil, http.StatusNotFound, &gResp)
// Valid CVE but not in team scope
- s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1236", nil, http.StatusNotFound, &gResp, "team_id", fmt.Sprintf("%d", team.ID))
+ s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1246", nil, http.StatusNotFound, &gResp, "team_id", fmt.Sprintf("%d", team.ID))
// Invalid TeamID
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusForbidden, &gResp, "team_id", "100")
@@ -8253,11 +8586,8 @@ func (s *integrationTestSuite) TestOrbitConfigNotifications() {
require.False(t, resp.Notifications.RenewEnrollmentProfile)
// simulate ABM assignment
- mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
- insertAppConfigQuery := `INSERT INTO host_dep_assignments (host_id) VALUES (?)`
- _, err = q.ExecContext(context.Background(), insertAppConfigQuery, hFleetMDM.ID)
- return err
- })
+ err = s.ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*hFleetMDM})
+ require.NoError(t, err)
err = s.ds.SetOrUpdateMDMData(context.Background(), hSimpleMDM.ID, false, true, "https://simplemdm.com", false, fleet.WellKnownMDMSimpleMDM, "")
require.NoError(t, err)
resp = orbitGetConfigResponse{}
@@ -8441,6 +8771,10 @@ func createOrbitEnrolledHost(t *testing.T, os, suffix string, ds fleet.Datastore
HardwareSerial: h.HardwareSerial,
}, orbitKey, nil)
require.NoError(t, err)
+ err = ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), h.ID, "1.22.0", sql.NullString{String: "42", Valid: true}, sql.NullBool{Bool: true, Valid: true},
+ )
+ require.NoError(t, err)
h.OrbitNodeKey = &orbitKey
return h
}
@@ -9498,7 +9832,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows1, len(hosts)+1) // all hosts + header row
- assert.Len(t, rows1[0], 51) // total number of cols
+ assert.Len(t, rows1[0], 54) // total number of cols
var (
idIdx int
@@ -9525,7 +9859,7 @@ func (s *integrationTestSuite) TestHostsReportWithPolicyResults() {
res.Body.Close()
require.NoError(t, err)
require.Len(t, rows2, len(hosts)+1) // all hosts + header row
- assert.Len(t, rows2[0], 51) // total number of cols
+ assert.Len(t, rows2[0], 54) // total number of cols
// Check that all hosts have 0 issues and that they match the previous call to `/hosts/report`.
for i := 1; i < len(hosts)+1; i++ {
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 06d2e1e388..7504218bb4 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -1009,7 +1009,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() {
}
s.DoJSON("POST", "/api/latest/fleet/teams", team4, http.StatusUnprocessableEntity, &tmResp)
- // list teams
+ // list teams matching name of one team
var listResp listTeamsResponse
s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", name, "per_page", "2")
require.Len(t, listResp.Teams, 1)
@@ -1017,6 +1017,16 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() {
assert.NotNil(t, listResp.Teams[0].Config.AgentOptions)
tm1ID := listResp.Teams[0].ID
+ // same as above, with leading/trailing whitespace
+ s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", " "+name+" ", "per_page", "2")
+ require.Len(t, listResp.Teams, 1)
+ assert.Equal(t, team.Name, listResp.Teams[0].Name)
+ assert.NotNil(t, listResp.Teams[0].Config.AgentOptions)
+
+ // same as above, no match
+ s.DoJSON("GET", "/api/latest/fleet/teams", nil, http.StatusOK, &listResp, "query", " nope ", "per_page", "2")
+ require.Len(t, listResp.Teams, 0)
+
// get team
var getResp getTeamResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), nil, http.StatusOK, &getResp)
@@ -3666,6 +3676,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() {
}
func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
+ t := s.T()
fields := []string{"Query", "Name", "Description", "Resolution", "Platform", "Critical"}
createPol1 := &globalPolicyResponse{}
@@ -3678,7 +3689,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
Critical: true,
}
s.DoJSON("POST", "/api/latest/fleet/policies", createPol1Req, http.StatusOK, &createPol1)
- allEqual(s.T(), createPol1Req, createPol1.Policy, fields...)
+ allEqual(t, createPol1Req, createPol1.Policy, fields...)
createPol2 := &globalPolicyResponse{}
createPol2Req := &globalPolicyRequest{
@@ -3690,16 +3701,22 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
Critical: false,
}
s.DoJSON("POST", "/api/latest/fleet/policies", createPol2Req, http.StatusOK, &createPol2)
- allEqual(s.T(), createPol2Req, createPol2.Policy, fields...)
+ allEqual(t, createPol2Req, createPol2.Policy, fields...)
listPol := &listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPol)
- require.Len(s.T(), listPol.Policies, 2)
+ require.Len(t, listPol.Policies, 2)
sort.Slice(listPol.Policies, func(i, j int) bool {
return listPol.Policies[i].Name < listPol.Policies[j].Name
})
- require.Equal(s.T(), createPol1.Policy, listPol.Policies[0])
- require.Equal(s.T(), createPol2.Policy, listPol.Policies[1])
+ require.Equal(t, createPol1.Policy, listPol.Policies[0])
+ require.Equal(t, createPol2.Policy, listPol.Policies[1])
+
+ // match policy by name with leading/trailing whitespace
+ listPolByName := &listGlobalPoliciesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPolByName, "query", " name1 ")
+ require.Len(t, listPolByName.Policies, 1)
+ require.Equal(t, listPolByName.Policies[0].Name, "name1")
patchPol1Req := &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
@@ -3713,7 +3730,7 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
}
patchPol1 := &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol1.Policy.ID), patchPol1Req, http.StatusOK, patchPol1)
- allEqual(s.T(), patchPol1Req, patchPol1.Policy, fields...)
+ allEqual(t, patchPol1Req, patchPol1.Policy, fields...)
patchPol2Req := &modifyGlobalPolicyRequest{
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
@@ -3727,21 +3744,21 @@ func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
}
patchPol2 := &modifyGlobalPolicyResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol2.Policy.ID), patchPol2Req, http.StatusOK, patchPol2)
- allEqual(s.T(), patchPol2Req, patchPol2.Policy, fields...)
+ allEqual(t, patchPol2Req, patchPol2.Policy, fields...)
listPol = &listGlobalPoliciesResponse{}
s.DoJSON("GET", "/api/latest/fleet/policies", nil, http.StatusOK, listPol)
- require.Len(s.T(), listPol.Policies, 2)
+ require.Len(t, listPol.Policies, 2)
sort.Slice(listPol.Policies, func(i, j int) bool {
return listPol.Policies[i].Name < listPol.Policies[j].Name
})
// not using require.Equal because "PATCH policies" returns the wrong updated timestamp.
- allEqual(s.T(), patchPol1.Policy, listPol.Policies[0], fields...)
- allEqual(s.T(), patchPol2.Policy, listPol.Policies[1], fields...)
+ allEqual(t, patchPol1.Policy, listPol.Policies[0], fields...)
+ allEqual(t, patchPol2.Policy, listPol.Policies[1], fields...)
getPol2 := &getPolicyByIDResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/policies/%d", createPol2.Policy.ID), nil, http.StatusOK, getPol2)
- require.Equal(s.T(), listPol.Policies[1], getPol2.Policy)
+ require.Equal(t, listPol.Policies[1], getPol2.Policy)
}
func (s *integrationEnterpriseTestSuite) TestTeamPolicyCreateReadPatch() {
@@ -5106,6 +5123,27 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
require.True(t, scriptResultResp.HostTimeout)
require.Contains(t, scriptResultResp.Message, fleet.RunScriptHostTimeoutErrMsg)
+ // Disable scripts on the host
+ scriptsEnabled := false
+ err = s.ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: scriptsEnabled, Valid: true},
+ )
+ require.NoError(t, err)
+ s.DoJSON(
+ "POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"},
+ http.StatusUnprocessableEntity, &runResp,
+ )
+ s.DoJSON(
+ "POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"},
+ http.StatusUnprocessableEntity, &runResp,
+ )
+ // Re-enable scripts on the host
+ scriptsEnabled = true
+ err = s.ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), host.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: scriptsEnabled, Valid: true},
+ )
+ require.NoError(t, err)
+
// make the host "offline"
err = s.ds.MarkHostsSeen(context.Background(), []uint{host.ID}, time.Now().Add(-time.Hour))
require.NoError(t, err)
@@ -7048,6 +7086,99 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
},
}, resp.SoftwareTitles)
+ // match software title by name with leading whitespace
+ resp = listSoftwareTitlesResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/software/titles",
+ listSoftwareTitlesRequest{},
+ http.StatusOK, &resp,
+ "query", " ba",
+ )
+ require.Equal(t, 2, resp.Count)
+ require.NotEmpty(t, resp.CountsUpdatedAt)
+ softwareTitlesMatch([]fleet.SoftwareTitle{
+ {
+ Name: "bar",
+ Source: "apps",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}},
+ },
+ },
+ {
+ Name: "baz",
+ Source: "deb_packages",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.5", Vulnerabilities: nil},
+ },
+ },
+ }, resp.SoftwareTitles)
+
+ // match software title by name with trailing whitespace
+ resp = listSoftwareTitlesResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/software/titles",
+ listSoftwareTitlesRequest{},
+ http.StatusOK, &resp,
+ "query", "ba ",
+ )
+ require.Equal(t, 2, resp.Count)
+ require.NotEmpty(t, resp.CountsUpdatedAt)
+ softwareTitlesMatch([]fleet.SoftwareTitle{
+ {
+ Name: "bar",
+ Source: "apps",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}},
+ },
+ },
+ {
+ Name: "baz",
+ Source: "deb_packages",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.5", Vulnerabilities: nil},
+ },
+ },
+ }, resp.SoftwareTitles)
+
+ // match software title by name with leading and trailing whitespace
+ resp = listSoftwareTitlesResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/software/titles",
+ listSoftwareTitlesRequest{},
+ http.StatusOK, &resp,
+ "query", " ba ",
+ )
+ require.Equal(t, 2, resp.Count)
+ require.NotEmpty(t, resp.CountsUpdatedAt)
+ softwareTitlesMatch([]fleet.SoftwareTitle{
+ {
+ Name: "bar",
+ Source: "apps",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.4", Vulnerabilities: &fleet.SliceString{"cve-123-123-132"}},
+ },
+ },
+ {
+ Name: "baz",
+ Source: "deb_packages",
+ VersionsCount: 1,
+ HostsCount: 1,
+ Versions: []fleet.SoftwareVersion{
+ {Version: "0.0.5", Vulnerabilities: nil},
+ },
+ },
+ }, resp.SoftwareTitles)
+
// find the ID of "foo"
s.DoJSON(
"GET", "/api/latest/fleet/software/titles",
@@ -7223,6 +7354,28 @@ func (s *integrationEnterpriseTestSuite) TestLockUnlockWipeWindowsLinux() {
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Windows MDM isn't turned on.")
+ // Disable scripts on Linux host
+ err := s.ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: false, Valid: true},
+ )
+ require.NoError(t, err)
+ // try to lock/unlock/wipe the Linux host. Fails because scripts are not enabled.
+ res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusUnprocessableEntity)
+ errMsg = extractServerErrorText(res.Body)
+ require.Contains(t, errMsg, "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts")
+ res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", linuxHost.ID), nil, http.StatusUnprocessableEntity)
+ errMsg = extractServerErrorText(res.Body)
+ require.Contains(t, errMsg, "Couldn't unlock host. To unlock, deploy the fleetd agent with --enable-scripts")
+ res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", linuxHost.ID), nil, http.StatusUnprocessableEntity)
+ errMsg = extractServerErrorText(res.Body)
+ require.Contains(t, errMsg, "Couldn't wipe host. To wipe, deploy the fleetd agent with --enable-scripts")
+
+ // Enable scripts on Linux host
+ err = s.ds.SetOrUpdateHostOrbitInfo(
+ context.Background(), linuxHost.ID, "1.22.0", sql.NullString{}, sql.NullBool{Bool: true, Valid: true},
+ )
+ require.NoError(t, err)
+
// try to lock/unlock/wipe the Linux host succeeds, no MDM constraints
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", linuxHost.ID), nil, http.StatusNoContent)
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 02d20a72e7..ff68f1f013 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -152,6 +152,12 @@ func (s *integrationMDMTestSuite) SetupSuite() {
workr.Register(macosJob, appleMDMJob)
s.worker = workr
+ // clear the jobs queue of any pending jobs generated via DB migrations
+ mysql.ExecAdhocSQL(s.T(), s.ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(context.Background(), "DELETE FROM jobs")
+ return err
+ })
+
var depSchedule *schedule.Schedule
var integrationsSchedule *schedule.Schedule
var profileSchedule *schedule.Schedule
@@ -1509,7 +1515,6 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
// create a host enrolled in fleet
mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
- s.runWorker()
// create a host that's not enrolled into MDM
nonMDMHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
@@ -1537,6 +1542,14 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
EnrollmentProfile: json.RawMessage(noTeamProf),
}, http.StatusOK, &globalAsstResp)
+ // set the global Enable Release Device manually setting to true,
+ // will be inherited by teams created via preassign/match.
+ s.Do("PATCH", "/api/latest/fleet/setup_experience",
+ json.RawMessage(jsonMustMarshal(t, map[string]any{"enable_release_device_manually": true})),
+ http.StatusNoContent)
+
+ s.runWorker()
+
// preassign an empty profile, fails
s.Do("POST", "/api/latest/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileRequest{MDMApplePreassignProfilePayload: fleet.MDMApplePreassignProfilePayload{ExternalHostIdentifier: "empty", HostUUID: nonMDMHost.UUID, Profile: nil}}, http.StatusUnprocessableEntity)
@@ -1571,6 +1584,8 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
tm1, err := s.ds.Team(ctx, *h.TeamID)
require.NoError(t, err)
require.Equal(t, "g1", tm1.Name)
+ require.True(t, tm1.Config.MDM.EnableDiskEncryption)
+ require.True(t, tm1.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
runWithAdminToken(func() {
// it create activities for the new team, the profiles assigned to it,
@@ -1595,34 +1610,51 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
0)
})
- // and the team has the expected profiles
+ // and the team has the expected profiles (prof1 and prof2)
profs, err := s.ds.ListMDMAppleConfigProfiles(ctx, &tm1.ID)
require.NoError(t, err)
require.Len(t, profs, 2)
// order is guaranteed by profile name
require.Equal(t, prof1, []byte(profs[0].Mobileconfig))
require.Equal(t, prof2, []byte(profs[1].Mobileconfig))
- // filevault is enabled by default
- require.True(t, tm1.Config.MDM.EnableDiskEncryption)
// setup assistant settings are copyied from "no team"
teamAsst, err := s.ds.GetMDMAppleSetupAssistant(ctx, &tm1.ID)
require.NoError(t, err)
require.Equal(t, globalAsstResp.Name, teamAsst.Name)
require.JSONEq(t, string(globalAsstResp.Profile), string(teamAsst.Profile))
- // create a team and set profiles to it
+ // trigger the schedule so profiles are set in their state
+ s.awaitTriggerProfileSchedule(t)
+ s.runWorker()
+
+ // the mdm host has the same profiles (i1, i2, plus fleetd config and disk encryption)
+ s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
+ mdmHost: {
+ {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: "i2", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ },
+ })
+
+ // create a team and set profiles to it (note that it doesn't have disk encryption enabled)
tm2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
- Name: "g1 - g4",
+ Name: "g1 - g4",
+ Secrets: []*fleet.EnrollSecret{{Secret: "tm2secret"}},
})
require.NoError(t, err)
prof4 := mobileconfigForTest("n4", "i4")
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
prof1, prof4,
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
+ // tm2 has disk encryption and release device manually disabled
+ require.False(t, tm2.Config.MDM.EnableDiskEncryption)
+ require.False(t, tm2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
// create another team with a superset of profiles
tm3, err := s.ds.NewTeam(context.Background(), &fleet.Team{
- Name: "team3_" + t.Name(),
+ Name: "team3_" + t.Name(),
+ Secrets: []*fleet.EnrollSecret{{Secret: "tm3secret"}},
})
require.NoError(t, err)
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
@@ -1631,16 +1663,14 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
// and yet another team with the same profiles as tm3
tm4, err := s.ds.NewTeam(context.Background(), &fleet.Team{
- Name: "team4_" + t.Name(),
+ Name: "team4_" + t.Name(),
+ Secrets: []*fleet.EnrollSecret{{Secret: "tm4secret"}},
})
require.NoError(t, err)
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
prof1, prof2, prof4,
}}, http.StatusNoContent, "team_id", fmt.Sprint(tm4.ID))
- // trigger the schedule so profiles are set in their state
- s.awaitTriggerProfileSchedule(t)
-
// preassign the MDM host to prof1 and prof4, should match existing team tm2
//
// additionally, use external host identifiers with different
@@ -1657,51 +1687,45 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
require.NoError(t, err)
require.NotNil(t, h.TeamID)
require.Equal(t, tm2.ID, *h.TeamID)
+ // tm2 still has disk encryption and release device manually disabled
+ tm2, err = s.ds.Team(ctx, *h.TeamID)
+ require.NoError(t, err)
+ require.False(t, tm2.Config.MDM.EnableDiskEncryption)
+ require.False(t, tm2.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
// the host's profiles are:
- // - the same as the team's and are pending
+ // - the same as the team's and are pending (prof1 + prof4)
// - prof2 + old filevault are pending removal
- // - fleetd config being reinstalled (to update the enroll secret)
+ // - fleetd config being reinstalled (for new enroll secret)
s.awaitTriggerProfileSchedule(t)
- hostProfs, err := s.ds.GetHostMDMAppleProfiles(ctx, mdmHost.UUID)
- require.NoError(t, err)
- require.Len(t, hostProfs, 5)
- sort.Slice(hostProfs, func(i, j int) bool {
- l, r := hostProfs[i], hostProfs[j]
- return l.Name < r.Name
+ // useful for debugging
+ //mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ // mysql.DumpTable(t, q, "host_mdm_apple_profiles")
+ // return nil
+ //})
+ s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
+ mdmHost: {
+ {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: "i2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
+ {Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
+ },
})
- require.Equal(t, "Disk encryption", hostProfs[0].Name)
- require.NotNil(t, hostProfs[0].Status)
- require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[0].Status)
- require.Equal(t, fleet.MDMOperationTypeRemove, hostProfs[0].OperationType)
- require.Equal(t, "Fleetd configuration", hostProfs[1].Name)
- require.NotNil(t, hostProfs[1].Status)
- require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[1].Status)
- require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[1].OperationType)
- require.Equal(t, "n1", hostProfs[2].Name)
- require.NotNil(t, hostProfs[2].Status)
- require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[2].Status)
- require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[2].OperationType)
- require.Equal(t, "n2", hostProfs[3].Name)
- require.NotNil(t, hostProfs[3].Status)
- require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[3].Status)
- require.Equal(t, fleet.MDMOperationTypeRemove, hostProfs[3].OperationType)
- require.Equal(t, "n4", hostProfs[4].Name)
- require.NotNil(t, hostProfs[4].Status)
- require.Equal(t, fleet.MDMDeliveryPending, *hostProfs[4].Status)
- require.Equal(t, fleet.MDMOperationTypeInstall, hostProfs[4].OperationType)
// create a new mdm host enrolled in fleet
mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
- s.runWorker()
+
// make it part of team 2
s.Do("POST", "/api/v1/fleet/hosts/transfer",
addHostsToTeamRequest{TeamID: &tm2.ID, HostIDs: []uint{mdmHost2.ID}}, http.StatusOK)
// simulate having its profiles installed
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
- _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID)
+ res, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID)
+ n, _ := res.RowsAffected()
+ require.Equal(t, 3, int(n))
return err
})
@@ -1719,23 +1743,14 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
// and its profiles have been left untouched
s.awaitTriggerProfileSchedule(t)
- hostProfs, err = s.ds.GetHostMDMAppleProfiles(ctx, mdmHost2.UUID)
- require.NoError(t, err)
- require.Len(t, hostProfs, 3)
- sort.Slice(hostProfs, func(i, j int) bool {
- l, r := hostProfs[i], hostProfs[j]
- return l.Name < r.Name
+ s.assertHostConfigProfiles(map[*fleet.Host][]fleet.HostMDMAppleProfile{
+ mdmHost2: {
+ {Identifier: "i1", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
+ {Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
+ {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryVerifying},
+ },
})
- require.Equal(t, "Fleetd configuration", hostProfs[0].Name)
- require.NotNil(t, hostProfs[0].Status)
- require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[0].Status)
- require.Equal(t, "n1", hostProfs[1].Name)
- require.NotNil(t, hostProfs[1].Status)
- require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[1].Status)
- require.Equal(t, "n4", hostProfs[2].Name)
- require.NotNil(t, hostProfs[2].Status)
- require.Equal(t, fleet.MDMDeliveryVerifying, *hostProfs[2].Status)
}
// while s.TestPuppetMatchPreassignProfiles focuses on many edge cases/extra
@@ -5587,6 +5602,20 @@ func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhook() {
},
})
require.NoError(t, err)
+
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ resp.Devices = map[string]string{
+ prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
+ }
+ encoder := json.NewEncoder(w)
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
}
}))
s.runDEPSchedule()
@@ -5702,6 +5731,19 @@ func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhookErrors() {
},
})
require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ resp.Devices = map[string]string{
+ prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
+ }
+ encoder := json.NewEncoder(w)
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
}
}))
s.runDEPSchedule()
@@ -6369,6 +6411,30 @@ func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint,
return profile
}
+func (s *integrationMDMTestSuite) assertMacOSConfigProfilesByName(teamID *uint, profileName string, exists bool) {
+ t := s.T()
+ if teamID == nil {
+ teamID = ptr.Uint(0)
+ }
+ var cfgProfs []*fleet.MDMAppleConfigProfile
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_configuration_profiles WHERE team_id = ?`, teamID)
+ })
+
+ label := "exist"
+ if !exists {
+ label = "not exist"
+ }
+ require.Condition(t, func() bool {
+ for _, p := range cfgProfs {
+ if p.Name == profileName {
+ return exists // success if we want it to exist, failure if we don't
+ }
+ }
+ return !exists
+ }, "a config profile must %s with name: %s", label, profileName)
+}
+
func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint, profileName string, exists bool) {
t := s.T()
if teamID == nil {
@@ -7258,6 +7324,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
// simulate that the device is assigned to Fleet in ABM
+ profileAssignmentStatusResponse := fleet.DEPAssignProfileResponseSuccess
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
switch r.URL.Path {
@@ -7280,9 +7347,33 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
},
})
require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ resp.Devices = map[string]string{
+ prof.Devices[0]: string(profileAssignmentStatusResponse),
+ }
+ encoder := json.NewEncoder(w)
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
}
}))
- s.runDEPSchedule()
+
+ cleanAssignmentStatus := func() {
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ stmt := `UPDATE host_dep_assignments
+ SET assign_profile_response = NULL,
+ response_updated_at = NULL,
+ profile_uuid = NULL
+ WHERE host_id = ?`
+ _, err := q.ExecContext(context.Background(), stmt, host.ID)
+ return err
+ })
+ }
// simulate that the device is enrolled in a third-party MDM and DEP capable
err := s.ds.SetOrUpdateMDMData(
@@ -7297,24 +7388,70 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
)
require.NoError(t, err)
+ // simulate a response before we have the chance to assign the profile
getDesktopResp = fleetDesktopResponse{}
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
require.NoError(t, res.Body.Close())
require.NoError(t, getDesktopResp.Err)
require.Zero(t, *getDesktopResp.FailingPolicies)
- require.True(t, getDesktopResp.Notifications.NeedsMDMMigration)
+ require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)
-
orbitConfigResp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
+ require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ cleanAssignmentStatus()
+
+ // simulate a "FAILED" JSON profile assignment
+ profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseFailed
+ s.runDEPSchedule()
+ getDesktopResp = fleetDesktopResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
+ require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
+ require.NoError(t, res.Body.Close())
+ require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ orbitConfigResp = orbitGetConfigResponse{}
+ s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
+ require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
+ cleanAssignmentStatus()
+
+ // simulate a "NOT_ACCESSIBLE" JSON profile assignment
+ profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseNotAccessible
+ s.runDEPSchedule()
+ getDesktopResp = fleetDesktopResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
+ require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
+ require.NoError(t, res.Body.Close())
+ require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
+ require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
+ cleanAssignmentStatus()
+
+ // simulate a "SUCCESS" JSON profile assignment
+ profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseSuccess
+ s.runDEPSchedule()
+ getDesktopResp = fleetDesktopResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
+ require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
+ require.NoError(t, res.Body.Close())
+ require.True(t, getDesktopResp.Notifications.NeedsMDMMigration)
+ require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
require.True(t, orbitConfigResp.Notifications.NeedsMDMMigration)
require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
+ cleanAssignmentStatus()
// simulate that the device needs to be enrolled in fleet, DEP capable
err = s.ds.SetOrUpdateMDMData(
@@ -7384,6 +7521,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
host := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
createDeviceTokenForHost(t, s.ds, host.ID, token)
checkMigrationResponses(host, token)
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team-1"})
require.NoError(t, err)
@@ -12338,3 +12476,159 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() {
require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)
require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status)
}
+
+func (s *integrationMDMTestSuite) TestMDMBatchSetProfilesKeepsReservedNames() {
+ t := s.T()
+ ctx := context.Background()
+
+ checkMacProfs := func(teamID *uint, names ...string) {
+ var count int
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ var tid uint
+ if teamID != nil {
+ tid = *teamID
+ }
+ return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_apple_configuration_profiles WHERE team_id = ?`, tid)
+ })
+ require.Equal(t, len(names), count)
+ for _, n := range names {
+ s.assertMacOSConfigProfilesByName(teamID, n, true)
+ }
+ }
+
+ checkWinProfs := func(teamID *uint, names ...string) {
+ var count int
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ var tid uint
+ if teamID != nil {
+ tid = *teamID
+ }
+ return sqlx.GetContext(ctx, q, &count, `SELECT COUNT(*) FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tid)
+ })
+ for _, n := range names {
+ s.assertWindowsConfigProfilesByName(teamID, n, true)
+ }
+ }
+
+ acResp := appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.True(t, acResp.MDM.EnabledAndConfigured)
+ require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
+
+ // ensures that the fleetd profile is created
+ secrets, err := s.ds.GetEnrollSecrets(ctx, nil)
+ require.NoError(t, err)
+ if len(secrets) == 0 {
+ require.NoError(t, s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}}))
+ }
+ require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger))
+
+ // turn on disk encryption and os updates
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": {
+ "enable_disk_encryption": true,
+ "windows_updates": {
+ "deadline_days": 3,
+ "grace_period_days": 1
+ },
+ "macos_updates": {
+ "deadline": "2023-12-31",
+ "minimum_version": "13.3.7"
+ }
+ }
+ }`), http.StatusOK, &acResp)
+ checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...)
+ checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...)
+
+ // batch set only windows profiles doesn't remove the reserved names
+ newWinProfile := syncml.ForTestWithData(map[string]string{"l1": "d1"})
+ var testProfiles []fleet.MDMProfileBatchPayload
+ testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
+ Name: "n1",
+ Contents: newWinProfile,
+ })
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
+ checkMacProfs(nil, servermdm.ListFleetReservedMacOSProfileNames()...)
+ checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)
+
+ // batch set windows and mac profiles doesn't remove the reserved names
+ newMacProfile := mcBytesForTest("n2", "i2", uuid.NewString())
+ testProfiles = append(testProfiles, fleet.MDMProfileBatchPayload{
+ Name: "n2",
+ Contents: newMacProfile,
+ })
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
+ checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
+ checkWinProfs(nil, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)
+
+ // batch set only mac profiles doesn't remove the reserved names
+ testProfiles = []fleet.MDMProfileBatchPayload{{
+ Name: "n2",
+ Contents: newMacProfile,
+ }}
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
+ checkMacProfs(nil, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
+ checkWinProfs(nil, servermdm.ListFleetReservedWindowsProfileNames()...)
+
+ // create a team
+ var tmResp teamResponse
+ s.DoJSON("POST", "/api/v1/fleet/teams", map[string]string{"Name": t.Name()}, http.StatusOK, &tmResp)
+
+ // edit team mdm config to turn on disk encryption and os updates
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tmResp.Team.ID), modifyTeamRequest{
+ TeamPayload: fleet.TeamPayload{
+ Name: ptr.String(t.Name()),
+ MDM: &fleet.TeamPayloadMDM{
+ EnableDiskEncryption: optjson.SetBool(true),
+ WindowsUpdates: &fleet.WindowsUpdates{
+ DeadlineDays: optjson.SetInt(4),
+ GracePeriodDays: optjson.SetInt(1),
+ },
+ MacOSUpdates: &fleet.MacOSUpdates{
+ Deadline: optjson.SetString("2023-12-31"),
+ MinimumVersion: optjson.SetString("13.3.8"),
+ },
+ },
+ },
+ }, http.StatusOK, &teamResponse{})
+
+ s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d", tmResp.Team.ID), nil, http.StatusOK, &tmResp)
+ require.True(t, tmResp.Team.Config.MDM.EnableDiskEncryption)
+ require.Equal(t, 4, tmResp.Team.Config.MDM.WindowsUpdates.DeadlineDays.Value)
+ require.Equal(t, 1, tmResp.Team.Config.MDM.WindowsUpdates.GracePeriodDays.Value)
+ require.Equal(t, "2023-12-31", tmResp.Team.Config.MDM.MacOSUpdates.Deadline.Value)
+ require.Equal(t, "13.3.8", tmResp.Team.Config.MDM.MacOSUpdates.MinimumVersion.Value)
+
+ require.NoError(t, ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger))
+
+ checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...)
+ checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...)
+
+ // batch set only windows profiles doesn't remove the reserved names
+ var testTeamProfiles []fleet.MDMProfileBatchPayload
+ testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{
+ Name: "n1",
+ Contents: newWinProfile,
+ })
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID)))
+ checkMacProfs(&tmResp.Team.ID, servermdm.ListFleetReservedMacOSProfileNames()...)
+ checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)
+
+ // batch set windows and mac profiles doesn't remove the reserved names
+ testTeamProfiles = append(testTeamProfiles, fleet.MDMProfileBatchPayload{
+ Name: "n2",
+ Contents: newMacProfile,
+ })
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID)))
+ checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
+ checkWinProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedWindowsProfileNames(), "n1")...)
+
+ // batch set only mac profiles doesn't remove the reserved names
+ testTeamProfiles = []fleet.MDMProfileBatchPayload{{
+ Name: "n2",
+ Contents: newMacProfile,
+ }}
+ s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testTeamProfiles}, http.StatusNoContent, "team_id", strconv.Itoa(int(tmResp.Team.ID)))
+ checkMacProfs(&tmResp.Team.ID, append(servermdm.ListFleetReservedMacOSProfileNames(), "n2")...)
+ checkWinProfs(&tmResp.Team.ID, servermdm.ListFleetReservedWindowsProfileNames()...)
+}
diff --git a/server/service/labels.go b/server/service/labels.go
index 799b17fae5..6b15ec0787 100644
--- a/server/service/labels.go
+++ b/server/service/labels.go
@@ -2,6 +2,8 @@ package service
import (
"context"
+ "fmt"
+ "net/http"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
@@ -64,6 +66,12 @@ func (svc *Service) NewLabel(ctx context.Context, p fleet.LabelPayload) (*fleet.
label.Description = *p.Description
}
+ for name := range fleet.ReservedLabelNames() {
+ if label.Name == name {
+ return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot add label '%s' because it conflicts with the name of a built-in label", name))
+ }
+ }
+
label, err := svc.ds.NewLabel(ctx, label)
if err != nil {
return nil, err
@@ -111,7 +119,16 @@ func (svc *Service) ModifyLabel(ctx context.Context, id uint, payload fleet.Modi
if err != nil {
return nil, err
}
+ if label.LabelType == fleet.LabelTypeBuiltIn {
+ return nil, fleet.NewInvalidArgumentError("label_type", fmt.Sprintf("cannot modify built-in label '%s'", label.Name))
+ }
if payload.Name != nil {
+ // Check if the new name is a reserved label name
+ for name := range fleet.ReservedLabelNames() {
+ if *payload.Name == name {
+ return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot rename label to '%s' because it conflicts with the name of a built-in label", name))
+ }
+ }
label.Name = *payload.Name
}
if payload.Description != nil {
@@ -319,6 +336,13 @@ func (svc *Service) DeleteLabel(ctx context.Context, name string) error {
return err
}
+ // check if the label is a built-in label
+ for n := range fleet.ReservedLabelNames() {
+ if n == name {
+ return fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot delete built-in label '%s'", name))
+ }
+ }
+
return svc.ds.DeleteLabel(ctx, name)
}
@@ -354,6 +378,15 @@ func (svc *Service) DeleteLabelByID(ctx context.Context, id uint) error {
if err != nil {
return err
}
+ if label.LabelType == fleet.LabelTypeBuiltIn {
+ return fleet.NewInvalidArgumentError("label_type", fmt.Sprintf("cannot delete built-in label '%s'", label.Name))
+ }
+ for name := range fleet.ReservedLabelNames() {
+ if label.Name == name {
+ return fleet.NewInvalidArgumentError("name", fmt.Sprintf("cannot delete built-in label '%s'", label.Name))
+ }
+ }
+
return svc.ds.DeleteLabel(ctx, label.Name)
}
@@ -393,6 +426,14 @@ func (svc *Service) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpe
// Hosts list doesn't need to contain anything, but it should at least not be nil.
return ctxerr.Errorf(ctx, "label %s is declared as manual but contains no `hosts key`", spec.Name)
}
+ if spec.LabelType == fleet.LabelTypeBuiltIn {
+ return fleet.NewUserMessageError(ctxerr.Errorf(ctx, "cannot modify built-in label '%s'", spec.Name), http.StatusUnprocessableEntity)
+ }
+ for name := range fleet.ReservedLabelNames() {
+ if spec.Name == name {
+ return fleet.NewUserMessageError(ctxerr.Errorf(ctx, "cannot modify built-in label '%s'", name), http.StatusUnprocessableEntity)
+ }
+ }
}
return svc.ds.ApplyLabelSpecs(ctx, specs)
}
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index 6d38c9203c..d6e34624f6 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
+ "database/sql"
"encoding/json"
"errors"
"fmt"
@@ -1676,8 +1677,12 @@ func TestDetailQueries(t *testing.T) {
require.Equal(t, "3.4.5", version)
return nil
}
- ds.SetOrUpdateHostOrbitInfoFunc = func(ctx context.Context, hostID uint, version string) error {
+ ds.SetOrUpdateHostOrbitInfoFunc = func(
+ ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool,
+ ) error {
require.Equal(t, "42", version)
+ require.Equal(t, sql.NullString{String: "1.2.3", Valid: true}, desktopVersion)
+ require.Equal(t, sql.NullBool{Bool: true, Valid: true}, scriptsEnabled)
return nil
}
ds.SetOrUpdateDeviceAuthTokenFunc = func(ctx context.Context, hostID uint, authToken string) error {
@@ -1851,7 +1856,9 @@ func TestDetailQueries(t *testing.T) {
],
"fleet_detail_query_orbit_info": [
{
- "version": "42"
+ "version": "42",
+ "desktop_version": "1.2.3",
+ "scripts_enabled": "1"
}
]
}
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index b7b3d6409f..f9a6af2bcd 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -2,6 +2,7 @@ package osquery_utils
import (
"context"
+ "database/sql"
"encoding/base64"
"encoding/hex"
"fmt"
@@ -607,8 +608,9 @@ var extraDetailQueries = map[string]DetailQuery{
DirectIngestFunc: directIngestOSUnixLike,
},
"orbit_info": {
- Query: `SELECT version FROM orbit_info`,
+ Query: `SELECT * FROM orbit_info`,
DirectIngestFunc: directIngestOrbitInfo,
+ Platforms: append(fleet.HostLinuxOSs, "darwin", "windows"),
Discovery: discoveryTable("orbit_info"),
},
"disk_encryption_darwin": {
@@ -628,11 +630,26 @@ var extraDetailQueries = map[string]DetailQuery{
// osquery table on darwin and linux, it is always present.
},
"disk_encryption_windows": {
- Query: `SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1;`,
+ // Bitlocker is an optional component on Windows Server and
+ // isn't guaranteed to be installed. If we try to query the
+ // bitlocker_info table when the bitlocker component isn't
+ // present, the query will crash and fail to report back to
+ // the server. Before querying bitlocke_info, we check if it's
+ // either:
+ // 1. both an optional component, and installed.
+ // OR
+ // 2. not optional, meaning it's built into the OS
+ Query: `
+ WITH encrypted(enabled) AS (
+ SELECT CASE WHEN
+ NOT EXISTS(SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker')
+ OR
+ (SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker' AND state = 1)
+ THEN (SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1)
+ END)
+ SELECT 1 FROM encrypted WHERE enabled IS NOT NULL`,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestDiskEncryption,
- // the "bitlocker_info" table doesn't need a Discovery query as it is an official
- // osquery table on windows, it is always present.
},
}
@@ -1054,7 +1071,15 @@ func directIngestOrbitInfo(ctx context.Context, logger log.Logger, host *fleet.H
return ctxerr.Errorf(ctx, "directIngestOrbitInfo invalid number of rows: %d", len(rows))
}
version := rows[0]["version"]
- if err := ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, version); err != nil {
+ var desktopVersion sql.NullString
+ desktopVersion.String, desktopVersion.Valid = rows[0]["desktop_version"]
+ var scriptsEnabled sql.NullBool
+ scriptsEnabledStr, ok := rows[0]["scripts_enabled"]
+ if ok {
+ scriptsEnabled.Bool = scriptsEnabledStr == "1"
+ scriptsEnabled.Valid = true
+ }
+ if err := ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, version, desktopVersion, scriptsEnabled); err != nil {
return ctxerr.Wrap(ctx, err, "directIngestOrbitInfo update host orbit info")
}
diff --git a/server/service/scripts.go b/server/service/scripts.go
index e383b49746..0017648a4b 100644
--- a/server/service/scripts.go
+++ b/server/service/scripts.go
@@ -202,6 +202,13 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript
return nil, fleet.NewUserMessageError(errors.New(fleet.RunScriptDisabledErrMsg), http.StatusUnprocessableEntity)
}
+ // If scripts are disabled (according to the last detail query), we return an error.
+ // host.ScriptsEnabled may be nil for older orbit versions.
+ if host.ScriptsEnabled != nil && !*host.ScriptsEnabled {
+ svc.authz.SkipAuthorization(ctx)
+ return nil, fleet.NewUserMessageError(errors.New(fleet.RunScriptsOrbitDisabledErrMsg), http.StatusUnprocessableEntity)
+ }
+
maxPending := maxPendingScripts
// authorize with the host's team and the script id provided, as both affect
diff --git a/server/service/team_policies.go b/server/service/team_policies.go
index 15a7a90acd..7786c7fe68 100644
--- a/server/service/team_policies.go
+++ b/server/service/team_policies.go
@@ -154,8 +154,8 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee
/////////////////////////////////////////////////////////////////////////////////
type countTeamPoliciesRequest struct {
- fleet.ListOptions `url:"list_options"`
- TeamID uint `url:"team_id"`
+ ListOptions fleet.ListOptions `url:"list_options"`
+ TeamID uint `url:"team_id"`
}
type countTeamPoliciesResponse struct {
@@ -167,7 +167,7 @@ func (r countTeamPoliciesResponse) error() error { return r.Err }
func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*countTeamPoliciesRequest)
- resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.MatchQuery)
+ resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery)
if err != nil {
return countTeamPoliciesResponse{Err: err}, nil
}
diff --git a/server/service/testing_client.go b/server/service/testing_client.go
index 5b67ada9ee..493d35bccb 100644
--- a/server/service/testing_client.go
+++ b/server/service/testing_client.go
@@ -40,7 +40,7 @@ type withDS struct {
func (ts *withDS) SetupSuite(dbName string) {
t := ts.s.T()
ts.ds = mysql.CreateNamedMySQLDS(t, dbName)
- test.AddAllHostsLabel(t, ts.ds)
+ test.AddBuiltinLabels(t, ts.ds)
// setup the required fields on AppConfig
appConf, err := ts.ds.AppConfig(context.Background())
diff --git a/server/service/translator.go b/server/service/translator.go
index 078fbf09d9..f81177e6b4 100644
--- a/server/service/translator.go
+++ b/server/service/translator.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "fmt"
"github.com/fleetdm/fleet/v4/server/fleet"
)
@@ -61,6 +62,12 @@ func translateHostToID(ctx context.Context, ds fleet.Datastore, identifier strin
}
func (svc *Service) Translate(ctx context.Context, payloads []fleet.TranslatePayload) ([]fleet.TranslatePayload, error) {
+ if len(payloads) == 0 {
+ // skip auth since there is no case in which this request will make sense with no payloads
+ svc.authz.SkipAuthorization(ctx)
+ return nil, badRequest("payloads must not be empty")
+ }
+
var finalPayload []fleet.TranslatePayload
for _, payload := range payloads {
@@ -88,7 +95,15 @@ func (svc *Service) Translate(ctx context.Context, payloads []fleet.TranslatePay
}
translateFunc = translateHostToID
default:
- return nil, fleet.NewErrorf(fleet.ErrNoUnknownTranslate, "Type %s is unknown.", payload.Type)
+ // if no supported payload type, this is bad regardless of authorization
+ svc.authz.SkipAuthorization(ctx)
+ return nil, badRequestErr(
+ fmt.Sprintf("Type %s is unknown. ", payload.Type),
+ fleet.NewErrorf(
+ fleet.ErrNoUnknownTranslate,
+ "Type %s is unknown.",
+ payload.Type),
+ )
}
id, err := translateFunc(ctx, svc.ds, payload.Payload.Identifier)
diff --git a/server/service/transport.go b/server/service/transport.go
index e7b2ece2e1..8db2a4b976 100644
--- a/server/service/transport.go
+++ b/server/service/transport.go
@@ -186,7 +186,7 @@ func listOptionsFromRequest(r *http.Request) (fleet.ListOptions, error) {
PerPage: uint(perPage),
OrderKey: orderKey,
OrderDirection: orderDirection,
- MatchQuery: query,
+ MatchQuery: strings.TrimSpace(query),
After: afterString,
}, nil
}
diff --git a/server/service/vulnerabilities_test.go b/server/service/vulnerabilities_test.go
index 0e5ee6aa53..fd0a7e2eec 100644
--- a/server/service/vulnerabilities_test.go
+++ b/server/service/vulnerabilities_test.go
@@ -46,7 +46,7 @@ func TestListVulnerabilities(t *testing.T) {
require.Contains(t, err.Error(), "invalid order key")
// valid order key
- opts.OrderKey = "cve"
+ opts.ListOptions.OrderKey = "cve"
_, _, err = svc.ListVulnerabilities(ctx, opts)
require.NoError(t, err)
})
diff --git a/server/test/new_objects.go b/server/test/new_objects.go
index 8db4f56858..29963e0773 100644
--- a/server/test/new_objects.go
+++ b/server/test/new_objects.go
@@ -106,6 +106,74 @@ func AddAllHostsLabel(t *testing.T, ds fleet.Datastore) {
require.NoError(t, err)
}
+func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) {
+ builtins := []*fleet.Label{
+ {
+ Name: "All Hosts",
+ Query: "select 1",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeManual,
+ },
+ {
+ Name: "macOS",
+ Query: "select 1 from os_version where platform = 'darwin';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "Ubuntu Linux",
+ Query: "select 1 from os_version where platform = 'ubuntu';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "CentOS Linux",
+ Query: "select 1 from os_version where platform = 'centos' or name like '%centos%';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "MS Windows",
+ Query: "select 1 from os_version where platform = 'windows';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "Red Hat Linux",
+ Query: "SELECT 1 FROM os_version WHERE name LIKE '%red hat%'",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "All Linux",
+ Query: "SELECT 1 FROM osquery_info WHERE build_platform LIKE '%ubuntu%' OR build_distro LIKE '%centos%';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ {
+ Name: "chrome",
+ Query: "select 1 from os_version where platform = 'chrome';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
+ }
+
+ names := fleet.ReservedLabelNames()
+ require.Equal(t, len(builtins), len(names))
+ storedByName := map[string]*fleet.Label{}
+ for _, b := range builtins {
+ stored, err := ds.NewLabel(context.Background(), b)
+ require.NoError(t, err)
+ storedByName[stored.Name] = stored
+ }
+ require.Len(t, storedByName, len(builtins))
+
+ for name := range names {
+ _, ok := storedByName[name]
+ require.True(t, ok, "expected label %s to be created", name)
+ }
+}
+
// NewHostOption is an Option for the NewHost function.
type NewHostOption func(*fleet.Host)
diff --git a/server/vulnerabilities/oval/parsed/utils.go b/server/vulnerabilities/oval/parsed/utils.go
index d9fd835b2c..ad6ffafd67 100644
--- a/server/vulnerabilities/oval/parsed/utils.go
+++ b/server/vulnerabilities/oval/parsed/utils.go
@@ -20,7 +20,7 @@ func ReplaceFedoraOSVersion(version string) string {
"Red Hat Enterprise Linux 6.0.0": regexp.MustCompile(`Fedora Linux (12|13|14|15|16|17|18)\.`),
"Red Hat Enterprise Linux 7.0.0": regexp.MustCompile(`Fedora Linux (19|20|21|22|23|24|25|26|27)\.`),
"Red Hat Enterprise Linux 8.0.0": regexp.MustCompile(`Fedora Linux (28|29|30|31|32|33)\.`),
- "Red Hat Enterprise Linux 9.0.0": regexp.MustCompile(`Fedora Linux (34|35|36)\.`),
+ "Red Hat Enterprise Linux 9.0.0": regexp.MustCompile(`Fedora Linux (34|35|36|37|38|39|40)\.`),
}
for rep, pattern := range rules {
if pattern.ReplaceAllString(version, rep) != version {
diff --git a/server/vulnerabilities/utils/rpmvercmp.go b/server/vulnerabilities/utils/rpmvercmp.go
index 7c5016f1ca..beabd1035d 100644
--- a/server/vulnerabilities/utils/rpmvercmp.go
+++ b/server/vulnerabilities/utils/rpmvercmp.go
@@ -69,9 +69,9 @@ func (s segment) compare(b segment) int {
return 0
} else if *s.number < *b.number {
return -1
- } else {
- return 1
}
+
+ return 1
// 'a' is a number seg, 'b' is a letter seg,
// numbers are always greater than letters
} else if s.number != nil && b.number == nil {
@@ -81,12 +81,12 @@ func (s segment) compare(b segment) int {
return -1
// Both segs are letters, then we just
// compare them
- } else {
- if s.letters == b.letters {
- return 0
- }
- return strings.Compare(s.letters, b.letters)
}
+
+ if s.letters == b.letters {
+ return 0
+ }
+ return strings.Compare(s.letters, b.letters)
}
// Returns the next maximal alphabetic or numeric segment,
diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go
index 01624d85d0..0537af05e8 100644
--- a/server/worker/apple_mdm.go
+++ b/server/worker/apple_mdm.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@@ -238,6 +239,13 @@ func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArg
return ctxerr.Wrap(ctx, err, "failed to get host MDM profiles")
}
for _, prof := range profs {
+ // NOTE: DDM profiles (declarations) are ignored because while a device is
+ // awaiting to be released, it cannot process a DDM session (at least
+ // that's what we noticed during testing).
+ if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
+ continue
+ }
+
// if it has any pending profiles, then its profiles are not done being
// delivered (installed or removed).
if prof.Status == nil || *prof.Status == fleet.MDMDeliveryPending {
diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go
index 822d67dfd8..0f597102b2 100644
--- a/server/worker/apple_mdm_test.go
+++ b/server/worker/apple_mdm_test.go
@@ -75,12 +75,8 @@ func TestAppleMDM(t *testing.T) {
return err
})
if depAssignedToFleet {
- mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- _, err := q.ExecContext(ctx, `
- INSERT INTO host_dep_assignments (host_id) VALUES (?) ON DUPLICATE KEY UPDATE host_id = host_id, deleted_at = NULL
- `, h.ID)
- return err
- })
+ err := ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h})
+ require.NoError(t, err)
}
err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "http://example.com", depAssignedToFleet, fleet.WellKnownMDMFleet, "")
require.NoError(t, err)
diff --git a/terraform/addons/logging-destination-firehose/.terraform.lock.hcl b/terraform/addons/logging-destination-firehose/.terraform.lock.hcl
new file mode 100644
index 0000000000..3d971a357d
--- /dev/null
+++ b/terraform/addons/logging-destination-firehose/.terraform.lock.hcl
@@ -0,0 +1,24 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "5.44.0"
+ hashes = [
+ "h1:QqMTKuyylmJ633mwNheXdFupfd5sozqCUTTSj89pnm8=",
+ "zh:1224a42bb04574785549b89815d98bda11f6e9992352fc6c36c5622f3aea91c0",
+ "zh:2a8d1095a2f1ab097f516d9e7e0d289337849eebb3fcc34f075070c65063f4fa",
+ "zh:46cce11150eb4934196d9bff693b72d0494c85917ceb3c2914d5ff4a785af861",
+ "zh:4a7c15d585ee747d17f4b3904851cd95cfbb920fa197aed3df78e8d7ef9609b6",
+ "zh:508f1a85a0b0f93bf26341207d809bd55b60c8fdeede40097d91f30111fc6f5d",
+ "zh:52f968ffc21240213110378d0ffb298cbd23e9157a6d01dfac5a4360492d69c2",
+ "zh:5e9846b48ef03eb59541049e81b15cae8bc7696a3779ae4a5412fdce60bb24e0",
+ "zh:850398aecaf7dc0231fc320fdd6dffe41836e07a54c8c7b40eb28e7525d3c0a9",
+ "zh:8f87eeb05bdd1b873b6cfb3898dfad6402ac180dfa3c8f9754df8f85dcf92ca6",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:c726b87cd6ed111536f875dccedecff21abc802a4087264515ffab113cac36dc",
+ "zh:d57ea706d2f98b93c7b05b0c6bc3420de8e8cf2d0b6703085dc15ed239b2cc49",
+ "zh:d5d1a21246e68c2a7a04c5619eb0ad5a81644f644c432cb690537b816a156de2",
+ "zh:e869904cac41114b7e4ee66bcd2ce4585ed15ca842040a60cb47119f69472c91",
+ "zh:f1a09f2f3ea72cbe795b865cf31ad9b1866a536a8050cf0bb93d3fa51069582e",
+ ]
+}
diff --git a/terraform/addons/logging-destination-firehose/README.md b/terraform/addons/logging-destination-firehose/README.md
index c7ce437a30..054cc69b3a 100644
--- a/terraform/addons/logging-destination-firehose/README.md
+++ b/terraform/addons/logging-destination-firehose/README.md
@@ -9,7 +9,7 @@ No requirements.
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | 5.25.0 |
+| [aws](#provider\_aws) | 5.44.0 |
## Modules
@@ -46,6 +46,7 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
+| [compression\_format](#input\_compression\_format) | n/a | `string` | `"UNCOMPRESSED"` | no |
| [osquery\_results\_s3\_bucket](#input\_osquery\_results\_s3\_bucket) | n/a | object({
name = optional(string, "fleet-osquery-results-archive")
expires_days = optional(number, 1)
}) | {
"expires_days": 1,
"name": "fleet-osquery-results-archive"
} | no |
| [osquery\_status\_s3\_bucket](#input\_osquery\_status\_s3\_bucket) | n/a | object({
name = optional(string, "fleet-osquery-status-archive")
expires_days = optional(number, 1)
}) | {
"expires_days": 1,
"name": "fleet-osquery-status-archive"
} | no |
diff --git a/terraform/addons/logging-destination-firehose/main.tf b/terraform/addons/logging-destination-firehose/main.tf
index 8f8bd958e6..fcefd188de 100644
--- a/terraform/addons/logging-destination-firehose/main.tf
+++ b/terraform/addons/logging-destination-firehose/main.tf
@@ -151,8 +151,9 @@ resource "aws_kinesis_firehose_delivery_stream" "osquery_results" {
destination = "extended_s3"
extended_s3_configuration {
- role_arn = aws_iam_role.firehose-results.arn
- bucket_arn = aws_s3_bucket.osquery-results.arn
+ compression_format = var.compression_format
+ role_arn = aws_iam_role.firehose-results.arn
+ bucket_arn = aws_s3_bucket.osquery-results.arn
}
}
@@ -161,8 +162,9 @@ resource "aws_kinesis_firehose_delivery_stream" "osquery_status" {
destination = "extended_s3"
extended_s3_configuration {
- role_arn = aws_iam_role.firehose-status.arn
- bucket_arn = aws_s3_bucket.osquery-status.arn
+ compression_format = var.compression_format
+ role_arn = aws_iam_role.firehose-status.arn
+ bucket_arn = aws_s3_bucket.osquery-status.arn
}
}
diff --git a/terraform/addons/logging-destination-firehose/variables.tf b/terraform/addons/logging-destination-firehose/variables.tf
index 3b3ca4524a..c97e2c36f6 100644
--- a/terraform/addons/logging-destination-firehose/variables.tf
+++ b/terraform/addons/logging-destination-firehose/variables.tf
@@ -19,3 +19,7 @@ variable "osquery_status_s3_bucket" {
expires_days = 1
}
}
+
+variable "compression_format" {
+ default = "UNCOMPRESSED"
+}
diff --git a/terraform/byo-vpc/README.md b/terraform/byo-vpc/README.md
index 0cc44fdec0..88fccdd0b9 100644
--- a/terraform/byo-vpc/README.md
+++ b/terraform/byo-vpc/README.md
@@ -33,7 +33,7 @@ No requirements.
|------|-------------|------|---------|:--------:|
| [alb\_config](#input\_alb\_config) | n/a | object({
name = optional(string, "fleet")
subnets = list(string)
security_groups = optional(list(string), [])
access_logs = optional(map(string), {})
certificate_arn = string
allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
allowed_ipv6_cidrs = optional(list(string), ["::/0"])
egress_cidrs = optional(list(string), ["0.0.0.0/0"])
egress_ipv6_cidrs = optional(list(string), ["::/0"])
extra_target_groups = optional(any, [])
https_listener_rules = optional(any, [])
tls_policy = optional(string, "ELBSecurityPolicy-TLS-1-2-2017-01")
idle_timeout = optional(number, 60)
}) | n/a | yes |
| [ecs\_cluster](#input\_ecs\_cluster) | The config for the terraform-aws-modules/ecs/aws module | object({
autoscaling_capacity_providers = optional(any, {})
cluster_configuration = optional(any, {
execute_command_configuration = {
logging = "OVERRIDE"
log_configuration = {
cloud_watch_log_group_name = "/aws/ecs/aws-ec2"
}
}
})
cluster_name = optional(string, "fleet")
cluster_settings = optional(map(string), {
"name" : "containerInsights",
"value" : "enabled",
})
create = optional(bool, true)
default_capacity_provider_use_fargate = optional(bool, true)
fargate_capacity_providers = optional(any, {
FARGATE = {
default_capacity_provider_strategy = {
weight = 100
}
}
FARGATE_SPOT = {
default_capacity_provider_strategy = {
weight = 0
}
}
})
tags = optional(map(string))
}) | {
"autoscaling_capacity_providers": {},
"cluster_configuration": {
"execute_command_configuration": {
"log_configuration": {
"cloud_watch_log_group_name": "/aws/ecs/aws-ec2"
},
"logging": "OVERRIDE"
}
},
"cluster_name": "fleet",
"cluster_settings": {
"name": "containerInsights",
"value": "enabled"
},
"create": true,
"default_capacity_provider_use_fargate": true,
"fargate_capacity_providers": {
"FARGATE": {
"default_capacity_provider_strategy": {
"weight": 100
}
},
"FARGATE_SPOT": {
"default_capacity_provider_strategy": {
"weight": 0
}
}
},
"tags": {}
} | no |
-| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.0")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
+| [fleet\_config](#input\_fleet\_config) | The configuration object for Fleet itself. Fields that default to null will have their respective resources created if not specified. | object({
mem = optional(number, 4096)
cpu = optional(number, 512)
image = optional(string, "fleetdm/fleet:v4.48.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
mount_points = optional(list(any), [])
volumes = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
extra_iam_policies = optional(list(string), [])
extra_execution_iam_policies = optional(list(string), [])
extra_secrets = optional(map(string), {})
security_groups = optional(list(string), null)
security_group_name = optional(string, "fleet")
iam_role_arn = optional(string, null)
service = optional(object({
name = optional(string, "fleet")
}), {
name = "fleet"
})
database = optional(object({
password_secret_arn = string
user = string
database = string
address = string
rr_address = optional(string, null)
}), {
password_secret_arn = null
user = null
database = null
address = null
rr_address = null
})
redis = optional(object({
address = string
use_tls = optional(bool, true)
}), {
address = null
use_tls = true
})
awslogs = optional(object({
name = optional(string, null)
region = optional(string, null)
create = optional(bool, true)
prefix = optional(string, "fleet")
retention = optional(number, 5)
}), {
name = null
region = null
prefix = "fleet"
retention = 5
})
loadbalancer = optional(object({
arn = string
}), {
arn = null
})
extra_load_balancers = optional(list(any), [])
networking = optional(object({
subnets = list(string)
security_groups = optional(list(string), null)
}), {
subnets = null
security_groups = null
})
autoscaling = optional(object({
max_capacity = optional(number, 5)
min_capacity = optional(number, 1)
memory_tracking_target_value = optional(number, 80)
cpu_tracking_target_value = optional(number, 80)
}), {
max_capacity = 5
min_capacity = 1
memory_tracking_target_value = 80
cpu_tracking_target_value = 80
})
iam = optional(object({
role = optional(object({
name = optional(string, "fleet-role")
policy_name = optional(string, "fleet-iam-policy")
}), {
name = "fleet-role"
policy_name = "fleet-iam-policy"
})
execution = optional(object({
name = optional(string, "fleet-execution-role")
policy_name = optional(string, "fleet-execution-role")
}), {
name = "fleet-execution-role"
policy_name = "fleet-iam-policy-execution"
})
}), {
name = "fleetdm-execution-role"
})
}) | {
"autoscaling": {
"cpu_tracking_target_value": 80,
"max_capacity": 5,
"memory_tracking_target_value": 80,
"min_capacity": 1
},
"awslogs": {
"create": true,
"name": null,
"prefix": "fleet",
"region": null,
"retention": 5
},
"cpu": 256,
"database": {
"address": null,
"database": null,
"password_secret_arn": null,
"rr_address": null,
"user": null
},
"depends_on": [],
"extra_environment_variables": {},
"extra_execution_iam_policies": [],
"extra_iam_policies": [],
"extra_load_balancers": [],
"extra_secrets": {},
"family": "fleet",
"iam": {
"execution": {
"name": "fleet-execution-role",
"policy_name": "fleet-iam-policy-execution"
},
"role": {
"name": "fleet-role",
"policy_name": "fleet-iam-policy"
}
},
"iam_role_arn": null,
"image": "fleetdm/fleet:v4.31.1",
"loadbalancer": {
"arn": null
},
"mem": 512,
"mount_points": [],
"networking": {
"security_groups": null,
"subnets": null
},
"redis": {
"address": null,
"use_tls": true
},
"security_group_name": "fleet",
"security_groups": null,
"service": {
"name": "fleet"
},
"sidecars": [],
"volumes": []
} | no |
| [migration\_config](#input\_migration\_config) | The configuration object for Fleet's migration task. | object({
mem = number
cpu = number
}) | {
"cpu": 1024,
"mem": 2048
} | no |
| [rds\_config](#input\_rds\_config) | The config for the terraform-aws-modules/rds-aurora/aws module | object({
name = optional(string, "fleet")
engine_version = optional(string, "8.0.mysql_aurora.3.02.2")
instance_class = optional(string, "db.t4g.large")
subnets = optional(list(string), [])
allowed_security_groups = optional(list(string), [])
allowed_cidr_blocks = optional(list(string), [])
apply_immediately = optional(bool, true)
monitoring_interval = optional(number, 10)
db_parameter_group_name = optional(string)
db_parameters = optional(map(string), {})
db_cluster_parameter_group_name = optional(string)
db_cluster_parameters = optional(map(string), {})
enabled_cloudwatch_logs_exports = optional(list(string), [])
master_username = optional(string, "fleet")
snapshot_identifier = optional(string)
cluster_tags = optional(map(string), {})
preferred_maintenance_window = optional(string, "thu:23:00-fri:00:00")
}) | {
"allowed_cidr_blocks": [],
"allowed_security_groups": [],
"apply_immediately": true,
"cluster_tags": {},
"db_cluster_parameter_group_name": null,
"db_cluster_parameters": {},
"db_parameter_group_name": null,
"db_parameters": {},
"enabled_cloudwatch_logs_exports": [],
"engine_version": "8.0.mysql_aurora.3.02.2",
"instance_class": "db.t4g.large",
"master_username": "fleet",
"monitoring_interval": 10,
"name": "fleet",
"preferred_maintenance_window": "thu:23:00-fri:00:00",
"snapshot_identifier": null,
"subnets": []
} | no |
| [redis\_config](#input\_redis\_config) | n/a | object({
name = optional(string, "fleet")
replication_group_id = optional(string)
elasticache_subnet_group_name = optional(string, "")
allowed_security_group_ids = optional(list(string), [])
subnets = list(string)
allowed_cidrs = list(string)
availability_zones = optional(list(string), [])
cluster_size = optional(number, 3)
instance_type = optional(string, "cache.m5.large")
apply_immediately = optional(bool, true)
automatic_failover_enabled = optional(bool, false)
engine_version = optional(string, "6.x")
family = optional(string, "redis6.x")
at_rest_encryption_enabled = optional(bool, true)
transit_encryption_enabled = optional(bool, true)
parameter = optional(list(object({
name = string
value = string
})), [])
log_delivery_configuration = optional(list(map(any)), [])
tags = optional(map(string), {})
}) | {
"allowed_cidrs": null,
"allowed_security_group_ids": [],
"apply_immediately": true,
"at_rest_encryption_enabled": true,
"automatic_failover_enabled": false,
"availability_zones": [],
"cluster_size": 3,
"elasticache_subnet_group_name": "",
"engine_version": "6.x",
"family": "redis6.x",
"instance_type": "cache.m5.large",
"log_delivery_configuration": [],
"name": "fleet",
"parameter": [],
"replication_group_id": null,
"subnets": null,
"tags": {},
"transit_encryption_enabled": true
} | no |
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 20b8249bff..7dc23f8b20 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -13,7 +13,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.0")
+ image = optional(string, "fleetdm/fleet:v4.48.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index 88cb0c03fb..1da53d22ce 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -74,7 +74,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.0")
+ image = optional(string, "fleetdm/fleet:v4.48.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index 28311ecee1..38d32e4cc0 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.48.0"
+ fleet_image = "fleetdm/fleet:v4.48.2"
domain_name = "example.com"
}
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index 4ad249b0b0..f45a1229d9 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -167,7 +167,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.0")
+ image = optional(string, "fleetdm/fleet:v4.48.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 04b2691900..adfafe3cfb 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -59,8 +59,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.48.0"
- image = "fleetdm/fleet:v4.48.0" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.48.2"
+ image = "fleetdm/fleet:v4.48.2" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
diff --git a/terraform/variables.tf b/terraform/variables.tf
index a5cb956185..06cedeb8bd 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -215,7 +215,7 @@ variable "fleet_config" {
type = object({
mem = optional(number, 4096)
cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.48.0")
+ image = optional(string, "fleetdm/fleet:v4.48.2")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index 80a5a278cb..4067a3c169 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.48.0",
+ "version": "v4.48.2",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/mdm/apple/dep_sample_profile.json b/tools/mdm/apple/dep_sample_profile.json
deleted file mode 100644
index af76650f54..0000000000
--- a/tools/mdm/apple/dep_sample_profile.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "profile_name": "Fleet Device Management Inc.",
- "allow_pairing": true,
- "auto_advance_setup": false,
- "department": "it@fleetdm.com",
- "is_supervised": false,
- "is_multi_user": false,
- "is_mandatory": false,
- "is_mdm_removable": true,
- "language": "en",
- "org_magic": "1",
- "region": "US",
- "support_phone_number": "+1 408 555 1010",
- "support_email_address": "support@fleetdm.com",
- "anchor_certs": [],
- "supervising_host_certs": [],
- "skip_setup_items": ["Accessibility", "Appearance", "AppleID", "AppStore", "Biometric", "Diagnostics", "FileVault", "iCloudDiagnostics", "iCloudStorage", "Location", "Payment", "Privacy", "Restore", "ScreenTime", "Siri", "TermsOfAddress", "TOS", "UnlockWithWatch"]
-}
diff --git a/tools/mdm/apple/glossary-and-protocols.md b/tools/mdm/apple/glossary-and-protocols.md
index b4324a05a7..c0dcf8278b 100644
--- a/tools/mdm/apple/glossary-and-protocols.md
+++ b/tools/mdm/apple/glossary-and-protocols.md
@@ -80,7 +80,8 @@ For [DEP enrollment](#dep-device-enrollment-program) the enrollment profile is d
This (JSON) profile is used to configure a device in Apple Business Manager.
It contains all the necessary information that a device needs to automatically enroll to an MDM server during device setup.
-Sample: [dep_sample_profile.json](https://github.com/fleetdm/nanodep/blob/main/docs/dep-profile.example.json).
+[Example](https://fleetdm.com/example-dep-profile)
+
See all fields [here](https://developer.apple.com/documentation/devicemanagement/profile).
### Commands
diff --git a/tools/oncall/README.md b/tools/oncall/README.md
new file mode 100644
index 0000000000..5ada653c50
--- /dev/null
+++ b/tools/oncall/README.md
@@ -0,0 +1,8 @@
+# Oncall
+
+You can use the `oncall.sh` script to find out if there are any open issues or PRs from the community:
+```sh
+gh auth login
+./tools/oncall/oncall.sh issues
+./tools/oncall/oncall.sh prs
+```
diff --git a/scripts/on-call b/tools/oncall/oncall.sh
similarity index 78%
rename from scripts/on-call
rename to tools/oncall/oncall.sh
index 13cfdeed0b..7b8c3f1e0d 100755
--- a/scripts/on-call
+++ b/tools/oncall/oncall.sh
@@ -9,7 +9,7 @@ usage() {
Contains useful commands for on-call.
Usage:
- $(basename $0)
+ $(basename "$0")
Commands:
issues List open issues from outside contributors.
@@ -18,7 +18,7 @@ EOF
}
require() {
- type $1 >/dev/null 2>&1 || {
+ type "$1" >/dev/null 2>&1 || {
echo "$1 is required but not installed. Aborting." >&2
exit 1
}
@@ -29,12 +29,12 @@ issues() {
require jq
auth_status="$(gh auth status -t 2>&1)"
- username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to [^[:space:]]+ as ([^[:space:]]+).*/\1/p')"
+ username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to github.com account ([^[:space:]]+).*/\1/p')"
token="$(echo "${auth_status}" | sed -n -r 's/^.*Token: ([a-zA-Z0-9_]*)/\1/p')"
- members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members | jq -r 'map(.login)')"
+ members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members?per_page=100 | jq -r 'map(.login)')"
- gh pr list --repo fleetdm/fleet --label "bug" --label ":reproduce" --json id,title,author,url,createdAt |
+ gh issue list --repo fleetdm/fleet --json id,title,author,url,createdAt,labels --limit 100 |
jq -r --argjson members "$members" \
'map(select(.author.login as $in | $members | index($in) | not)) | sort_by(.createdAt) | reverse'
}
@@ -44,10 +44,11 @@ prs() {
require jq
auth_status="$(gh auth status -t 2>&1)"
- username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to [^[:space:]]+ as ([^[:space:]]+).*/\1/p')"
+ username="$(echo "${auth_status}" | sed -n -r 's/^.* Logged in to github.com account ([^[:space:]]+).*/\1/p')"
token="$(echo "${auth_status}" | sed -n -r 's/^.*Token: ([a-zA-Z0-9_]*)/\1/p')"
- members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members | jq -r 'map(.login)')"
+ members="$(curl -s -u "${username}:${token}" https://api.github.com/orgs/fleetdm/members?per_page=100 | jq -r 'map(.login)' | jq '. += ["app/dependabot"]')"
+
# defaults to listing open prs
gh pr list --repo fleetdm/fleet --json id,title,author,url,createdAt |
jq -r --argjson members "$members" \
diff --git a/tools/release/publish_release.sh b/tools/release/publish_release.sh
index 6cc615af17..f056d08ad5 100755
--- a/tools/release/publish_release.sh
+++ b/tools/release/publish_release.sh
@@ -190,7 +190,7 @@ build_changelog() {
prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.'
if [[ "$main_release" == "true" ]]; then
# Place to make a main targeted prompt
- #prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.'
+ prompt=$'I am creating a changelog for an open source project from a list of commit messages. Please format it for me using the following rules:\n1. Correct spelling and punctuation.\n2. Sentence casing.\n3. Past tense.\n4. Each list item is designated with an asterisk.\n5. Output in markdown format.'
fi
content=$(cat new_changelog | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g')
@@ -263,7 +263,7 @@ changelog_and_versions() {
cp /tmp/CHANGELOG.md .
git add CHANGELOG.md
escaped_start_version=$(echo "$start_milestone" | sed 's/\./\\./g')
- version_files=`ack -l --ignore-file=is:CHANGELOG.md "$escaped_start_version"`
+ version_files=`ack -l --ignore-dir=tools/release --ignore-dir=articles --ignore-file=is:CHANGELOG.md "$escaped_start_version"`
unameOut="$(uname -s)"
case "${unameOut}" in
Linux*) echo "$version_files" | xargs sed -i "s/$escaped_start_version/$target_milestone/g";;
diff --git a/tools/tuf/README.md b/tools/tuf/README.md
new file mode 100644
index 0000000000..857859809d
--- /dev/null
+++ b/tools/tuf/README.md
@@ -0,0 +1,288 @@
+# Releasing updates to Fleet's TUF repository
+
+The `releaser.sh` script automates the building and releasing of fleetd and osquery updates on [Fleet's TUF repository](https://tuf.fleetctl.com).
+
+> - The script was developed and tested on macOS Intel.
+> - It currently supports pushing new `fleetd` and `osqueryd` versions.
+> - By storing credentials encrypted in a USB flash drive and storing their decryption passphrase on 1Password we are enforcing a form of 2FA.
+
+```mermaid
+graph LR;
+ subgraph Workstation;
+ releaser[releaser.sh];
+ 1password("
1Password");
+ usb("
USB flash drive");
+ repository[(./repository)];
+ end;
+ s3("
s3://fleet-tuf-repo");
+ github("
Github Action\n(signing and notarization)");
+
+ usb--(1) copy encrypted signing keys-->releaser;
+ 1password--(2) get passphrases to decrypt encrypted signing keys-->releaser;
+ 1password--(3) get Github API token-->releaser;
+ s3--(4) pull TUF repository-->releaser;
+ releaser--(5) build components (new updates)\n(osqueryd, orbit, Fleet Desktop)-->github;
+ github--(6) download built components-->releaser;
+ releaser--(7) push updates and signed metadata-->s3;
+```
+
+## Permissions and configuration
+
+Following is the checklist for all credentials and configuration needed to run the script.
+
+### Dependencies
+
+- `make`
+- `git`
+- 1Password 8 application.
+- Install and configure 1Password's `op` cli to connect to the application: https://developer.1password.com/docs/cli/get-started/
+- `aws` cli :`brew install awscli`.
+- `fleetctl`: Either built from source or installed by npm.
+- `tuf`: Download the release from https://github.com/theupdateframework/go-tuf/releases/download/v0.7.0/tuf_0.7.0_darwin_amd64.tar.gz and place the `tuf` executable in `/usr/local/bin/tuf`. You will need to make an exception in "Privacy & Security" because the executable is not signed.
+
+### 1Password
+
+You need to create three passphrases on your private 1Password vault for encrypting the signing keys (more on signing keys below).
+Create three private "passwords" with the following names: `TUF TARGETS`, `TUF SNAPSHOT` and `TUF TIMESTAMP`.
+The resulting credentials will have the following "path" within 1Password (these paths will be provided to the `releaser.sh` script)
+```sh
+Private/TUF TARGETS/password
+Private/TUF SNAPSHOT/password
+Private/TUF TIMESTAMP/password
+```
+
+### AWS
+
+The following is required to be able to run `aws` cli commands.
+
+1. You will need to request the infrastructure team to add the "TUFAdministrators" role to your Google account.
+2. Configure AWS SSO with the following steps: https://github.com/fleetdm/confidential/tree/main/infrastructure/sso#how-to-use-sso.
+Set the profile name as `tuf` (the profile name will be provided to the `releaser.sh` script).
+3. Test the access by running:
+```sh
+AWS_PROFILE=tuf aws sso login
+```
+
+### TUF signing keys
+
+> You can skip this step if you already have authorized keys to sign and publish updates.
+
+To release updates to our TUF repository you need the `root` role (ask in Slack who has such `root` role) to sign your signing keys.
+First, run the following script
+```sh
+AWS_PROFILE=tuf \
+ACTION=generate-signing-keys \
+TUF_DIRECTORY=/Users/luk/tuf3.fleetctl.com \
+TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \
+SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \
+TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \
+./tools/tuf/releaser.sh
+```
+
+The human with the `root` role will run the following commands to sign the provided `staged/root.json`:
+```sh
+tuf sign
+tuf snapshot
+tuf timestamp
+tuf commit
+```
+And push the newly signed `root.json` to the remote repository.
+
+### Encrypted keys in USB
+
+For releasing fleetd you need to plug in the USB that contains encrypted signing keys.
+In this guide we assume the USB device will be mounted in `/Volumes/FLEET-TUF/` and it ONLY contains a `keys/` directory.
+
+### Github
+
+#### Personal access token
+
+> A personal access token is required to download artifacts from Github Actions using the Github API.
+
+1. Create a fine-grained personal access token at https://github.com/settings/tokens?type=beta
+2. Store the token on 1Password as a "password" with name "Github Token"
+The resulting credential will have the following "path" within 1Password (this path will be provided to the script)
+```sh
+Private/Github Token/password
+```
+
+#### Github session
+
+You need to log in to your Github account with your default browser.
+It will be used to open your browser and allow you to create the PR needed to build artifacts (this can be improved later, see TODOs).
+
+## Samples
+
+Following are samples of the script execution to release components to `edge` and `stable`.
+
+> When releasing fleetd you need to checkout the branch (e.g. `main`) you want to release.
+
+> NOTE: When releasing fleetd:
+> If there are only `orbit` changes on a release we still have to release the `desktop` component with its version string bumped
+> (even if there are no changes in it). This is due to the fact that we want users to see the new version in the tray icon,
+> e.g. `"Fleet Desktop v1.21.0"`. Technical debt: We could improve this process to reduce the complexity of releasing
+> fleetd when there are no Fleet Desktop changes.
+
+### Releasing to `edge`
+
+#### Releasing fleetd `1.23.0` to `edge`
+
+```sh
+AWS_PROFILE=tuf \
+TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \
+COMPONENT=fleetd \
+ACTION=release-to-edge \
+VERSION=1.23.0 \
+KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \
+TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \
+SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \
+TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \
+GITHUB_USERNAME=foobar \
+GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \
+PUSH_TO_REMOTE=1 \
+./tools/tuf/releaser.sh
+```
+
+#### Releasing osquery `5.12.1` to `edge`
+
+```sh
+AWS_PROFILE=tuf \
+TUF_DIRECTORY=/Users/luk/tuf.fleetctl.com \
+COMPONENT=osqueryd \
+ACTION=release-to-edge \
+VERSION=5.12.1 \
+KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \
+TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \
+SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \
+TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \
+GITHUB_USERNAME=foobar \
+GITHUB_TOKEN_1PASSWORD_PATH="Private/Github Token/password" \
+PUSH_TO_REMOTE=1 \
+./tools/tuf/releaser.sh
+```
+
+### Promoting from `edge` to `stable`
+
+#### Promoting fleetd `1.23.0` from `edge` to `stable`
+
+```sh
+AWS_PROFILE=tuf \
+TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \
+COMPONENT=fleetd \
+ACTION=promote-edge-to-stable \
+VERSION=1.23.0 \
+KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \
+TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \
+SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \
+TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \
+PUSH_TO_REMOTE=1 \
+./tools/tuf/releaser.sh
+```
+
+#### Promoting osqueryd `5.12.1` from `edge` to `stable`
+
+```sh
+AWS_PROFILE=tuf \
+TUF_DIRECTORY=/Users/foobar/tuf.fleetctl.com \
+COMPONENT=osqueryd \
+ACTION=promote-edge-to-stable \
+VERSION=5.12.1 \
+KEYS_SOURCE_DIRECTORY=/Volumes/FLEET-TUF/keys \
+TARGETS_PASSPHRASE_1PASSWORD_PATH="Private/TUF TARGETS/password" \
+SNAPSHOT_PASSPHRASE_1PASSWORD_PATH="Private/TUF SNAPSHOT/password" \
+TIMESTAMP_PASSPHRASE_1PASSWORD_PATH="Private/TUF TIMESTAMP/password" \
+PUSH_TO_REMOTE=1 \
+./tools/tuf/releaser.sh
+```
+
+#### Releasing `swiftDialog` to `stable`
+
+> `releaser.sh` doesn't support `swiftDialog` yet.
+> macOS only component
+
+The `swiftDialog` executable can be generated from a macOS host by running:
+```sh
+make swift-dialog-app-tar-gz version=2.2.1 build=4591 out-path=.
+```
+```sh
+fleetctl updates add --target /path/to/macos/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version 2.2.1 -t edge
+```
+
+#### Releasing `nudge` to `stable`
+
+> `releaser.sh` doesn't support `nudge` yet.
+> macOS only component
+
+The `nudge` executable can be generated from a macOS host by running:
+```sh
+make nudge-app-tar-gz version=1.1.10.81462 out-path=.
+```
+```sh
+fleetctl updates add --target /path/to/macos/nudge.app.tar.gz --platform macos --name nudge --version 1.1.10.81462 -t edge
+```
+
+## Testing and improving the script
+
+- You can specify `GIT_REPOSITORY_DIRECTORY` to set a separate path for the Fleet repository (it uses the current by default).
+This is sometimes necessary if the tooling the script uses is not present in the branch we are trying to release from.
+```sh
+git clone git@github.com:fleetdm/fleet.git
+GIT_REPOSITORY_DIRECTORY=
+```
+
+- If the PR and orbit tag were already generated but you need to run the script again you can set `SKIP_PR_AND_TAG_PUSH=1` to skip that part.
+
+- While developing you can run with `PUSH_TO_REMOTE=0` to prevent pushing invalid metadata/components to the production repository.
+
+## TODOs to improve releaser.sh
+
+- Create the pull requests automatically using `gh` or the Github API.
+- Support releasing `nudge` and `swiftDialog`.
+
+## Troubleshooting
+
+### Removing Unused Targets
+
+If you've inadvertently published a target that is no longer in use, follow these steps to remove it.
+
+> Before performing any actions on Fleet's TUF repository you must:
+> 1. Make sure your local copy of the repository is up-to-date. See [Syncing Fleet's TUF repository](#syncing-fleets-tuf-repository).
+> 2. Create a local backup in case we mess up with the repository:
+> ```sh
+> mkdir ~/tuf.fleetctl.com/backup
+> cp -r ~/tuf.fleetctl.com ~/tuf.fleetctl.com-backup
+> ```
+
+1. You'll need the [`go-tuf`](https://github.com/theupdateframework/go-tuf) binary. The removal operations aren't integrated into `fleetctl` at the moment.
+2. Use `tuf remove` to remove the target and update `targets.json`. Substitute `desktop/windows/stable/desktop.exe` with the target you intend to delete.
+```sh
+tuf remove desktop/windows/stable/desktop.exe
+```
+3. Snapshot, timestamp, and commit the changes.
+```sh
+tuf snapshot
+tuf timestamp
+tuf commit
+```
+4. Run the following command to generate a timestamp that expires in two weeks (otherwise the default expiration when using `go-tuf` commands is 1 day)
+```sh
+fleetctl updates timestamp
+```
+5. Confirm that the version of the local `timestamp.json` file is more recent than that of the remote server.
+6. Verify the changes that will be synced by running a dry sync. Include the `--delete` flag as you're removing targets.
+```sh
+aws s3 sync ./repository s3://fleet-tuf-repo --delete --dryrun
+```
+7. `diff` the local `targets.json` file with its remote version.
+8. To upload the changes, perform a sync without the `--dryrun`:
+```sh
+aws s3 sync ./repository s3://fleet-tuf-repo --delete
+```
+
+### Invalid timestamp.json version
+
+The following issue was solved by resigning the timestamp metadata `fleetctl updates timestamp` (executed three times to increase the version to `4175`)
+```sh
+2022-08-23T13:44:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174"
+2022-08-23T13:59:48-03:00 INF update failed error="update metadata: update metadata: tuf: failed to decode timestamp.json: version 4172 is lower than current version 4174"
+```
diff --git a/tools/tuf/download-artifacts/download-artifacts.go b/tools/tuf/download-artifacts/download-artifacts.go
index e0df2e3dec..2f0d333cff 100644
--- a/tools/tuf/download-artifacts/download-artifacts.go
+++ b/tools/tuf/download-artifacts/download-artifacts.go
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"strings"
+ "time"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
@@ -37,6 +38,7 @@ func orbitCommand() *cli.Command {
outputDirectory string
githubUsername string
githubAPIToken string
+ retry bool
)
return &cli.Command{
Name: "orbit",
@@ -70,13 +72,19 @@ func orbitCommand() *cli.Command {
Destination: &githubAPIToken,
Usage: "Github API token (https://github.com/settings/tokens)",
},
+ &cli.BoolFlag{
+ Name: "retry",
+ EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"},
+ Destination: &retry,
+ Usage: "Whether to retry if the artifact doesn't exist yet",
+ },
},
Action: func(c *cli.Context) error {
return downloadComponents("goreleaser-orbit.yaml", gitTag, map[string]string{
"macos": "orbit-macos",
"linux": "orbit-linux",
"windows": "orbit-windows",
- }, outputDirectory, githubUsername, githubAPIToken)
+ }, outputDirectory, githubUsername, githubAPIToken, retry)
},
}
}
@@ -87,6 +95,7 @@ func desktopCommand() *cli.Command {
outputDirectory string
githubUsername string
githubAPIToken string
+ retry bool
)
return &cli.Command{
Name: "desktop",
@@ -120,13 +129,19 @@ func desktopCommand() *cli.Command {
Destination: &githubAPIToken,
Usage: "Github API token (https://github.com/settings/tokens)",
},
+ &cli.BoolFlag{
+ Name: "retry",
+ EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"},
+ Destination: &retry,
+ Usage: "Whether to retry if the artifact doesn't exist yet",
+ },
},
Action: func(c *cli.Context) error {
return downloadComponents("generate-desktop-targets.yml", gitBranch, map[string]string{
"macos": "desktop.app.tar.gz",
"linux": "desktop.tar.gz",
"windows": "fleet-desktop.exe",
- }, outputDirectory, githubUsername, githubAPIToken)
+ }, outputDirectory, githubUsername, githubAPIToken, retry)
},
}
}
@@ -231,7 +246,7 @@ func extractZipFile(archiveReader *zip.File, destPath string) error {
return nil
}
-func downloadComponents(workflowName string, headBranch string, artifactNames map[string]string, outputDirectory string, githubUsername string, githubAPIToken string) error {
+func downloadComponents(workflowName string, headBranch string, artifactNames map[string]string, outputDirectory string, githubUsername string, githubAPIToken string, retry bool) error {
if err := os.RemoveAll(outputDirectory); err != nil {
return err
}
@@ -241,40 +256,55 @@ func downloadComponents(workflowName string, headBranch string, artifactNames ma
}
}
ctx := context.Background()
- gc := github.NewClient(fleethttp.NewClient())
- workflow, _, err := gc.Actions.GetWorkflowByFileName(ctx, "fleetdm", "fleet", workflowName)
- if err != nil {
- return err
- }
- workflowRuns, _, err := gc.Actions.ListWorkflowRunsByID(ctx, "fleetdm", "fleet", *workflow.ID, nil)
- if err != nil {
- return err
- }
var workflowRun *github.WorkflowRun
- for _, wr := range workflowRuns.WorkflowRuns {
- if headBranch == *wr.HeadBranch {
- workflowRun = wr
+ gc := github.NewClient(fleethttp.NewClient())
+ for {
+ workflow, _, err := gc.Actions.GetWorkflowByFileName(ctx, "fleetdm", "fleet", workflowName)
+ if err != nil {
+ return err
+ }
+ workflowRuns, _, err := gc.Actions.ListWorkflowRunsByID(ctx, "fleetdm", "fleet", *workflow.ID, nil)
+ if err != nil {
+ return err
+ }
+ for _, wr := range workflowRuns.WorkflowRuns {
+ if headBranch == *wr.HeadBranch {
+ workflowRun = wr
+ break
+ }
+ }
+ if workflowRun != nil || !retry {
break
}
+ fmt.Printf("Workflow not available yet, it might be queued, retrying in 60s...\n")
+ time.Sleep(60 * time.Second)
}
if workflowRun == nil {
return fmt.Errorf("workflow with tag %s not found", headBranch)
}
- artifactList, _, err := gc.Actions.ListWorkflowRunArtifacts(ctx, "fleetdm", "fleet", *workflowRun.ID, nil)
- if err != nil {
- return err
- }
- urls := make(map[string]string)
- for _, artifact := range artifactList.Artifacts {
- if *artifact.Name == artifactNames["linux"] {
- urls["linux"] = *artifact.ArchiveDownloadURL
- } else if *artifact.Name == artifactNames["macos"] {
- urls["macos"] = *artifact.ArchiveDownloadURL
- } else if *artifact.Name == artifactNames["windows"] {
- urls["windows"] = *artifact.ArchiveDownloadURL
- } else {
- return fmt.Errorf("unknown artifact name: %s", *artifact.Name)
+ var urls map[string]string
+ for {
+ artifactList, _, err := gc.Actions.ListWorkflowRunArtifacts(ctx, "fleetdm", "fleet", *workflowRun.ID, nil)
+ if err != nil {
+ return err
}
+ urls = make(map[string]string)
+ for _, artifact := range artifactList.Artifacts {
+ if *artifact.Name == artifactNames["linux"] {
+ urls["linux"] = *artifact.ArchiveDownloadURL
+ } else if *artifact.Name == artifactNames["macos"] {
+ urls["macos"] = *artifact.ArchiveDownloadURL
+ } else if *artifact.Name == artifactNames["windows"] {
+ urls["windows"] = *artifact.ArchiveDownloadURL
+ } else {
+ return fmt.Errorf("unknown artifact name: %s", *artifact.Name)
+ }
+ }
+ if len(urls) == 3 || !retry {
+ break
+ }
+ fmt.Printf("All artifacts are not available yet, the workflow might still be running, retrying in 60s...\n")
+ time.Sleep(60 * time.Second)
}
if len(urls) != 3 {
return fmt.Errorf("missing some artifact: %+v", urls)
@@ -295,6 +325,7 @@ func osquerydCommand() *cli.Command {
outputDirectory string
githubUsername string
githubAPIToken string
+ retry bool
)
return &cli.Command{
Name: "osqueryd",
@@ -328,13 +359,19 @@ func osquerydCommand() *cli.Command {
Destination: &githubAPIToken,
Usage: "Github API token (https://github.com/settings/tokens)",
},
+ &cli.BoolFlag{
+ Name: "retry",
+ EnvVars: []string{"DOWNLOAD_ARTIFACTS_RETRY"},
+ Destination: &retry,
+ Usage: "Whether to retry if the artifact doesn't exist yet",
+ },
},
Action: func(c *cli.Context) error {
return downloadComponents("generate-osqueryd-targets.yml", gitBranch, map[string]string{
"macos": "osqueryd.app.tar.gz",
"linux": "osqueryd",
"windows": "osqueryd.exe",
- }, outputDirectory, githubUsername, githubAPIToken)
+ }, outputDirectory, githubUsername, githubAPIToken, retry)
},
}
}
diff --git a/tools/tuf/promote_edge_to_stable.sh b/tools/tuf/promote_edge_to_stable.sh
deleted file mode 100755
index 32829deff1..0000000000
--- a/tools/tuf/promote_edge_to_stable.sh
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/bash
-
-
-component=$1
-version=$2
-
-if [[ -z $component || -z $version ]]; then
- echo "Usage: $0 "
- exit 1
-fi
-
-if [[ ! -d "./repository" ]]; then
- echo "Directory ./repository doesn't exist"
- exit 1
-fi
-
-version_parts=(${version//./ })
-major=${version_parts[0]}
-minor=${version_parts[1]}
-
-echo "Promoting $component from edge to stable, version='$version'"
-echo "Press any key to continue..."
-read -s -n 1
-
-case $1 in
- orbit)
- fleetctl updates add --target ./repository/targets/orbit/macos/edge/orbit --platform macos --name orbit --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/orbit/linux/edge/orbit --platform linux --name orbit --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/orbit/windows/edge/orbit.exe --platform windows --name orbit --version $version -t $major.$minor -t $major -t stable
- ;;
- desktop)
- fleetctl updates add --target ./repository/targets/desktop/macos/edge/desktop.app.tar.gz --platform macos --name desktop --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/desktop/linux/edge/desktop.tar.gz --platform linux --name desktop --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/desktop/windows/edge/fleet-desktop.exe --platform windows --name desktop --version $version -t $major.$minor -t $major -t stable
- ;;
- osqueryd)
- fleetctl updates add --target ./repository/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz --platform macos-app --name osqueryd --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/osqueryd/linux/edge/osqueryd --platform linux --name osqueryd --version $version -t $major.$minor -t $major -t stable
- fleetctl updates add --target ./repository/targets/osqueryd/windows/edge/osqueryd.exe --platform windows --name osqueryd --version $version -t $major.$minor -t $major -t stable
- ;;
- nudge)
- fleetctl updates add --target ./repository/targets/nudge/macos/edge/nudge.app.tar.gz --platform macos --name nudge --version $version -t stable
- ;;
- swiftDialog)
- fleetctl updates add --target ./repository/targets/swiftDialog/macos/edge/swiftDialog.app.tar.gz --platform macos --name swiftDialog --version $version -t stable
- ;;
- *)
- echo Unknown component $1
- exit 1
- ;;
-esac
diff --git a/tools/tuf/releaser.sh b/tools/tuf/releaser.sh
new file mode 100755
index 0000000000..9acf254f18
--- /dev/null
+++ b/tools/tuf/releaser.sh
@@ -0,0 +1,310 @@
+#!/bin/bash
+
+#
+# For usage documentation, see the README.md.
+#
+
+set -e
+
+#
+# Input environment variables:
+#
+# AWS_PROFILE
+# TUF_DIRECTORY
+# COMPONENT
+# ACTION
+# VERSION
+# KEYS_SOURCE_DIRECTORY
+# TARGETS_PASSPHRASE_1PASSWORD_PATH
+# SNAPSHOT_PASSPHRASE_1PASSWORD_PATH
+# TIMESTAMP_PASSPHRASE_1PASSWORD_PATH
+# GITHUB_USERNAME
+# GITHUB_TOKEN_1PASSWORD_PATH
+# SKIP_PR_AND_TAG_PUSH
+#
+
+#
+# Dev environment variables:
+# PUSH_TO_REMOTE
+# GIT_REPOSITORY_DIRECTORY
+#
+
+clean_up () {
+ echo "Cleaning up directories..."
+
+ # Make sure (best effort) to remove the keys after we are done.
+ rm -rf "$KEYS_DIRECTORY"
+ rm -rf "$ARTIFACTS_DOWNLOAD_DIRECTORY"
+ rm -rf "$GO_TOOLS_DIRECTORY"
+ ARG=$?
+ exit $ARG
+}
+
+setup () {
+ echo "Running setup..."
+
+ GO_TOOLS_DIRECTORY=$(mktemp -d)
+ ARTIFACTS_DOWNLOAD_DIRECTORY=$(mktemp -d)
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ REPOSITORY_DIRECTORY=$TUF_DIRECTORY/repository
+ STAGED_DIRECTORY=$TUF_DIRECTORY/staged
+ KEYS_DIRECTORY=$TUF_DIRECTORY/keys
+ if [[ -z $GIT_REPOSITORY_DIRECTORY ]]; then
+ GIT_REPOSITORY_DIRECTORY=$( realpath "$SCRIPT_DIR/../.." )
+ fi
+
+ mkdir -p "$REPOSITORY_DIRECTORY"
+ mkdir -p "$STAGED_DIRECTORY"
+ cp -r "$KEYS_SOURCE_DIRECTORY" "$KEYS_DIRECTORY"
+
+ if ! aws sts get-caller-identity &> /dev/null; then
+ aws sso login
+ prompt "AWS SSO login was successful, press any key to continue..."
+ fi
+
+ # GITHUB_TOKEN is only necessary when releasing to edge.
+ if [[ -n $GITHUB_TOKEN_1PASSWORD_PATH ]]; then
+ GITHUB_TOKEN=$(op read "op://$GITHUB_TOKEN_1PASSWORD_PATH")
+ fi
+
+ # These need to be exported for use by `fleetctl updates` commands.
+ FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH")
+ export FLEET_TARGETS_PASSPHRASE
+ FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH")
+ export FLEET_SNAPSHOT_PASSPHRASE
+ FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH")
+ export FLEET_TIMESTAMP_PASSPHRASE
+
+ go build -o "$GO_TOOLS_DIRECTORY/replace" "$SCRIPT_DIR/../../tools/tuf/replace"
+ go build -o "$GO_TOOLS_DIRECTORY/download-artifacts" "$SCRIPT_DIR/../../tools/tuf/download-artifacts"
+}
+
+pull_from_remote () {
+ echo "Pulling repository from tuf.fleetctl.com... (--dryrun first)"
+ aws s3 sync s3://fleet-tuf-repo "$REPOSITORY_DIRECTORY" --exact-timestamps --dryrun
+ prompt "If the --dryrun looks good, press any key to continue... (no output means nothing to update)"
+ aws s3 sync s3://fleet-tuf-repo "$REPOSITORY_DIRECTORY" --exact-timestamps
+}
+
+promote_component_edge_to_stable () {
+ component_name=$1
+ component_version=$2
+
+ IFS='.' read -r -a version_parts <<< "$component_version"
+ major=${version_parts[0]}
+ minor=${version_parts[1]}
+
+ pushd "$TUF_DIRECTORY"
+ case $component_name in
+ orbit)
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/macos/edge/orbit" --platform macos --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux/edge/orbit" --platform linux --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/windows/edge/orbit.exe" --platform windows --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ ;;
+ desktop)
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/macos/edge/desktop.app.tar.gz" --platform macos --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux/edge/desktop.tar.gz" --platform linux --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/windows/edge/fleet-desktop.exe" --platform windows --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ ;;
+ osqueryd)
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux/edge/osqueryd" --platform linux --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/windows/edge/osqueryd.exe" --platform windows --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable
+ ;;
+ *)
+ echo "Unknown component $component_name"
+ exit 1
+ ;;
+ esac
+ popd
+}
+
+promote_edge_to_stable () {
+ cd "$REPOSITORY_DIRECTORY"
+ if [[ $COMPONENT == "fleetd" ]]; then
+ echo "Promoting fleetd from edge to stable..."
+ promote_component_edge_to_stable orbit "$VERSION"
+ promote_component_edge_to_stable desktop "$VERSION"
+ elif [[ $COMPONENT == "osqueryd" ]]; then
+ echo "Promoting osqueryd from edge to stable..."
+ promote_component_edge_to_stable osqueryd "$VERSION"
+ else
+ echo "Unsupported component: $COMPONENT"
+ exit 1
+ fi
+}
+
+release_fleetd_to_edge () {
+ echo "Releasing fleetd to edge..."
+ BRANCH_NAME="release-fleetd-v$VERSION"
+ ORBIT_TAG="orbit-v$VERSION"
+ if [[ "$SKIP_PR_AND_TAG_PUSH" != "1" ]]; then
+ prompt "A PR for bumping the fleetd version will be created to trigger a Github Action that will build 'Fleet Desktop'. Press any key to continue..."
+ pushd "$GIT_REPOSITORY_DIRECTORY"
+ git checkout -b "$BRANCH_NAME"
+ make changelog-orbit version="$VERSION"
+ ORBIT_CHANGELOG=orbit/CHANGELOG.md
+ "$GO_TOOLS_DIRECTORY/replace" .github/workflows/generate-desktop-targets.yml "FLEET_DESKTOP_VERSION: .+\n" "FLEET_DESKTOP_VERSION: $VERSION\n"
+ git add .github/workflows/generate-desktop-targets.yml "$ORBIT_CHANGELOG"
+ git commit -m "Release fleetd $VERSION"
+ git push origin "$BRANCH_NAME"
+ open "https://github.com/fleetdm/fleet/pull/new/$BRANCH_NAME"
+ prompt "Press any key to continue after the PR is created..."
+ prompt "A 'git tag' will be created to trigger a Github Action to build orbit, press any key to continue..."
+ git tag "$ORBIT_TAG"
+ git push origin "$ORBIT_TAG"
+ popd
+ fi
+ DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/desktop"
+ mkdir -p "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY"
+ "$GO_TOOLS_DIRECTORY/download-artifacts" desktop \
+ --git-branch "$BRANCH_NAME" \
+ --output-directory "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY" \
+ --github-username "$GITHUB_USERNAME" --github-api-token "$GITHUB_TOKEN" \
+ --retry
+ ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/orbit"
+ mkdir -p "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY"
+ "$GO_TOOLS_DIRECTORY/download-artifacts" orbit \
+ --git-tag "$ORBIT_TAG" \
+ --output-directory "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY" \
+ --github-username "$GITHUB_USERNAME" --github-api-token "$GITHUB_TOKEN" \
+ --retry
+ pushd "$TUF_DIRECTORY"
+ fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/macos/orbit" --platform macos --name orbit --version "$VERSION" -t edge
+ fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux/orbit" --platform linux --name orbit --version "$VERSION" -t edge
+ fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/windows/orbit.exe" --platform windows --name orbit --version "$VERSION" -t edge
+ fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/macos/desktop.app.tar.gz" --platform macos --name desktop --version "$VERSION" -t edge
+ fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux/desktop.tar.gz" --platform linux --name desktop --version "$VERSION" -t edge
+ fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/windows/fleet-desktop.exe" --platform windows --name desktop --version "$VERSION" -t edge
+ popd
+}
+
+release_osqueryd_to_edge () {
+ echo "Releasing osqueryd to edge..."
+ prompt "A branch and PR for bumping the osquery version will be created. Press any key to continue..."
+ BRANCH_NAME=release-osqueryd-v$VERSION
+ if [[ "$SKIP_PR_AND_TAG_PUSH" != "1" ]]; then
+ pushd "$GIT_REPOSITORY_DIRECTORY"
+ git checkout -b "$BRANCH_NAME"
+ "$GO_TOOLS_DIRECTORY/replace" .github/workflows/generate-osqueryd-targets.yml "OSQUERY_VERSION: .+\n" "OSQUERY_VERSION: $VERSION\n"
+ git add .github/workflows/generate-osqueryd-targets.yml
+ git commit -m "Bump osqueryd version to $VERSION"
+ git push origin "$BRANCH_NAME"
+ open "https://github.com/fleetdm/fleet/pull/new/$BRANCH_NAME"
+ prompt "Press any key to continue after the PR is created..."
+ popd
+ fi
+ OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY="$ARTIFACTS_DOWNLOAD_DIRECTORY/osqueryd"
+ mkdir -p "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY"
+ "$GO_TOOLS_DIRECTORY/download-artifacts" osqueryd \
+ --git-branch "$BRANCH_NAME" \
+ --output-directory "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY" \
+ --github-username "$GITHUB_USERNAME" \
+ --github-api-token "$GITHUB_TOKEN" \
+ --retry
+ pushd "$TUF_DIRECTORY"
+ fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/macos/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$VERSION" -t edge
+ fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux/osqueryd" --platform linux --name osqueryd --version "$VERSION" -t edge
+ fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/windows/osqueryd.exe" --platform windows --name osqueryd --version "$VERSION" -t edge
+ popd
+}
+
+release_to_edge () {
+ if [[ $COMPONENT == "fleetd" ]]; then
+ release_fleetd_to_edge
+ elif [[ $COMPONENT == "osqueryd" ]]; then
+ release_osqueryd_to_edge
+ else
+ echo "Unsupported component: $COMPONENT"
+ exit 1
+ fi
+}
+
+push_to_remote () {
+ echo "Running --dryrun push of repository to tuf.fleetctl.com..."
+ aws s3 sync "$REPOSITORY_DIRECTORY" s3://fleet-tuf-repo --dryrun
+ if [[ $PUSH_TO_REMOTE == "1" ]]; then
+ echo "WARNING: This step will push the release to tuf.fleetctl.com (production)..."
+ prompt "If the --dryrun looks good, press any key to continue..."
+ aws s3 sync "$REPOSITORY_DIRECTORY" s3://fleet-tuf-repo
+ echo "Release has been pushed!"
+ echo "NOTE: You might see some clients failing to upgrade due to some sha256 mismatches."
+ echo "These temporary failures are expected because it takes some time for caches to be invalidated (these errors should go away after ~15-30 minutes)."
+ else
+ echo "PUSH_TO_REMOTE not set to 1, so not pushing."
+ fi
+}
+
+prompt () {
+ printf "%s\n" "$1"
+ read -r -s -n 1
+}
+
+setup_to_become_publisher () {
+ echo "Running setup to become publisher..."
+
+ REPOSITORY_DIRECTORY=$TUF_DIRECTORY/repository
+ STAGED_DIRECTORY=$TUF_DIRECTORY/staged
+ KEYS_DIRECTORY=$TUF_DIRECTORY/keys
+ mkdir -p "$REPOSITORY_DIRECTORY"
+ mkdir -p "$STAGED_DIRECTORY"
+ mkdir -p "$KEYS_DIRECTORY"
+ if ! aws sts get-caller-identity &> /dev/null; then
+ aws sso login
+ prompt "AWS SSO login was successful, press any key to continue..."
+ fi
+ # These need to be exported for use by `tuf` commands.
+ FLEET_TARGETS_PASSPHRASE=$(op read "op://$TARGETS_PASSPHRASE_1PASSWORD_PATH")
+ export TUF_TARGETS_PASSPHRASE=$FLEET_TARGETS_PASSPHRASE
+ FLEET_SNAPSHOT_PASSPHRASE=$(op read "op://$SNAPSHOT_PASSPHRASE_1PASSWORD_PATH")
+ export TUF_SNAPSHOT_PASSPHRASE=$FLEET_SNAPSHOT_PASSPHRASE
+ FLEET_TIMESTAMP_PASSPHRASE=$(op read "op://$TIMESTAMP_PASSPHRASE_1PASSWORD_PATH")
+ export TUF_TIMESTAMP_PASSPHRASE=$FLEET_TIMESTAMP_PASSPHRASE
+}
+
+if [[ $ACTION == "generate-signing-keys" ]]; then
+ setup_to_become_publisher
+ pull_from_remote
+ cd "$TUF_DIRECTORY"
+ tuf gen-key targets && echo
+ tuf gen-key snapshot && echo
+ tuf gen-key timestamp && echo
+ echo "Keys have been generated, now do the following actions:"
+ echo "- Share '$TUF_DIRECTORY/staged/root.json' with Fleet member with the 'root' role, who will sign with its root key and push it to the remote repository."
+ echo "- Store the '$TUF_DIRECTORY/keys' folder (that contains the encrypted keys) on a USB flash drive that you will ONLY use for releasing fleetd updates."
+ exit 0
+fi
+
+print_reminder () {
+ if [[ $ACTION == "release-to-edge" ]]; then
+ if [[ $COMPONENT == "fleetd" ]]; then
+ prompt "Make sure to install fleetd with '--orbit-channel=edge --desktop-channel=edge' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..."
+ elif [[ $COMPONENT == "osqueryd" ]]; then
+ prompt "Make sure to install fleetd with '--osqueryd-channel=edge' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..."
+ fi
+ elif [[ $ACTION == "promote-edge-to-stable" ]]; then
+ if [[ $COMPONENT == "fleetd" ]]; then
+ prompt "Make sure to install fleetd with '--orbit-channel=stable --desktop-channel=stable' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..."
+ elif [[ $COMPONENT == "osqueryd" ]]; then
+ prompt "Make sure to install fleetd with '--osqueryd-channel=stable' on a Linux, Windows and macOS VM. (To smoke test the release.) Press any key to continue..."
+ fi
+ else
+ echo "Unsupported action: $ACTION"
+ fi
+}
+
+trap clean_up EXIT
+print_reminder
+setup
+pull_from_remote
+
+if [[ $ACTION == "release-to-edge" ]]; then
+ release_to_edge
+elif [[ $ACTION == "promote-edge-to-stable" ]]; then
+ promote_edge_to_stable
+else
+ echo "Unsupported action: $ACTION"
+ exit 1
+fi
+
+push_to_remote
\ No newline at end of file
diff --git a/tools/tuf/replace/main.go b/tools/tuf/replace/main.go
new file mode 100644
index 0000000000..ba742f09e7
--- /dev/null
+++ b/tools/tuf/replace/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "os"
+ "regexp"
+ "strings"
+)
+
+// This tool was created to prevent issues between GNU's sed and OSX's sed.
+
+func main() {
+ inputPath := os.Args[1]
+ expression := os.Args[2]
+ replace := os.Args[3]
+ r := regexp.MustCompile(expression)
+ stat, err := os.Stat(inputPath)
+ if err != nil {
+ panic(err)
+ }
+ input, err := os.ReadFile(inputPath)
+ if err != nil {
+ panic(err)
+ }
+ if strings.HasSuffix(replace, `\n`) {
+ replace = strings.TrimSuffix(replace, `\n`) + "\n"
+ }
+ output := r.ReplaceAllString(string(input), replace)
+ if err := os.WriteFile(inputPath, []byte(output), stat.Mode()); err != nil {
+ panic(err)
+ }
+}
diff --git a/tools/tuf/test/README.md b/tools/tuf/test/README.md
index dacac03e75..6bf057e602 100644
--- a/tools/tuf/test/README.md
+++ b/tools/tuf/test/README.md
@@ -9,6 +9,7 @@ Scripts in this directory aim to ease the testing of Orbit and the [TUF](https:/
1. The script is executed on a macOS host.
2. Fleet server also running on the same macOS host.
3. All VMs (and the macOS host itself) are configured to resolve `host.docker.internal` to the macOS host IP (by modifying their `hosts` file).
+4. The hosts are running on the same GOARCH as the macOS host. If not, you can set the `GOARCH` environment variable to compile for the desired architecture. For example: `GOARCH=amd64`
> PS: We use `host.docker.internal` because the testing certificate `./tools/osquery/fleet.crt`
> has such hostname (and `localhost`) defined as SANs.
diff --git a/tools/tuf/test/create_repository.sh b/tools/tuf/test/create_repository.sh
index 8772db5b37..9131852d5b 100755
--- a/tools/tuf/test/create_repository.sh
+++ b/tools/tuf/test/create_repository.sh
@@ -82,7 +82,7 @@ for system in $SYSTEMS; do
ORBIT_BINARY_PATH=$orbit_target \
go run ./orbit/tools/build/build.go
else
- GOOS=$goose_value GOARCH=$goarch_value go build -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=42" -o $orbit_target ./orbit/cmd/orbit
+ CGO_ENABLED=0 GOOS=$goose_value GOARCH=$goarch_value go build -ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=42" -o $orbit_target ./orbit/cmd/orbit
fi
./build/fleetctl updates add \
diff --git a/website/api/controllers/save-questionnaire-progress.js b/website/api/controllers/save-questionnaire-progress.js
index 8ef697de53..3f53545906 100644
--- a/website/api/controllers/save-questionnaire-progress.js
+++ b/website/api/controllers/save-questionnaire-progress.js
@@ -59,6 +59,22 @@ module.exports = {
await User.updateOne({id: this.req.me.id}).set({
primaryBuyingSituation
});
+ // Send a POST request to Zapier
+ await sails.helpers.http.post.with({
+ url: 'https://hooks.zapier.com/hooks/catch/3627242/3pl7yt1/',
+ data: {
+ primaryBuyingSituation,
+ emailAddress: this.req.me.emailAddress,
+ webhookSecret: sails.config.custom.zapierSandboxWebhookSecret,
+ }
+ })
+ .timeout(5000)
+ .tolerate(['non200Response', 'requestFailed'], (err)=>{
+ // Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
+ sails.log.warn(`When a user completed the 'What are you using Fleet for' questionnaire step, a lead/contact could not be updated in the CRM for this email address: ${this.req.me.emailAddress}. Raw error: ${err}`);
+ return;
+ });
+
// Set the primary buying situation in the user's session.
this.req.session.primaryBuyingSituation = primaryBuyingSituation;
}
diff --git a/website/api/controllers/webhooks/receive-from-github.js b/website/api/controllers/webhooks/receive-from-github.js
index 375e685b0e..fff9b3155a 100644
--- a/website/api/controllers/webhooks/receive-from-github.js
+++ b/website/api/controllers/webhooks/receive-from-github.js
@@ -89,6 +89,7 @@ module.exports = {
'nonpunctual',
'hughestaylor',
'dantecatalfamo',
+ 'unearthlyglow',
];
let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05)
diff --git a/website/api/hooks/custom/index.js b/website/api/hooks/custom/index.js
index b4998f3a23..20a7884014 100644
--- a/website/api/hooks/custom/index.js
+++ b/website/api/hooks/custom/index.js
@@ -145,23 +145,18 @@ will be disabled and/or hidden in the UI.
res.locals.me = undefined;
}//fi
- // Check for UTM parameters for website personalization.
+ // Check for website personalization parameter, and if valid, absorb it in the session.
+ // (This makes the experience simpler and less confusing for people, prioritizing showing things that matter for them)
// [?] https://en.wikipedia.org/wiki/UTM_parameters
// e.g.
- // https://fleetdm.com/device-management?utm_source=linkedin&utm_campaign=evergreen+leadgen&utm_content=mdm
- if (['eo-security', 'eo-it', 'mdm', 'vm'].includes(req.param('utm_content'))) {
- // If this is set to something weird, then we silently ignore it.
- // Modify the active session instance. (This will be persisted when the response is sent.)
- req.session.primaryBuyingSituation = req.param('utm_content');
- // FUTURE: Auto-redirect without the querystring after absorbtion to make it prettier in the URL bar.
- // (except this probably messes up analytics so before doing that, figure out how to solve that problem)
+ // https://fleetdm.com/device-management?utm_content=mdm
+ if (['clear','eo-security', 'eo-it', 'mdm', 'vm'].includes(req.param('utm_content'))) {
+ req.session.primaryBuyingSituation = req.param('utm_content') === 'clear' ? undefined : req.param('utm_content');
+ return res.redirect(req.path);// « auto-redirect without querystring to make it prettier in the URL bar.
}//fi
- if(req.param('utm_content') === 'clear'){
- req.session.primaryBuyingSituation = undefined;
- }
+
if (req.method === 'GET' || req.method === 'HEAD') {
- // Include information about the primary buying situation
- // If set in the session (e.g. from an ad), use the primary buying situation for personalization.
+ // Include information about the primary buying situation for use in the HTML layout, views, and page scripts.
res.locals.primaryBuyingSituation = req.session.primaryBuyingSituation || undefined;
}//fi
diff --git a/website/assets/js/components/parallax-city.component.js b/website/assets/js/components/parallax-city.component.js
index 6f3de558ef..423aa7d735 100644
--- a/website/assets/js/components/parallax-city.component.js
+++ b/website/assets/js/components/parallax-city.component.js
@@ -61,14 +61,14 @@ parasails.registerComponent('parallaxCity', {
}
// Check for hardware/graphics acceleration.
if(bowser.chrome || bowser.opera) {
- this.enableAnimation = this.isHardwareAccelerationEnabledOnChromiumBrowsers();
+ this.enableAnimation = this._isHardwareAccelerationEnabledOnChromiumBrowsers();
} else if(bowser.firefox){
- this.enableAnimation = this.isHardwareAccelerationEnabledOnFirefox();
+ this.enableAnimation = this._isHardwareAccelerationEnabledOnFirefox();
}
},
mounted: async function(){
- if(!this.enableAnimation) {
- this.setupParallaxAnimation();
+ if(this.enableAnimation) {
+ this._setupParallaxAnimation();
}
},
beforeDestroy: function() {
@@ -79,13 +79,13 @@ parasails.registerComponent('parallaxCity', {
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
- getElementPositions: function() {
+ _getElementPositions: function() {
this.elementHeight = this.parallaxCityElement.clientHeight;
this.distanceFromTopOfPage = this.parallaxCityElement.offsetTop;
this.distanceFromBottomOfPage = document.body.scrollHeight - this.distanceFromTopOfPage - (this.elementHeight * .5);
this.elementBottomPosition = this.elementHeight + this.distanceFromTopOfPage;
},
- setupParallaxAnimation: function() {
+ _setupParallaxAnimation: function() {
// Store a reference to the parent container, we'll use this to determine the elements position relative to the user's viewport.
this.parallaxCityElement = $('div[purpose="parallax-city-container"]')[0];
// Build an array of parallax layers, and set the initial bottom position of each layer to be negative the layer's scroll amount.
@@ -95,15 +95,15 @@ parasails.registerComponent('parallaxCity', {
this.parallaxLayers.push({element: layer, scrollAmount});
}
// Determine the parallax image's position on the page/user's viewport.
- this.getElementPositions();
+ this._getElementPositions();
// If the bottom of the element is within the user's viewport, update the positions of the layers.
if(this.parallaxCityElement.getBoundingClientRect().bottom > this.parallaxCityElement.offsetTop) {
this.scrollParallaxLayers();
}
// Add a scroll event listener
- $(window).scroll(this.throttleParallaxScroll);
+ $(window).scroll(this._throttleParallaxScroll);
// Add a resize event listener.
- $(window).resize(this.getElementPositions);
+ $(window).resize(this._getElementPositions);
},
scrollParallaxLayers: function() {
if(!this.parallaxLayersAreCurrentlyAnimating) {
@@ -122,13 +122,13 @@ parasails.registerComponent('parallaxCity', {
}
}
},
- throttleParallaxScroll: function() {
+ _throttleParallaxScroll: function() {
this.scrollParallaxLayers();
setTimeout(()=>{
this.parallaxLayersAreCurrentlyAnimating = false;
}, 100);
},
- isHardwareAccelerationEnabledOnChromiumBrowsers: function() {
+ _isHardwareAccelerationEnabledOnChromiumBrowsers: function() {
let isHardwareAccelerationEnabled = true;
// For Chromium based browsers, we'll check the vendor of the user's graphics card.
// See https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8 for more info about this method.
@@ -148,7 +148,7 @@ parasails.registerComponent('parallaxCity', {
}
return isHardwareAccelerationEnabled;
},
- isHardwareAccelerationEnabledOnFirefox: function() {
+ _isHardwareAccelerationEnabledOnFirefox: function() {
// For Firefox, the method we use for chrome does not always work.
// Instead, we'll run two tests, one with forced software rendering, and one without to see if the results are the same.
// See https://stackoverflow.com/a/77170999 for more info about this method.
diff --git a/website/assets/js/pages/contact.page.js b/website/assets/js/pages/contact.page.js
index 2b629aa352..541e375f7b 100644
--- a/website/assets/js/pages/contact.page.js
+++ b/website/assets/js/pages/contact.page.js
@@ -48,7 +48,11 @@ parasails.registerPage('contact', {
if(this.formToShow === 'contact'){
this.formToDisplay = this.formToShow;
}
- if(this.prefillFormDataFromUserRecord){
+ if(this.primaryBuyingSituation){ // If the user has a priamry buying situation set in their sesssion, pre-fill the form.
+ // Note: this will be overriden if the user is logged in and has a primaryBuyingSituation set in the database.
+ this.formData.primaryBuyingSituation = this.primaryBuyingSituation;
+ }
+ if(this.prefillFormDataFromUserRecord){// prefill from database
this.formDataToPrefillForLoggedInUsers.emailAddress = this.me.emailAddress;
this.formDataToPrefillForLoggedInUsers.firstName = this.me.firstName;
this.formDataToPrefillForLoggedInUsers.lastName = this.me.lastName;
@@ -59,12 +63,9 @@ parasails.registerPage('contact', {
}
this.formData = _.clone(this.formDataToPrefillForLoggedInUsers);
}
- if(window.location.search){
+ if(window.location.search){// auto-clear query string (TODO: Document why we're doing this further. I think this shouldn't exist in the frontend code, instead in the hook. Because analytics corruption.)
window.history.replaceState({}, document.title, '/contact' );
}
- if(this.primaryBuyingSituation){
- this.formData.primaryBuyingSituation = this.primaryBuyingSituation;// prefill form
- }
},
mounted: async function() {
//…
diff --git a/website/assets/js/pages/device-management.page.js b/website/assets/js/pages/device-management.page.js
index 76633cef05..be249f1a8f 100644
--- a/website/assets/js/pages/device-management.page.js
+++ b/website/assets/js/pages/device-management.page.js
@@ -1,4 +1,4 @@
-parasails.registerPage('device-management', {
+parasails.registerPage('device-management-page', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
diff --git a/website/assets/js/pages/endpoint-ops.page.js b/website/assets/js/pages/endpoint-ops.page.js
index 2a97714019..f79d51d830 100644
--- a/website/assets/js/pages/endpoint-ops.page.js
+++ b/website/assets/js/pages/endpoint-ops.page.js
@@ -1,4 +1,4 @@
-parasails.registerPage('endpoint-ops', {
+parasails.registerPage('endpoint-ops-page', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
diff --git a/website/assets/js/pages/start.page.js b/website/assets/js/pages/start.page.js
index 9b7173da0b..47dfe0bc98 100644
--- a/website/assets/js/pages/start.page.js
+++ b/website/assets/js/pages/start.page.js
@@ -75,7 +75,7 @@ parasails.registerPage('start', {
}
// If this user has not completed the 'what are you using fleet for' step, and has a primaryBuyingSituation set by an ad. prefill the formData for this step.
if(this.primaryBuyingSituation && _.isEmpty(this.formData['what-are-you-using-fleet-for'])){
- this.formData['what-are-you-using-fleet-for'].primaryBuyingSituation = _.clone(this.primaryBuyingSituation);
+ this.formData['what-are-you-using-fleet-for'] = {primaryBuyingSituation: this.primaryBuyingSituation};
}
},
mounted: async function() {
diff --git a/website/assets/js/pages/vulnerability-management.page.js b/website/assets/js/pages/vulnerability-management.page.js
index a4e9cf678a..29c8bfc12f 100644
--- a/website/assets/js/pages/vulnerability-management.page.js
+++ b/website/assets/js/pages/vulnerability-management.page.js
@@ -1,4 +1,4 @@
-parasails.registerPage('vulnerability-management', {
+parasails.registerPage('vulnerability-management-page', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
diff --git a/website/assets/styles/components/logo-carousel.component.less b/website/assets/styles/components/logo-carousel.component.less
index 2e057bfc05..b16e8ace53 100644
--- a/website/assets/styles/components/logo-carousel.component.less
+++ b/website/assets/styles/components/logo-carousel.component.less
@@ -57,10 +57,10 @@
}
@keyframes scroll-horizontal {
0% {
- transform: translateX(50%);
+ transform: translateX(25%);
}
100% {
- transform: translateX(-50%);
+ transform: translateX(-75%);
}
}
}
diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less
index 210cc4914b..a03435ff6c 100644
--- a/website/assets/styles/layout.less
+++ b/website/assets/styles/layout.less
@@ -74,6 +74,7 @@ html, body {
position: relative;
font-size: 14px;
font-weight: 700;
+ line-height: 1;
padding: 16px;
box-shadow: 1px 1px 2px rgba(25, 33, 71, 0.24);
border-radius: 4px;
@@ -231,6 +232,7 @@ html, body {
position: relative;
font-size: 14px;
font-weight: 700;
+ line-height: 1;
padding: 16px;
box-shadow: 1px 1px 2px rgba(25, 33, 71, 0.24);
border-radius: 4px;
diff --git a/website/assets/styles/pages/device-management.less b/website/assets/styles/pages/device-management.less
index 6f85892181..d3a9cb6339 100644
--- a/website/assets/styles/pages/device-management.less
+++ b/website/assets/styles/pages/device-management.less
@@ -1,4 +1,4 @@
-#device-management {
+#device-management-page {
@heading-lineheight: 120%;
@text-lineheight: 150%;
diff --git a/website/assets/styles/pages/endpoint-ops.less b/website/assets/styles/pages/endpoint-ops.less
index c39657dad9..aa92b9d1f2 100644
--- a/website/assets/styles/pages/endpoint-ops.less
+++ b/website/assets/styles/pages/endpoint-ops.less
@@ -1,4 +1,4 @@
-#endpoint-ops {
+#endpoint-ops-page {
@heading-line-height: 120%;
@text-line-height: 150%;
@@ -636,6 +636,9 @@
}
}
@media (max-width: 472px) {
+ h1 {
+ font-size: 36px;
+ }
[purpose='testimonial-videos'] {
flex-direction: column;
}
diff --git a/website/assets/styles/pages/entrance/login.less b/website/assets/styles/pages/entrance/login.less
index f49be521f6..42cadfb9be 100644
--- a/website/assets/styles/pages/entrance/login.less
+++ b/website/assets/styles/pages/entrance/login.less
@@ -62,7 +62,6 @@
border-radius: 8px;
padding-top: 16px;
padding-bottom: 16px;
- height: 48px;
display: flex;
align-items: center;
span {
@@ -70,7 +69,7 @@
margin-left: auto;
margin-right: auto;
font-size: 16px;
- line-height: 20px;
+ line-height: 16px;
text-align: center;
font-weight: 700;
}
diff --git a/website/assets/styles/pages/entrance/signup.less b/website/assets/styles/pages/entrance/signup.less
index 6c9a3b2c78..058d58681c 100644
--- a/website/assets/styles/pages/entrance/signup.less
+++ b/website/assets/styles/pages/entrance/signup.less
@@ -66,7 +66,6 @@
border-radius: 8px;
padding-top: 16px;
padding-bottom: 16px;
- height: 48px;
display: flex;
align-items: center;
span {
@@ -74,7 +73,7 @@
margin-left: auto;
margin-right: auto;
font-size: 16px;
- line-height: 20px;
+ line-height: 16px;
text-align: center;
font-weight: 700;
}
diff --git a/website/assets/styles/pages/handbook/basic-handbook.less b/website/assets/styles/pages/handbook/basic-handbook.less
index 0cd2959a2d..941231abb6 100644
--- a/website/assets/styles/pages/handbook/basic-handbook.less
+++ b/website/assets/styles/pages/handbook/basic-handbook.less
@@ -402,12 +402,9 @@
padding: 16px;
border-radius: 8px;
display: flex;
- div.d-block {
- margin-left: 12px;
- }
img {
display: flex;
- margin-top: 4px;
+ margin: 4px 12px 0 0;
height: 16px;
width: 16px;
padding: 0px;
diff --git a/website/assets/styles/pages/vulnerability-management.less b/website/assets/styles/pages/vulnerability-management.less
index c0757a512e..e38708df49 100644
--- a/website/assets/styles/pages/vulnerability-management.less
+++ b/website/assets/styles/pages/vulnerability-management.less
@@ -1,4 +1,4 @@
-#vulnerability-management {
+#vulnerability-management-page {
background: linear-gradient(180deg, #E8F1F6 0%, #FFF 8.76%);
h1 {
font-size: 56px;
diff --git a/website/config/routes.js b/website/config/routes.js
index d1d490ffc7..daa4c7b4bc 100644
--- a/website/config/routes.js
+++ b/website/config/routes.js
@@ -248,6 +248,7 @@ module.exports.routes = {
locals: {
pageTitleForMeta: 'Endpoint ops | Fleet',
pageDescriptionForMeta: 'Pulse check anything, build reports, and ship data to any platform with Fleet.',
+ currentSection: 'platform',
}
},
@@ -256,6 +257,7 @@ module.exports.routes = {
locals: {
pageTitleForMeta: 'Vulnerability management | Fleet',
pageDescriptionForMeta: 'Report CVEs, software inventory, security posture, and other risks down to the chipset of any endpoint with Fleet.',
+ currentSection: 'platform',
}
},
@@ -462,8 +464,14 @@ module.exports.routes = {
'GET /try-fleet/sandbox-expired': '/try-fleet',
'GET /try-fleet/sandbox': '/try-fleet',
'GET /try-fleet/waitlist': '/try-fleet',
- 'GET /mdm': '/device-management',// « alias for radio ad
'GET /endpoint-operations': '/endpoint-ops',// « just in case we type it the wrong way
+ 'GET /example-dep-profile': 'https://github.com/fleetdm/fleet/blob/main/it-and-security/lib/automatic-enrollment.dep.json',
+
+ // Shortlinks for texting friends, radio ads, etc
+ 'GET /mdm': '/device-management?utm_content=mdm',// « alias for radio ad
+ 'GET /it': '/endpoint-ops?utm_content=eo-it',
+ 'GET /seceng': '/endpoint-ops?utm_content=eo-security',
+ 'GET /vm': '/vulnerability-management?utm_content=vm',
// Fleet UI
// =============================================================================================================
@@ -482,6 +490,8 @@ module.exports.routes = {
'GET /learn-more-about/google-workspace-domains': 'https://admin.google.com/ac/domains/manage',
'GET /learn-more-about/domain-wide-delegation': 'https://admin.google.com/ac/owl/domainwidedelegation',
'GET /learn-more-about/enabling-calendar-api': 'https://console.cloud.google.com/apis/library/calendar-json.googleapis.com',
+ 'GET /learn-more-about/downgrading': '/docs/using-fleet/downgrading-fleet',
+ 'GET /learn-more-about/fleetd': '/docs/get-started/anatomy#fleetd',
// Sitemap
// =============================================================================================================
diff --git a/website/package.json b/website/package.json
index e050e3271f..987acade2e 100644
--- a/website/package.json
+++ b/website/package.json
@@ -6,16 +6,16 @@
"keywords": [],
"dependencies": {
"@sailshq/connect-redis": "^6.1.3",
- "@sailshq/lodash": "^3.10.3",
+ "@sailshq/lodash": "^3.10.5",
"@sailshq/socket.io-redis": "^6.1.2",
- "jsonwebtoken": "9.0.0",
+ "jsonwebtoken": "9.0.2",
"moment": "2.29.4",
- "sails": "^1.5.7",
- "sails-hook-apianalytics": "^2.0.5",
+ "sails": "^1.5.10",
+ "sails-hook-apianalytics": "^2.0.6",
"sails-hook-organics": "^2.2.2",
- "sails-hook-orm": "^4.0.2",
+ "sails-hook-orm": "^4.0.3",
"sails-hook-sockets": "^3.0.0",
- "sails-postgresql": "^5.0.0"
+ "sails-postgresql": "^5.0.1"
},
"devDependencies": {
"eslint": "5.16.0",
@@ -23,7 +23,7 @@
"htmlhint": "0.11.0",
"lesshint": "6.3.6",
"marked": "4.0.10",
- "sails-hook-grunt": "^4.0.0",
+ "sails-hook-grunt": "^5.0.0",
"yaml": "1.10.2"
},
"scripts": {
diff --git a/website/scripts/create-issues-for-todays-rituals.js b/website/scripts/create-issues-for-todays-rituals.js
index 9faf00bca1..5e1303c736 100644
--- a/website/scripts/create-issues-for-todays-rituals.js
+++ b/website/scripts/create-issues-for-todays-rituals.js
@@ -64,19 +64,14 @@ module.exports = {
let nextIssueShouldBeCreatedAt = ritualStartedAt + ((Math.floor(howManyRitualsCycles) + 1) * ritualsFrequencyInMs);
// Get the amount of this ritual's cycle remaining.
let amountOfCycleRemainingTillNextRitual = (Math.floor(howManyRitualsCycles) - howManyRitualsCycles) + 1;
- // If amountOfCycleRemainingTillNextRitual is 0, then it is time to create a new issue for this ritual (Note: This will probably never happen)
- if(amountOfCycleRemainingTillNextRitual === 0 || amountOfCycleRemainingTillNextRitual === -0){
+ // Get the number of milliseconds until the next issue for this ritual will be created.
+ let timeToNextRitualInMs = amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs;
+ if(_.startsWith(ritual.frequency, 'Daily')) {// Using _.startsWith() to handle frequencies with emoji ("Daily ⏰") and with out ("Daily")
+ // Since this script runs once a day, we'll always create issues for daily rituals.
+ isItTimeToCreateANewIssue = true;
+ } else if(timeToNextRitualInMs === ritualsFrequencyInMs) {
+ // For any other frequency, we'll check to see if the calculated timeToNextRitualInMs is the same as the rituals frequency.
isItTimeToCreateANewIssue = true;
- } else {
- // Otherwise, get the number of milliseconds until the next issue for this ritual will be created.
- let timeToNextRitualInMs = amountOfCycleRemainingTillNextRitual * ritualsFrequencyInMs;
- // Since this script runs once a day at the same time, we'll create issues we'll create issues for
- if(_.startsWith(ritual.frequency, 'Daily')) {// Using _.startsWith() to handle frequencies with emoji ("Daily ⏰") and with out ("Daily")
- isItTimeToCreateANewIssue = true;
- } else if(timeToNextRitualInMs < 86400000) {
- // If the next occurance of this ritual is in less than 24 hours (before this script runs again), we'll create an issue for it.
- isItTimeToCreateANewIssue = true;
- }
}
// Skip to the next ritual if it isn't time yet.
if (!isItTimeToCreateANewIssue) {
diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs
index dd6720d902..9825336ecc 100644
--- a/website/views/layouts/layout.ejs
+++ b/website/views/layouts/layout.ejs
@@ -102,14 +102,6 @@
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o
- <% /* Snitcher analytics code */ %>
-
<% /* HubSpot Embed Code */ %>
<% }
@@ -179,7 +171,7 @@