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

This commit is contained in:
Martin Angers 2024-04-02 09:21:51 -04:00 committed by GitHub
commit 0f55bf241d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 2176 additions and 1266 deletions

1
.yarnrc Normal file
View file

@ -0,0 +1 @@
save-prefix ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View file

@ -0,0 +1 @@
- Query report is reset when there is a change to the selected platform or selected minimum osquery version

View file

@ -0,0 +1 @@
- `fleetctl gitops` now batch processes queries and policies

View file

@ -0,0 +1 @@
* Fix a small alignment bug

View file

@ -0,0 +1 @@
- Added cross-platform check for duplicate MDM profiles names in batch set MDM profiles API.

View file

@ -0,0 +1 @@
- Fix UI's ability to bulk delete hosts when "All teams" is selected

View file

@ -0,0 +1 @@
- UI fix: styling of live query disabled warning

View file

@ -17,6 +17,8 @@ import (
"github.com/go-kit/log/level"
)
const calendarConsumers = 18
func newCalendarSchedule(
ctx context.Context,
instanceID string,
@ -200,11 +202,10 @@ func processCalendarFailingHosts(
) {
hosts = filterHostsWithSameEmail(hosts)
const consumers = 20
hostsCh := make(chan fleet.HostPolicyMembershipData)
var wg sync.WaitGroup
for i := 0; i < consumers; i++ {
for i := 0; i < calendarConsumers; i++ {
wg.Add(+1)
go func() {
defer wg.Done()
@ -500,11 +501,10 @@ func removeCalendarEventsFromPassingHosts(
})
}
const consumers = 20
emailsCh := make(chan emailWithHosts)
var wg sync.WaitGroup
for i := 0; i < consumers; i++ {
for i := 0; i < calendarConsumers; i++ {
wg.Add(+1)
go func() {
defer wg.Done()
@ -601,15 +601,16 @@ func cronCalendarEventsCleanup(ctx context.Context, ds fleet.Datastore, logger k
}
var userCalendar fleet.UserCalendar
var calendarConfig *fleet.GoogleCalendarIntegration
if len(appConfig.Integrations.GoogleCalendar) > 0 {
googleCalendarIntegrationConfig := appConfig.Integrations.GoogleCalendar[0]
userCalendar = createUserCalendarFromConfig(ctx, googleCalendarIntegrationConfig, logger)
calendarConfig = appConfig.Integrations.GoogleCalendar[0]
userCalendar = createUserCalendarFromConfig(ctx, calendarConfig, logger)
}
// If global setting is disabled, we remove all calendar events from the DB
// (we cannot delete the events from the user calendar because there's no configuration anymore).
if userCalendar == nil {
if err := deleteAllCalendarEvents(ctx, ds, nil, nil); err != nil {
if err := deleteAllCalendarEvents(ctx, ds, nil, nil, logger); err != nil {
return fmt.Errorf("delete all calendar events: %w", err)
}
// We've deleted all calendar events, nothing else to do.
@ -630,7 +631,7 @@ func cronCalendarEventsCleanup(ctx context.Context, ds fleet.Datastore, logger k
}
for _, team := range teams {
if err := cleanupTeamCalendarEvents(ctx, ds, userCalendar, *team); err != nil {
if err := cleanupTeamCalendarEvents(ctx, ds, calendarConfig, *team, logger); err != nil {
level.Info(logger).Log("msg", "delete team calendar events", "team_id", team.ID, "err", err)
}
}
@ -644,38 +645,59 @@ func cronCalendarEventsCleanup(ctx context.Context, ds fleet.Datastore, logger k
if err != nil {
return fmt.Errorf("list out of date calendar events: %w", err)
}
for _, outOfDateCalendarEvent := range outOfDateCalendarEvents {
if err := deleteCalendarEvent(ctx, ds, userCalendar, outOfDateCalendarEvent); err != nil {
return fmt.Errorf("delete user calendar event: %w", err)
}
}
deleteCalendarEventsInParallel(ctx, ds, calendarConfig, outOfDateCalendarEvents, logger)
return nil
}
func deleteAllCalendarEvents(
ctx context.Context,
ds fleet.Datastore,
userCalendar fleet.UserCalendar,
calendarConfig *fleet.GoogleCalendarIntegration,
teamID *uint,
logger kitlog.Logger,
) error {
calendarEvents, err := ds.ListCalendarEvents(ctx, teamID)
if err != nil {
return fmt.Errorf("list calendar events: %w", err)
}
for _, calendarEvent := range calendarEvents {
if err := deleteCalendarEvent(ctx, ds, userCalendar, calendarEvent); err != nil {
return fmt.Errorf("delete user calendar event: %w", err)
}
}
deleteCalendarEventsInParallel(ctx, ds, calendarConfig, calendarEvents, logger)
return nil
}
func deleteCalendarEventsInParallel(
ctx context.Context, ds fleet.Datastore, calendarConfig *fleet.GoogleCalendarIntegration, calendarEvents []*fleet.CalendarEvent,
logger kitlog.Logger,
) {
if len(calendarEvents) > 0 {
calendarEventCh := make(chan *fleet.CalendarEvent)
var wg sync.WaitGroup
for i := 0; i < calendarConsumers; i++ {
wg.Add(+1)
go func() {
defer wg.Done()
for calEvent := range calendarEventCh {
userCalendar := createUserCalendarFromConfig(ctx, calendarConfig, logger)
if err := deleteCalendarEvent(ctx, ds, userCalendar, calEvent); err != nil {
level.Error(logger).Log("msg", "delete user calendar event", "err", err)
continue
}
}
}()
}
for _, outOfDateCalendarEvent := range calendarEvents {
calendarEventCh <- outOfDateCalendarEvent
}
close(calendarEventCh)
wg.Wait()
}
}
func cleanupTeamCalendarEvents(
ctx context.Context,
ds fleet.Datastore,
userCalendar fleet.UserCalendar,
calendarConfig *fleet.GoogleCalendarIntegration,
team fleet.Team,
logger kitlog.Logger,
) error {
teamFeatureEnabled := team.Config.Integrations.GoogleCalendar != nil && team.Config.Integrations.GoogleCalendar.Enable
@ -692,7 +714,7 @@ func cleanupTeamCalendarEvents(
// so we want to cleanup all calendar events for the team.
}
return deleteAllCalendarEvents(ctx, ds, userCalendar, &team.ID)
return deleteAllCalendarEvents(ctx, ds, calendarConfig, &team.ID, logger)
}
func deleteCalendarEvent(ctx context.Context, ds fleet.Datastore, userCalendar fleet.UserCalendar, calendarEvent *fleet.CalendarEvent) error {

View file

@ -101,8 +101,10 @@ func TestMDMRunCommand(t *testing.T) {
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("Pending")},
}
hostByUUID := make(map[string]*fleet.Host)
hostByID := make(map[uint]*fleet.Host)
for _, h := range []*fleet.Host{macEnrolled, winEnrolled, macUnenrolled, winUnenrolled, linuxUnenrolled, macEnrolled2, winEnrolled2, macNonFleetEnrolled, winNonFleetEnrolled, macPending, winPending} {
hostByUUID[h.UUID] = h
hostByID[h.ID] = h
}
// define some files to use in the tests
@ -221,6 +223,15 @@ func TestMDMRunCommand(t *testing.T) {
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
return &fleet.HostMDMDiskEncryption{}, nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
h, ok := hostByID[hostID]
require.True(t, ok)
if h.MDMInfo == nil {
return nil, &notFoundError{}
}
return h.MDMInfo, nil
}
enqueuer.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
return map[string]error{}, nil
}
@ -467,6 +478,15 @@ func TestMDMLockCommand(t *testing.T) {
return nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
h, ok := hostsByID[hostID]
if !ok || h.MDMInfo == nil {
return nil, &notFoundError{}
}
return h.MDMInfo, nil
}
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
successfulOutput := func(ident string) string {
@ -701,6 +721,15 @@ func TestMDMUnlockCommand(t *testing.T) {
return nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
h, ok := hostsByID[hostID]
if !ok || h.MDMInfo == nil {
return nil, &notFoundError{}
}
return h.MDMInfo, nil
}
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
successfulOutput := func(ident string) string {
@ -1001,6 +1030,15 @@ func TestMDMWipeCommand(t *testing.T) {
return nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
h, ok := hostsByID[hostID]
if !ok || h.MDMInfo == nil {
return nil, &notFoundError{}
}
return h.MDMInfo, nil
}
appCfgAllMDM, appCfgWinMDM, appCfgMacMDM, appCfgNoMDM := setupAppConigs()
appCfgScriptsDisabled := &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}}

View file

@ -401,7 +401,7 @@ type agent struct {
// single goroutine at a time can execute scripts.
scriptExecRunning atomic.Bool
softwareVSCodeExtensionsProb float64
softwareQueryFailureProb float64
softwareVSCodeExtensionsFailProb float64
//
@ -544,7 +544,7 @@ func newAgent(
UUID: hostUUID,
SerialNumber: serialNumber,
softwareVSCodeExtensionsProb: softwareQueryFailureProb,
softwareQueryFailureProb: softwareQueryFailureProb,
softwareVSCodeExtensionsFailProb: softwareVSCodeExtensionsQueryFailureProb,
macMDMClient: macMDMClient,
@ -1737,7 +1737,7 @@ func (a *agent) processQuery(name, query string) (
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_macos":
ss := fleet.StatusOK
if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb {
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
ss = fleet.OsqueryStatus(1)
}
if ss == fleet.StatusOK {
@ -1746,7 +1746,7 @@ func (a *agent) processQuery(name, query string) (
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_windows":
ss := fleet.StatusOK
if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb {
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
ss = fleet.OsqueryStatus(1)
}
if ss == fleet.StatusOK {
@ -1755,7 +1755,7 @@ func (a *agent) processQuery(name, query string) (
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_linux":
ss := fleet.StatusOK
if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb {
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
ss = fleet.OsqueryStatus(1)
}
if ss == fleet.StatusOK {
@ -2019,8 +2019,8 @@ func main() {
onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled")
nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use")
softwareQueryFailureProb = flag.Float64("software_query_fail_prob", 0.5, "Probability of the software query failing")
softwareVSCodeExtensionsQueryFailureProb = flag.Float64("software_vscode_extensions_query_fail_prob", 0.5, "Probability of the software vscode_extensions query failing")
softwareQueryFailureProb = flag.Float64("software_query_fail_prob", 0.05, "Probability of the software query failing")
softwareVSCodeExtensionsQueryFailureProb = flag.Float64("software_vscode_extensions_query_fail_prob", 0.05, "Probability of the software vscode_extensions query failing")
commonSoftwareCount = flag.Int("common_software_count", 10, "Number of common installed applications reported to fleet")
commonVSCodeExtensionsSoftwareCount = flag.Int("common_vscode_extensions_software_count", 5, "Number of common vscode_extensions installed applications reported to fleet")

View file

@ -113,7 +113,7 @@ To uninstall the osquery agent, follow the below instructions for your operating
Run the Orbit [cleanup script](https://github.com/fleetdm/fleet/blob/main/orbit/tools/cleanup/cleanup_macos.sh)
#### Windows
Use the "Add or remove programs" dialog to remove Orbit.
Use the "Add or remove programs" dialog to remove Fleet osquery.
#### Ubuntu
Run `sudo apt remove fleet-osquery -y`

View file

@ -7373,10 +7373,10 @@ This allows you to easily configure scheduled queries that will impact a whole t
- [Run script](#run-script)
- [Get script result](#get-script-result)
- [Run live script](#run-live-script)
- [Upload a script](#upload-a-script)
- [Delete a script](#delete-a-script)
- [Add script](#add-script)
- [Delete script](#delete-script)
- [List scripts](#list-scripts)
- [Get or download a script](#get-or-download-a-script)
- [Get or download script](#get-or-download-script)
- [Get script details by host](#get-script-details-by-host)
### Run script
@ -7488,7 +7488,7 @@ Run a live script and get results back (5 minute timeout). Live scripts only run
}
```
### Upload a script
### Add script
Uploads a script, making it available to run on hosts assigned to the specified team (or no team).
@ -7538,7 +7538,7 @@ echo "hello"
}
```
### Delete a script
### Delete script
Deletes an existing script.
@ -7566,6 +7566,7 @@ Deletes an existing script.
| Name | Type | In | Description |
| --------------- | ------- | ----- | ----------------------------------------------------------------------------------------------------------------------------- |
| team_id | integer | query | _Available in Fleet Premium_. The ID of the team to filter scripts by. If not specified, it will filter only scripts that are available to hosts with no team. |
| page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. |
@ -7603,7 +7604,7 @@ Deletes an existing script.
```
### Get or download a script
### Get or download script
`GET /api/v1/fleet/scripts/:id`
@ -7614,7 +7615,7 @@ Deletes an existing script.
| id | integer | path | **Required.** The desired script's ID. |
| alt | string | query | If specified and set to "media", downloads the script's contents. |
#### Example (get a script)
#### Example (get script)
`GET /api/v1/fleet/scripts/123`
@ -7633,7 +7634,7 @@ Deletes an existing script.
```
#### Example (download a script's contents)
#### Example (download script)
`GET /api/v1/fleet/scripts/123?alt=media`

View file

@ -2,8 +2,6 @@
_Available in Fleet Premium_.
## Overview
CIS Benchmarks represent the consensus-based effort of cybersecurity experts globally to help you protect your systems against threats more confidently.
For more information about CIS Benchmarks check out [Center for Internet Security](https://www.cisecurity.org/cis-benchmarks)'s website.
@ -46,22 +44,6 @@ Two things are being evaluated in this policy:
If either of these conditions fails, the host is considered to be failing the policy.
## Requirements
Following are the requirements to use the CIS Benchmarks in Fleet:
- To use these policies, Fleet must have an up-to-date paid license (≥Fleet Premium).
- Devices must be running [`fleetd`](https://fleetdm.com/docs/using-fleet/orbit), the lightweight agent that bundles the latest osqueryd.
- Some CIS Benchmarks explicitly involve verifying MDM-based controls, so devices must be enrolled to an MDM solution. (Any MDM solution works, it doesn't have to be Fleet.)
- On macOS, the orbit executable in Fleetd must have "Full Disk Access", see [Grant Full Disk Access to Osquery on macOS](./Adding-hosts.md#grant-full-disk-access-to-osquery-on-macos).
### MDM required
Some of the policies created by Fleet use the [managed_policies](https://www.fleetdm.com/tables/managed_policies) table. This checks whether an MDM solution has turned on the setting to enforce the policy.
Using MDM is the recommended way to manage and enforce CIS Benchmarks. To learn how to set up MDM in Fleet, visit [here](/docs/using-fleet/mdm-macos-setup).
### Fleetd required
Fleet's CIS Benchmarks require our [osquery manager, Fleetd](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). This is because Fleetd includes tables which are not part of vanilla osquery in order to accomplish auditing the benchmarks.
## How to add CIS Benchmarks
All CIS policies are stored under our restricted licensed folder `ee/cis/`.
@ -89,25 +71,6 @@ To apply the policies on a specific team use the `--policies-team` flag:
fleetctl apply --policies-team "Workstations" -f cis-policy-queries.yml
```
## Limitations
Certain benchmarks require human action to audit, and cannot be automated by a policy in Fleet. For a list of specific benchmarks which are not covered, please visit the README for each benchmark:
- [macOS 13.0 Ventura](https://github.com/fleetdm/fleet/blob/main/ee/cis/macos-13/README.md)
- [macOS 14.0 Sonoma](https://github.com/fleetdm/fleet/blob/main/ee/cis/macos-14/README.md)
- [Windows 10 Enterprise](https://github.com/fleetdm/fleet/blob/main/ee/cis/win-10/README.md)
- [Windows 11 Enterprise](https://github.com/fleetdm/fleet/blob/main/ee/cis/win-11/README.md)
### Audit vs. remediation
Each benchmark has two elements:
1. Audit - how to find out whether the host is in compliance with the benchmark
2. Remediation - if the host is out of compliance with the benchmark, how to fix it
Since Fleetd is currently read-only without the ability to execute actions on the host, Fleet does not implement the remediation portions of CIS benchmarks.
To implement automated remediation, you can install a separate agent such as Munki, Chef, Puppet, etc. which has write functionality.
## Levels 1 and 2
CIS designates various benchmarks as Level 1 or Level 2 to describe the level of thoroughness and burden that each benchmark represents.
@ -126,6 +89,22 @@ This profile extends the "Level 1" profile. Items in this profile exhibit one or
- are intended for environments or use cases where security is paramount or acts as defense in depth measure
- may negatively inhibit the utility or performance of the technology.
## Requirements
Following are the requirements to use the CIS Benchmarks in Fleet:
- Devices must be running [`fleetd`](https://fleetdm.com/docs/using-fleet/orbit), Fleet's lightweight agent.
- Some CIS Benchmarks explicitly involve verifying MDM-based controls, so devices must be enrolled to an MDM solution.
- On macOS, the orbit component of fleetd must have "Full Disk Access", see [Grant Full Disk Access to Osquery on macOS](./Adding-hosts.md#grant-full-disk-access-to-osquery-on-macos).
## Limitations
Certain benchmarks cannot be automated by a policy in Fleet. For a list of specific benchmarks which are not covered, please visit the README for each benchmark:
- [macOS 13.0 Ventura](https://github.com/fleetdm/fleet/blob/main/ee/cis/macos-13/README.md)
- [macOS 14.0 Sonoma](https://github.com/fleetdm/fleet/blob/main/ee/cis/macos-14/README.md)
- [Windows 10 Enterprise](https://github.com/fleetdm/fleet/blob/main/ee/cis/win-10/README.md)
- [Windows 11 Enterprise](https://github.com/fleetdm/fleet/blob/main/ee/cis/win-11/README.md)
## Performance testing
In August 2023, we completed scale testing on 10k Windows hosts and 70k macOS hosts. Ultimately, we validated both server and host performance at that scale.

View file

@ -123,7 +123,7 @@ How to unenroll a host from Fleet:
## Advanced
- [Fleet agent (fleetd) components](#fleetd-components)
- [Signing fleetd installer](#signing-fleetd-installer)
- [Grant full disk access to osquery on macOS](#grant-full-disk-access-to-osquery-on-macos)
- [Using mTLS](#using-mtls)
@ -134,6 +134,25 @@ How to unenroll a host from Fleet:
- [Generating Windows installers using local WiX toolset](#generating-windows-installers-using-local-wix-toolset)
- [Experimental features](#experimental-features)
### fleetd components
```mermaid
graph LR;
tuf["<a href=https://theupdateframework.io/>TUF</a> file server<br>(default: <a href=https://tuf.fleetctl.com>tuf.fleetctl.com</a>)"];
fleet_server[Fleet<br>Server];
subgraph fleetd
orbit[orbit];
desktop[Fleet Desktop<br>Tray App];
osqueryd[osqueryd];
desktop_browser[Fleet Desktop<br> from Browser];
end
orbit -- "Fleet Orbit API (TLS)" --> fleet_server;
desktop -- "Fleet Desktop API (TLS)" --> fleet_server;
osqueryd -- "osquery<br>remote API (TLS)" --> fleet_server;
desktop_browser -- "My Device API (TLS)" --> fleet_server;
orbit -- "Auto Update (TLS)" --> tuf;
```
### Signing fleetd installers
>**Note:** Currently, the `fleetctl package` command does not support signing Windows fleetd installers. Windows installers can be signed after building.

View file

@ -120,15 +120,14 @@ An API-only user does not have access to the Fleet UI. Instead, it's only purpos
### Create API-only user
Before creating the API-only user, log in to `fleetctl` as an admin. See [authentication](https://#authentication) above for details.
To create your new API-only user, use `fleetctl user create`:
```sh
fleetctl user create --name "API User" --email api@example.com --password temp@pass123 --api-only
```
To use fleetctl with an API-only user, you will need to log in via `fleetctl`. See [authentication](https://#authentication) above for details.
#### Permissions
An API-only user can be given the same permissions as a regular user. The default access level is **Observer**. You can specify what level of access the new user should have using the `--global-role` flag:

View file

@ -60,7 +60,7 @@ const LogDestinationIndicator = ({
return (
<>
Each time a query runs, the data is sent to <br />
Amazon Kinesis Data Firehose.`
Amazon Kinesis Data Firehose.
</>
);
case "kinesis":
@ -81,7 +81,7 @@ const LogDestinationIndicator = ({
return (
<>
Each time a query runs, the data is <br /> sent to Google Cloud Pub
/ Sub.`
/ Sub.
</>
);
case "kafta":

View file

@ -799,10 +799,8 @@ const TAGGED_TEMPLATES = {
return (
<>
{" "}
edited declaration (DDM) profile <b>
{activity.details?.profile_name}
</b>{" "}
for{" "}
edited declaration (DDM) profiles{" "}
<b>{activity.details?.profile_name}</b> for{" "}
{getProfileMessageSuffix(
isPremiumTier,
"darwin",

View file

@ -2,7 +2,7 @@ import React from "react";
import Card from "components/Card";
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
import OsPrefillPreview from "../../../../../../../../assets/images/os-prefill-preview.gif";
const baseClass = "setup-assistant-preview";
@ -25,7 +25,7 @@ const SetupAssistantPreview = () => {
</p>
<img
className={`${baseClass}__preview-img`}
src={OsSetupPreview}
src={OsPrefillPreview}
alt="OS setup preview"
/>
</Card>

View file

@ -27,7 +27,7 @@ const GOOGLE_WORKSPACE_DOMAINS =
const DOMAIN_WIDE_DELEGATION =
"https://www.fleetdm.com/learn-more-about/domain-wide-delegation";
const ENABLING_CALENDAR_API =
"fleetdm.com/learn-more-about/enabling-calendar-api";
"https://www.fleetdm.com/learn-more-about/enabling-calendar-api";
const OAUTH_SCOPES =
"https://www.googleapis.com/auth/calendar.events,https://www.googleapis.com/auth/calendar.settings.readonly";
@ -112,10 +112,10 @@ const Calendars = (): JSX.Element => {
// Must set all keys or no keys at all
if (!curFormData.apiKeyJson && !!curFormData.domain) {
errors.apiKeyJson = "API key JSON must be present";
errors.apiKeyJson = "API key JSON must be completed";
}
if (!curFormData.domain && !!curFormData.apiKeyJson) {
errors.domain = "Domain must be present";
errors.domain = "Domain must be completed";
}
if (curFormData.apiKeyJson) {
try {
@ -167,11 +167,11 @@ const Calendars = (): JSX.Element => {
await configAPI.update({ integrations: destination });
renderFlash(
"success",
"Successfully saved calendar integration settings"
"Successfully saved calendar integration settings."
);
refetchConfig();
} catch (e) {
renderFlash("error", "Could not save calendar integration settings");
renderFlash("error", "Could not save calendar integration settings.");
} finally {
setIsUpdatingSettings(false);
}
@ -286,7 +286,7 @@ const Calendars = (): JSX.Element => {
</p>
<p className={`${baseClass}__configuration`}>
5. Configure your service account integration in Fleet using the
form below:
form below.
<ul>
<li>
Paste the full contents of the JSON file downloaded when

View file

@ -12,6 +12,10 @@
margin-block-start: $pad-small;
}
ul {
margin: 0;
}
li {
margin: $pad-small 0;
}
@ -27,6 +31,7 @@
}
&__api-key-json {
font-family: "SourceCodePro", $monospace;
min-width: 100%; // resize vertically only
height: 294px;
font-size: $x-small;

View file

@ -1089,12 +1089,10 @@ const ManageHostsPage = ({
const onDeleteHostSubmit = async () => {
setIsUpdatingHosts(true);
const teamId = isAnyTeamSelected ? currentTeamId ?? null : null;
try {
await (isAllMatchingHostsSelected
? hostsAPI.destroyByFilter({
teamId,
teamId: teamIdForApi,
query: searchQuery,
status,
labelId: selectedLabel?.id,

View file

@ -15,19 +15,34 @@
// NOTE: Look more into this styling
&__os_settings-dropdown,
&__macsettings-dropdown {
width: 137px;
.Select-value {
display: flex;
align-items: center;
position: initial !important;
line-height: initial !important;
&::before {
position: relative;
content: url(../assets/images/icon-filter-v2-black-16x16@2x.png);
transform: scale(0.5);
left: -8px;
top: 4px;
top: 1px;
}
}
.Select-input {
display: none !important;
}
.Select {
box-sizing: initial;
}
.Select-control {
height: 36px;
}
.dropdown__select {
border: 1px solid #e2e4ea;
}
}
}

View file

@ -788,7 +788,9 @@ const ManagePolicyPage = ({
place="left"
positionStrategy="fixed"
offset={24}
opacity={1}
disableStyleInjection
classNameArrow="tooltip-arrow"
>
Available in Fleet Premium
</ReactTooltip5>
@ -806,7 +808,9 @@ const ManagePolicyPage = ({
place="left"
positionStrategy="fixed"
offset={24}
opacity={1}
disableStyleInjection
classNameArrow="tooltip-arrow"
>
Select a team to manage
<br />

View file

@ -42,6 +42,15 @@
font-style: normal;
text-align: center;
}
// arrow styles directly from react-tooltip-5 css
.tooltip-arrow {
width: 8px;
height: 8px;
}
[class*="react-tooltip__place-left"] > .tooltip-arrow {
transform: rotate(315deg);
}
}
}
.Select-control {

View file

@ -345,13 +345,15 @@ const generateDataSet = (
// So, we need to add `osquery_policy` to the time of the cron update.
let policiesLastRun: Date;
let osqueryPolicyMs = 0;
const hostCountUpdatedAt =
const policiesThatHaveRunHostCountUpdatedAt =
// host counts of all policies that have run are updated at the same time, and are therefore
// identical, so we can use the first one. Those that haven't run will be `null`.
policiesList.find((p) => !!p.host_count_updated_at)
?.host_count_updated_at || "";
// If host_count_updated_at is not present, we assume the worst case.
const hostCountUpdateIntervalMs = 60 * 60 * 1000; // 1 hour (from server's `cron.go`)
const hostCountUpdatedAtDate = hostCountUpdatedAt
? new Date(hostCountUpdatedAt)
const hostCountUpdatedAtDate = policiesThatHaveRunHostCountUpdatedAt
? new Date(policiesThatHaveRunHostCountUpdatedAt)
: new Date(Date.now() - hostCountUpdateIntervalMs);
if (osquery_policy) {
// Convert from nanosecond to milliseconds
@ -360,13 +362,14 @@ const generateDataSet = (
hostCountUpdatedAtDate.getTime() - osqueryPolicyMs
);
} else {
// temporarily unused - will restore use with upcoming DB update
policiesLastRun = hostCountUpdatedAtDate;
}
// Now we figure out when the next host count update will be.
// The % (mod) is used below in case server was restarted and previously scheduled host count update was skipped.
const nextHostCountUpdateMs =
hostCountUpdateIntervalMs -
(hostCountUpdatedAt
(policiesThatHaveRunHostCountUpdatedAt
? (Date.now() - hostCountUpdatedAtDate.getTime()) %
hostCountUpdateIntervalMs
: 0);
@ -380,7 +383,14 @@ const generateDataSet = (
// Define policy has_run based on updated_at compared against last time policies ran.
const policyItemUpdatedAt = new Date(policyItem.updated_at);
policyItem.has_run = isAfter(policiesLastRun, policyItemUpdatedAt);
// TODO: restore and update setting of policyItem.has_run based on upcoming custom
// `policy_membership_updated_at`(ish) DB column/API response field
// policyItem.has_run = isAfter(policiesLastRun, policyItemUpdatedAt);
// all of the policiess `has_run` will be either true (cron has run, so host_count_updated_at
// has a value that is the same for all such policies) or false (policy is new, wasn't included
// in last cron run, host_count_updated_at is `null`)
policyItem.has_run = !!policyItem.host_count_updated_at;
if (!policyItem.has_run) {
// Include time for next update for reference in tooltip, which is only present if policy has not run.
policyItem.next_update_ms = nextPolicyUpdateMs(

View file

@ -14,11 +14,14 @@ import {
ICreateQueryRequestBody,
ISchedulableQuery,
} from "interfaces/schedulable_query";
import { IConfig } from "interfaces/config";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
import CustomLink from "components/CustomLink";
import BackLink from "components/BackLink";
import InfoBanner from "components/InfoBanner";
import useTeamIdParam from "hooks/useTeamIdParam";
@ -28,9 +31,7 @@ import PATHS from "router/paths";
import debounce from "utilities/debounce";
import deepDifference from "utilities/deep_difference";
import BackLink from "components/BackLink";
import EditQueryForm from "pages/queries/edit/components/EditQueryForm";
import { IConfig } from "interfaces/config";
import EditQueryForm from "./components/EditQueryForm";
interface IEditQueryPageProps {
router: InjectedRouter;
@ -304,19 +305,15 @@ const EditQueryPage = ({
}
return (
<div className={`${baseClass}__warning`}>
<div className={`${baseClass}__message`}>
<p>
Fleet is unable to run a live query. Refresh the page or log in
again. If this keeps happening please{" "}
<CustomLink
url="https://github.com/fleetdm/fleet/issues/new/choose"
text="file an issue"
newTab
/>
</p>
</div>
</div>
<InfoBanner color="yellow">
Fleet is unable to run a live query. Refresh the page or log in again.
If this keeps happening please{" "}
<CustomLink
url="https://github.com/fleetdm/fleet/issues/new/choose"
text="file an issue"
newTab
/>
</InfoBanner>
);
};

View file

@ -1,22 +1,6 @@
.edit-query-page {
@include page;
&__warning {
padding: $pad-medium;
font-size: $x-small;
color: $core-fleet-black;
background-color: #fff0b9;
border: 1px solid #f2c94c;
border-radius: $border-radius;
margin: 0;
margin-top: $pad-large;
p {
margin: 0;
line-height: 20px;
}
}
.ace_content {
min-height: 500px !important;
}

View file

@ -638,12 +638,32 @@ const EditQueryForm = ({
"differential_ignore_removals",
].includes(lastEditedQueryLoggingType);
// Note: The backend is not resetting the query reports with equivalent platform strings
// so we are not showing a warning unless the platform combinations differ
const formatPlatformEquivalences = (platforms?: string) => {
// Remove white spaces allowed by API and format into a sorted string converted from a sorted array
return platforms?.replace(/\s/g, "").split(",").sort().toString();
};
const changedPlatforms =
storedQuery &&
formatPlatformEquivalences(lastEditedQueryPlatforms) !==
formatPlatformEquivalences(storedQuery?.platform);
const changedMinOsqueryVersion =
storedQuery &&
lastEditedQueryMinOsqueryVersion !== storedQuery.min_osquery_version;
const enabledDiscardData =
storedQuery && lastEditedQueryDiscardData && !storedQuery.discard_data;
const confirmChanges =
currentlySavingQueryResults &&
(changedSQL || changedLoggingToDifferential || enabledDiscardData);
(changedSQL ||
changedLoggingToDifferential ||
enabledDiscardData ||
changedPlatforms ||
changedMinOsqueryVersion);
const showChangedSQLCopy =
changedSQL && !changedLoggingToDifferential && !enabledDiscardData;
@ -660,6 +680,7 @@ const EditQueryForm = ({
const disableSaveFormErrors =
(lastEditedQueryName === "" && !!lastEditedQueryId) || !!size(errors);
console.log("lastEditedQueryPlatforms", lastEditedQueryPlatforms);
return (
<>
<form className={`${baseClass}`} autoComplete="off">

View file

@ -116,7 +116,7 @@ export interface IExportHostsOptions {
}
export interface IActionByFilter {
teamId: number | null;
teamId?: number | null;
query: string;
status: string;
labelId?: number;

View file

@ -1,13 +1,13 @@
# Application security
- [Describe your secure coding practices (SDLC)](#describe-your-secure-coding-practices-including-code-reviews-use-of-staticdynamic-security-testing-tools-3rd-party-scansreviews)
- [SQL injection](#sql-injection)
- [Broken authentication](#broken-authentication--authentication-session-management-flaws-that-compromise-passwords-keys-session-tokens-etc)
- [Passwords](#passwords)
- [Authentication tokens](#authentication-tokens)
- [Sensitive data exposure](#sensitive-data-exposure--encryption-in-transit-at-rest-improperly-implemented-APIs)
- [Cross-site scripting](#cross-site-scripting--ensure-an-attacker-cant-execute-scripts-in-the-users-browser)
- [Components with known vulnerabilities](#components-with-known-vulnerabilities--prevent-the-use-of-libraries-frameworks-other-software-with-existing-vulnerabilities)
- [Describe your secure coding practices (SDLC)](https://fleetdm.com/handbook/business-operations/application-security#describe-your-secure-coding-practices-including-code-reviews-use-of-static-dynamic-security-testing-tools-3-rd-party-scans-reviews)
- [SQL injection](https://fleetdm.com/handbook/business-operations/application-security#sql-injection)
- [Broken authentication](https://fleetdm.com/handbook/business-operations/application-security#broken-authentication-authentication-session-management-flaws-that-compromise-passwords-keys-session-tokens-etc)
- [Passwords](https://fleetdm.com/handbook/business-operations/application-security#passwords)
- [Authentication tokens](https://fleetdm.com/handbook/business-operations/application-security#authentication-tokens)
- [Sensitive data exposure](https://fleetdm.com/handbook/business-operations/application-security#sensitive-data-exposure-encryption-in-transit-at-rest-improperly-implemented-apis)
- [Cross-site scripting](https://fleetdm.com/handbook/business-operations/application-security#cross-site-scripting-ensure-an-attacker-cant-execute-scripts-in-the-users-browser)
- [Components with known vulnerabilities](https://fleetdm.com/handbook/business-operations/application-security#components-with-known-vulnerabilities-prevent-the-use-of-libraries-frameworks-other-software-with-existing-vulnerabilities)
The Fleet community follows best practices when coding. Here are some of the ways we mitigate against the OWASP top 10 issues:

View file

@ -108,6 +108,34 @@ For Fleet's US contractors, running payroll is a manual process:
- Adjust time frame to match current payroll period (the 27th through 26th of the month)
- Sync hours and run contractor payroll.
### Grant role-specific license to a team member (RevOps)
Certain new team members, especially in go-to-market (GTM) roles, will need paid access to paid tools like Salesforce and LinkedIn Sales Navigator immediately on their first day with the company. Gong licenses that other departments need may [request them from BizOps](https://fleetdm.com/handbook/business-operations#contact-us) and we will make sure there is no license redundancy in that department. The table below can be used to determine which paid licenses they will need, based on their role:
| Role | Salesforce CRM | Salesforce "Inbox" | LinkedIn _(paid)_ | Gong _(paid)_ | Zoom _(paid)_|
|:-----------------|:---|:---|:----|:---|:---|
| 🐋 AE | ✅ | ✅ | ✅ | ✅ | ✅
| 🐋 CSM | ✅ | ✅ | ❌ | ✅ | ✅
| 🐋 SC | ✅ | ✅ | ❌ | ❌ | ✅
| ⚗️ PM | ❌ | ❌ | ❌ | ✅ | ✅
| ⚗️ PD | ❌ | ❌ | ❌ | ✅ | ✅
| 🔦 CEO | ✅ | ✅ | ✅ | ✅ | ✅
| Other roles | ❌ | ❌ | ❌ | ❌ | ✅
> **Warning:** Do NOT buy LinkedIn Recruiter. AEs and SDRs should use their personal Brex card to purchase the monthly [Core Sales Navigator](https://business.linkedin.com/sales-solutions/compare-plans) plan. Fleet does not use a company wide Sales Navigator account. The goal of Sales Navigator is to access to profile views and data, not InMail. Fleet does not send InMail.
### Add a seat to Salesforce
Here are the steps we take to grant appropriate Salesforce licenses to a new hire:
- Go to ["My Account"](https://fleetdm.lightning.force.com/lightning/n/standard-OnlineSalesHome).
- View contracts -> pick current contract.
- Add the desired number of licenses.
- Sign DocuSign sent to the email.
- The order will be processed in ~30m.
- Once the basic license has been added, you can create a new user using the new team member's `@fleetdm.com` email and assign a license to it.
- To also assign a user an "Inbox license", go to the ["Setup" page](https://fleetdm.lightning.force.com/lightning/setup/SetupOneHome/home) and select "User > Permission sets". Find the [inbox permission set](https://fleetdm.lightning.force.com/lightning/setup/PermSets/page?address=%2F005%3Fid%3D0PS4x000002uUn2%26isUserEntityOverride%3D1%26SetupNode%3DPermSets%26sfdcIFrameOrigin%3Dhttps%253A%252F%252Ffleetdm.lightning.force.com%26clc%3D1) and assign it to the new team member.
### Run US commission payroll
- Update [commission calculator](https://docs.google.com/spreadsheets/d/1vw6Q7kCC7-FdG5Fgx3ghgUdQiF2qwxk6njgK6z8_O9U/edit) with new revenue from any deals that are closed/won (have a subscription agreement signed by both parties) and have an **effective start date** within the previous month.
- Find detailed notes on this process in [Notes - Run commission payroll in Gusto](https://docs.google.com/document/d/1FQLpGxvHPW6X801HYYLPs5y8o943mmasQD3m9k_c0so/edit#).

View file

@ -42,6 +42,16 @@
autoIssue:
labels: [ "#g-business-operations" ]
repo: "confidential"
-
task: "Revenue report" # TODO tie this to a responsibility
startedOn: "2024-02-12"
frequency: "Weekly"
description: "At the start of every week, check the Salesforce reports for past due invoices, non-invoiced opportunities, and past due renewals. Report any findings to in the `#g-sales` channel by mentioning Alex Mitchell and Mike McNeil."
moreInfoUrl:
dri: "jostableford"
autoIssue:
labels: [ "#g-digital-experience" ]
repo: "confidential"
-
task: "AP invoice monitoring" # TODO tie this to a responsibility
startedOn: "2024-04-01"

View file

@ -29,29 +29,29 @@ All Fleet employees and long-term collaborators are expected to read and electro
Fleet requires all team members to comply with the following acceptable use requirements and procedures:
1. The use of Fleet computing systems is subject to monitoring by Fleet IT and/or Security teams.
- The use of Fleet computing systems is subject to monitoring by Fleet IT and/or Security teams.
2. Fleet team members must not leave computing devices (including laptops and smart devices) used for business purposes, including company-provided and BYOD devices, unattended in public. Unattended devices (even in private spaces) must be locked with the lid closed or through the OS screen lock mechanism.
- Fleet team members must not leave computing devices (including laptops and smart devices) used for business purposes, including company-provided and BYOD devices, unattended in public. Unattended devices (even in private spaces) must be locked with the lid closed or through the OS screen lock mechanism.
3. Device encryption must be enabled for all mobile devices accessing company data, such as whole-disk encryption for all laptops. This is automatically enforced on Fleet-managed macOS devices and must be manually configured for any unmanaged workstations.
- Device encryption must be enabled for all mobile devices accessing company data, such as whole-disk encryption for all laptops. This is automatically enforced on Fleet-managed macOS devices and must be manually configured for any unmanaged workstations.
4. Anti-malware or equivalent protection and monitoring must be installed and enabled on all endpoint systems that may be affected by malware, including workstations, laptops, and servers. This is automatically enforced on Fleet-managed macOS devices and must be manually configured for any unmanaged workstations.
- Anti-malware or equivalent protection and monitoring must be installed and enabled on all endpoint systems that may be affected by malware, including workstations, laptops, and servers. This is automatically enforced on Fleet-managed macOS devices and must be manually configured for any unmanaged workstations.
5. Teams must exclusively use legal software with a valid license installed through the "app store" or trusted sources. Well-documented open source software can be used. If in doubt, ask in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC).
- Teams must exclusively use legal software with a valid license installed through the "app store" or trusted sources. Well-documented open source software can be used. If in doubt, ask in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC).
6. Avoid sharing credentials. Secrets must be stored safely, using features such as GitHub secrets. For accounts and other sensitive data that need to be shared, use the company-provided password manager (1Password). If you don't know how to use the password manager or safely access secrets, please ask in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC)!
- Avoid sharing credentials. Secrets must be stored safely, using features such as GitHub secrets. For accounts and other sensitive data that need to be shared, use the company-provided password manager (1Password). If you don't know how to use the password manager or safely access secrets, please ask in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC)!
7. Sanitize and remove any sensitive or confidential information prior to posting. At Fleet, we are public by default. Sensitive information from logs, screenshots, or other types of data (eg. debug profiles) should not be shared publicly.
- Sanitize and remove any sensitive or confidential information prior to posting. At Fleet, we are public by default. Sensitive information from logs, screenshots, or other types of data (eg. debug profiles) should not be shared publicly.
8. Fleet team members must not let anyone else use Fleet-provided and managed workstations unsupervised, including family members and support personnel of vendors. Use screen sharing instead of allowing them to access your system directly, and never allow unattended screen sharing.
- Fleet team members must not let anyone else use Fleet-provided and managed workstations unsupervised, including family members and support personnel of vendors. Use screen sharing instead of allowing them to access your system directly, and never allow unattended screen sharing.
9. Device operating systems must be kept up to date. Fleet-managed macOS workstations will receive prompts for updates to be installed, and unmanaged devices are to be updated by the team member using them. Access may be revoked for devices not kept up to date.
- Device operating systems must be kept up to date. Fleet-managed macOS workstations will receive prompts for updates to be installed, and unmanaged devices are to be updated by the team member using them. Access may be revoked for devices not kept up to date.
10. Team members must not store sensitive data on external storage devices (USB sticks, external hard drives).
- Team members must not store sensitive data on external storage devices (USB sticks, external hard drives).
11. The use of Fleet company accounts on "shared" computers, such as hotel kiosk systems, is strictly prohibited.
- The use of Fleet company accounts on "shared" computers, such as hotel kiosk systems, is strictly prohibited.
12. Lost or stolen devices (laptops, or any other company-owned or personal devices used for work purposes) must be reported as soon as possible. Minutes count when responding to security incidents triggered by missing devices. Report a lost, stolen, or missing device by posting in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC), or use the security@ (fleetdm.com) email alias if you no longer have access to Slack. Include your name, the type of device, timeline (when were you last in control of the device?), whether the device was locked, whether any sensitive information is on the device, and any other relevant information in the report.
- Lost or stolen devices (laptops, or any other company-owned or personal devices used for work purposes) must be reported as soon as possible. Minutes count when responding to security incidents triggered by missing devices. Report a lost, stolen, or missing device by posting in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC), or use the security@ (fleetdm.com) email alias if you no longer have access to Slack. Include your name, the type of device, timeline (when were you last in control of the device?), whether the device was locked, whether any sensitive information is on the device, and any other relevant information in the report.
When in doubt, **ASK!** (in [#g-security](https://fleetdm.slack.com/archives/C037Q8UJ0CC))
@ -64,53 +64,53 @@ When in doubt, **ASK!** (in [#g-security](https://fleetdm.slack.com/archives/C03
Fleet requires all workforce members to comply with the following acceptable use requirements and procedures, such that:
1. Access to all computing resources, including servers, end-user computing devices, network equipment, services, and applications, must be protected by strong authentication, authorization, and auditing.
- Access to all computing resources, including servers, end-user computing devices, network equipment, services, and applications, must be protected by strong authentication, authorization, and auditing.
2. Interactive user access to production systems must be associated with an account or login unique to each user.
- Interactive user access to production systems must be associated with an account or login unique to each user.
3. All credentials, including user passwords, service accounts, and access keys, must meet the length, complexity, age, and rotation requirements defined in Fleet security standards.
- All credentials, including user passwords, service accounts, and access keys, must meet the length, complexity, age, and rotation requirements defined in Fleet security standards.
4. Use a strong password and two-factor authentication (2FA) whenever possible to authenticate to all computing resources (including both devices and applications).
- Use a strong password and two-factor authentication (2FA) whenever possible to authenticate to all computing resources (including both devices and applications).
5. 2FA is required to access any critical system or resource, including but not limited to resources in Fleet production environments.
- 2FA is required to access any critical system or resource, including but not limited to resources in Fleet production environments.
6. Unused accounts, passwords, and access keys must be removed within 30 days.
- Unused accounts, passwords, and access keys must be removed within 30 days.
7. A unique access key or service account must be used for different applications or user access.
- A unique access key or service account must be used for different applications or user access.
8. Authenticated sessions must time out after a defined period of inactivity.
- Authenticated sessions must time out after a defined period of inactivity.
### Access authorization and termination
Fleet policy requires that:
1. Access authorization shall be implemented using role-based access control (RBAC) or a similar mechanism.
- Access authorization shall be implemented using role-based access control (RBAC) or a similar mechanism.
2. Standard access based on a user's job role may be pre-provisioned during employee onboarding. All subsequent access requests to computing resources must be approved by the requestors manager prior to granting and provisioning of access.
- Standard access based on a user's job role may be pre-provisioned during employee onboarding. All subsequent access requests to computing resources must be approved by the requestors manager prior to granting and provisioning of access.
3. Access to critical resources, such as production environments, must be approved by the security team in addition to the requestors manager.
- Access to critical resources, such as production environments, must be approved by the security team in addition to the requestors manager.
4. Access must be reviewed regularly and revoked if no longer needed.
- Access must be reviewed regularly and revoked if no longer needed.
5. Upon the termination of employment, all system access must be revoked, and user accounts terminated within 24-hours or one business day, whichever is shorter.
- Upon the termination of employment, all system access must be revoked, and user accounts terminated within 24-hours or one business day, whichever is shorter.
6. All system access must be reviewed at least annually and whenever a user's job role changes.
- All system access must be reviewed at least annually and whenever a user's job role changes.
### Shared secrets management
Fleet policy requires that:
1. Use of shared credentials/secrets must be minimized.
- Use of shared credentials/secrets must be minimized.
2. If required by business operations, secrets/credentials must be shared securely and stored in encrypted vaults that meet the Fleet data encryption standards.
- If required by business operations, secrets/credentials must be shared securely and stored in encrypted vaults that meet the Fleet data encryption standards.
### Privileged access management
Fleet policy requires that:
1. Automation with service accounts must be used to configure production systems when technically feasible.
- Automation with service accounts must be used to configure production systems when technically feasible.
2. Use of high privilege accounts must only be performed when absolutely necessary.
- Use of high privilege accounts must only be performed when absolutely necessary.
## Asset management policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_
@ -123,11 +123,11 @@ You can't protect what you can't see. Therefore, Fleet must maintain an accurate
Fleet policy requires that:
1. IT and/or security must maintain an inventory of all critical company assets, both physical and logical.
- IT and/or security must maintain an inventory of all critical company assets, both physical and logical.
2. All assets should have identified owners and a risk/data classification tag.
- All assets should have identified owners and a risk/data classification tag.
3. All company-owned computer purchases must be tracked.
- All company-owned computer purchases must be tracked.
## Business continuity and disaster recovery policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_
@ -140,11 +140,11 @@ The Fleet business continuity and disaster recovery plan establishes procedures
Fleet policy requires that:
1. A plan and process for business continuity and disaster recovery (BCDR), will be defined and documented including the backup and recovery of critical systems and data,.
- A plan and process for business continuity and disaster recovery (BCDR), will be defined and documented including the backup and recovery of critical systems and data,.
2. BCDR shall be simulated and tested at least once a year.
- BCDR shall be simulated and tested at least once a year.
3. Security controls and requirements will be maintained during all BCDR activities.
- Security controls and requirements will be maintained during all BCDR activities.
### Business continuity plan
@ -153,22 +153,22 @@ Fleet policy requires that:
The following order of succession to make sure that decision-making authority for the Fleet Contingency Plan is uninterrupted. The Chief Executive Officer (CEO) is responsible for ensuring the safety of personnel and the execution of procedures documented within this Fleet Contingency Plan. The CTO is responsible for the recovery of Fleet technical environments. If the CEO or Head of Engineering cannot function as the overall authority or choose to delegate this responsibility to a successor, the board of directors shall serve as that authority or choose an alternative delegate.
For technical incidents:
1. CTO (Luke Heath)
2. CEO (Mike McNeil)
- CTO (Luke Heath)
- CEO (Mike McNeil)
For business/operational incidents:
1. CEO (Mike McNeil)
2. Head of Business Operations (Joanne Stableford)
- CEO (Mike McNeil)
- Head of Business Operations (Joanne Stableford)
### Response Teams and Responsibilities
The following teams have been developed and trained to respond to a contingency event affecting Fleet infrastructure and systems.
1. **Infrastructure** is responsible for recovering the Fleet automatic update service hosted environment. The team includes personnel responsible for the daily IT operations and maintenance. The team reports to the CTO.
- **Infrastructure** is responsible for recovering the Fleet automatic update service hosted environment. The team includes personnel responsible for the daily IT operations and maintenance. The team reports to the CTO.
2. **People Ops** is responsible for ensuring the physical safety of all Fleet personnel and coordinating the response to incidents that could impact it. Fleet has no physical site to recover. The team reports to the CEO.
- **People Ops** is responsible for ensuring the physical safety of all Fleet personnel and coordinating the response to incidents that could impact it. Fleet has no physical site to recover. The team reports to the CEO.
3. **Security** is responsible for assessing and responding to all cybersecurity-related incidents according to Fleet Incident Response policy and procedures. The security team shall assist the above teams in recovery as needed in non-cybersecurity events. The team leader is the CTO.
- **Security** is responsible for assessing and responding to all cybersecurity-related incidents according to Fleet Incident Response policy and procedures. The security team shall assist the above teams in recovery as needed in non-cybersecurity events. The team leader is the CTO.
Members of the above teams must maintain local copies of the contact information of the BCDR succession team. Additionally, the team leads must maintain a local copy of this policy in the event Internet access is not available during a disaster scenario.
@ -184,20 +184,18 @@ This phase addresses the initial actions taken to detect and assess the damage i
The notification sequence is listed below:
* The first responder is to notify the CTO. All known information must be relayed.
* The CTO is to contact the Response Teams and inform them of the event. The CTO or delegate is responsible to beginning the assessment procedures.
* The CTO is to notify team members and direct them to complete the assessment procedures outlined below to determine the extent of the issue and estimated recovery time.
* The Fleet Contingency Plan is to be activated if one or more of the following criteria are met:
* Fleet automatic update service will be unavailable for more than 48 hours.
* Cloud infrastructure service is damaged and will be unavailable for more than 24 hours.
* Other criteria, as appropriate and as defined by Fleet.
* If the plan is to be activated, the CTO is to notify and inform team members of the event details.
* Upon notification from the CTO, group leaders and managers must notify their respective teams. Team members are to be informed of all applicable information and prepared to respond and relocate if necessary.
* The CTO is to notify the remaining personnel and executive leadership on the general status of the incident.
* Notification can be via Slack, email, or phone.
* The CTO posts a blog post explaining that the service is down and recovery is in progress.
1. The first responder is to notify the CTO. All known information must be relayed.
2. The CTO is to contact the Response Teams and inform them of the event. The CTO or delegate is responsible to beginning the assessment procedures.
3. The CTO is to notify team members and direct them to complete the assessment procedures outlined below to determine the extent of the issue and estimated recovery time.
4. The Fleet Contingency Plan is to be activated if one or more of the following criteria are met:
- Fleet automatic update service will be unavailable for more than 48 hours.
- Cloud infrastructure service is damaged and will be unavailable for more than 24 hours.
- Other criteria, as appropriate and as defined by Fleet.
5. If the plan is to be activated, the CTO is to notify and inform team members of the event details.
6. Upon notification from the CTO, group leaders and managers must notify their respective teams. Team members are to be informed of all applicable information and prepared to respond and relocate if necessary.
7. The CTO is to notify the remaining personnel and executive leadership on the general status of the incident.
8. Notification can be via Slack, email, or phone.
9. The CTO posts a blog post explaining that the service is down and recovery is in progress.
#### Reconstitution Phase
@ -228,33 +226,22 @@ Additionally, this policy outlines requirements and procedures to create and mai
Data backup is an important part of the day-to-day operations of Fleet. To protect the confidentiality, integrity, and availability of sensitive and critical data, both for Fleet and Fleet Customers, complete backups are done daily to assure that data remains available when needed and in case of a disaster.
Fleet policy requires that:
1. Data should be classified at the time of creation or acquisition.
2. Fleet must maintain an up-to-date inventory and data flows mapping of all critical data.
3. All business data should be stored or replicated to a company-controlled repository.
4. Data must be backed up according to the level defined in Fleet data classification.
5. Data backup must be validated for integrity.
6. The data retention period must be defined and comply with any and all applicable regulatory and contractual requirements. More specifically,
* Data and records belonging to Fleet platform customers must be retained
per Fleet product terms and conditions and/or specific contractual
agreements.
7. By default, all security documentation and audit trails are kept for a minimum of seven years unless otherwise specified by Fleet data classification, specific regulations, or contractual agreement.
- Data should be classified at the time of creation or acquisition.
- Fleet must maintain an up-to-date inventory and data flows mapping of all critical data.
- All business data should be stored or replicated to a company-controlled repository.
- Data must be backed up according to the level defined in Fleet data classification.
- Data backup must be validated for integrity.
- The data retention period must be defined and comply with any and all applicable regulatory and contractual requirements. More specifically, **data and records belonging to Fleet platform customers must be retained per Fleet product terms and conditions and/or specific contractual agreements.**
- By default, all security documentation and audit trails are kept for a minimum of seven years unless otherwise specified by Fleet data classification, specific regulations, or contractual agreement.
### Data Classification Model
Fleet defines the following four data classifications:
* **Critical**
* **Confidential**
* **Internal**
* **Public**
- **Critical**
- **Confidential**
- **Internal**
- **Public**
As Fleet is an open company by default, most of our data falls into **public**.
@ -268,12 +255,12 @@ External disclosure of critical data is strictly prohibited without an approved
*Example Critical Data Types* include
* PII (personal identifiable information)
* ePHI (electronically protected health information)
* Production security data, such as
- PII (personal identifiable information)
- ePHI (electronically protected health information)
- Production security data, such as
- Production secrets, passwords, access keys, certificates, etc.
- Production security audit logs, events, and incident data
* Production customer data
- Production customer data
**Confidential** and proprietary data represents company secrets and is of significant value to the company.
@ -284,14 +271,14 @@ Disclosure requires the signing of NDA and management approval.
*Example Confidential Data Types* include
* Business plans
* Employee/HR data
* News and public announcements (pre-announcement)
* Patents (pre-filing)
* Production metadata (server logs, non-secret configurations, etc.)
* Non-production security data, including
- Non-prod secrets, passwords, access keys, certificates, etc.
- Non-prod security audit logs, events, and incident data
- Business plans
- Employee/HR data
- News and public announcements (pre-announcement)
- Patents (pre-filing)
- Production metadata (server logs, non-secret configurations, etc.)
- Non-production security data, including
- Non-prod secrets, passwords, access keys, certificates, etc.
- Non-prod security audit logs, events, and incident data
**Internal** data contains information used for internal operations.
@ -305,11 +292,11 @@ protected.
*Example Internal Data Types* include
* Fleet source code.
* news and public announcements (post-announcement).
* marketing materials.
* product documentation.
* content posted on the company website(s) and social media channel(s).
- Fleet source code.
- news and public announcements (post-announcement).
- marketing materials.
- product documentation.
- content posted on the company website(s) and social media channel(s).
#### Data Handling Requirements Matrix
@ -345,17 +332,12 @@ This process is followed when offboarding a customer and deleting all of the pro
Fleet requires all workforce members to comply with the encryption policy, such that:
1. The storage drives of all Fleet-owned workstations must be encrypted and enforced by the IT and/or security team.
2. Confidential data must be stored in a manner that supports user access logs.
3. All Production Data at rest is stored on encrypted volumes.
4. Volume encryption keys and machines that generate volume encryption keys are protected from unauthorized access. Volume encryption key material is protected with access controls such that the key material is only accessible by privileged accounts.
5. Encrypted volumes use strong cipher algorithms, key strength, and key management process as defined below.
6. Data is protected in transit using recent TLS versions with ciphers recognized as secure.
- The storage drives of all Fleet-owned workstations must be encrypted and enforced by the IT and/or security team.
- Confidential data must be stored in a manner that supports user access logs.
- All Production Data at rest is stored on encrypted volumes.
- Volume encryption keys and machines that generate volume encryption keys are protected from unauthorized access. Volume encryption key material is protected with access controls such that the key material is only accessible by privileged accounts.
- Encrypted volumes use strong cipher algorithms, key strength, and key management process as defined below.
- Data is protected in transit using recent TLS versions with ciphers recognized as secure.
### Local disk/volume encryption
@ -363,13 +345,11 @@ Encryption and key management for local disk encryption of end-user devices foll
### Protecting data in transit
1. All external data transmission is encrypted end-to-end. This includes, but is not limited to, cloud infrastructure and third-party vendors and applications.
2. Transmission encryption keys and systems that generate keys are protected from unauthorized access. Transmission encryption key materials are protected with access controls and may only be accessed by privileged accounts.
3. TLS endpoints must score at least an "A" on SSLLabs.com.
4. Transmission encryption keys are limited to use for one year and then must be regenerated.
- All external data transmission is encrypted end-to-end. This includes, but is not limited to, cloud infrastructure and third-party vendors and applications.
- Transmission encryption keys and systems that generate keys are protected from unauthorized access.
- Transmission encryption key materials are protected with access controls and may only be accessed by privileged accounts.
- TLS endpoints must score at least an "A" on SSLLabs.com.
- Transmission encryption keys are limited to use for one year and then must be regenerated.
### Authorized Sub-Processors for Fleet Cloud services
@ -391,95 +371,70 @@ Fleet policy requires all workforce members to comply with the HR Security Polic
Fleet policy requires that:
1. Background verification checks on candidates for employees and contractors with production access to the Fleet infrastructure resources must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk.
2. Employees, contractors, and third-party users must agree to and sign the terms and conditions of their employment contract and comply with acceptable use.
3. Employees will perform an onboarding process that familiarizes them with the environments, systems, security requirements, and procedures that Fleet already has in place. Employees will also have ongoing security awareness training that is audited.
4. Employee offboarding will include reiterating any duties and responsibilities still valid after terminations, verifying that access to any Fleet systems has been removed, and ensuring that all company-owned assets are returned.
5. Fleet and its employees will take reasonable measures to make sure no sensitive data is transmitted via digital communications such as email or posted on social media outlets.
6. Fleet will maintain a list of prohibited activities that will be part of onboarding procedures and have training available if/when the list of those activities changes.
7. A fair disciplinary process will be used for employees suspected of committing security breaches. Fleet will consider multiple factors when deciding the response, such as whether or not this was a first offense, training, business contracts, etc. Fleet reserves the right to terminate employees in the case of severe cases of misconduct.
8. Fleet will maintain a reporting structure that aligns with the organization's business lines and/or individual's functional roles. The list of employees and reporting structure must be available to [all employees](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0).
9. Employees will receive regular feedback and acknowledgment from their managers and peers. Managers will give constant feedback on performance, including but not limited to during regular one-on-one meetings.
10. Fleet will publish job descriptions for available positions and conduct interviews to assess a candidate's technical skills as well as soft skills prior to hiring.
11. Background checks of an employee or contractor must be performed by operations and/or the hiring team before we grant the new employee or contractor access to the Fleet production environment.
12. A list of employees and contractors will be maintained, including their titles and managers, and made available to everyone internally.
13. An [anonymous](https://docs.google.com/forms/d/e/1FAIpQLSdv2abLfCUUSxFCrSwh4Ou5yF80c4V2K_POoYbHt3EU1IY-sQ/viewform?vc=0&c=0&w=1&flr=0&fbzx=4276110450338060288) form to report unethical behavior will be provided to employees.
- Background verification checks on candidates for employees and contractors with production access to the Fleet infrastructure resources must be carried out in accordance with relevant laws, regulations, and ethics. These checks should be proportional to the business requirements, the classification of the information to be accessed, and the perceived risk.
- Employees, contractors, and third-party users must agree to and sign the terms and conditions of their employment contract and comply with acceptable use.
- Employees will perform an onboarding process that familiarizes them with the environments, systems, security requirements, and procedures that Fleet already has in place. Employees will also have ongoing security awareness training that is audited.
- Employee offboarding will include reiterating any duties and responsibilities still valid after terminations, verifying that access to any Fleet systems has been removed, and ensuring that all company-owned assets are returned.
- Fleet and its employees will take reasonable measures to make sure no sensitive data is transmitted via digital communications such as email or posted on social media outlets.
- Fleet will maintain a list of prohibited activities that will be part of onboarding procedures and have training available if/when the list of those activities changes.
- A fair disciplinary process will be used for employees suspected of committing security breaches. Fleet will consider multiple factors when deciding the response, such as whether or not this was a first offense, training, business contracts, etc. Fleet reserves the right to terminate employees in the case of severe cases of misconduct.
- Fleet will maintain a reporting structure that aligns with the organization's business lines and/or individual's functional roles. The list of employees and reporting structure must be available to [all employees](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0).
- Employees will receive regular feedback and acknowledgment from their managers and peers. Managers will give constant feedback on performance, including but not limited to during regular one-on-one meetings.
- Fleet will publish job descriptions for available positions and conduct interviews to assess a candidate's technical skills as well as soft skills prior to hiring.
- Background checks of an employee or contractor must be performed by operations and/or the hiring team before we grant the new employee or contractor access to the Fleet production environment.
- A list of employees and contractors will be maintained, including their titles and managers, and made available to everyone internally.
- An [anonymous](https://docs.google.com/forms/d/e/1FAIpQLSdv2abLfCUUSxFCrSwh4Ou5yF80c4V2K_POoYbHt3EU1IY-sQ/viewform?vc=0&c=0&w=1&flr=0&fbzx=4276110450338060288) form to report unethical behavior will be provided to employees.
## Incident response policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/). Based on the SANS incident response process._
Fleet policy requires that:
1. All computing environments and systems must be monitored in accordance with Fleet policies and procedures specified in the Fleet handbook.
2. Alerts must be reviewed to identify security incidents.
3. Incident response procedures are invoked upon discovery of a valid security incident.
4. Incident response team and management must comply with any additional requests by law enforcement in the event of a criminal investigation or national security, including but not limited to warranted data requests, subpoenas, and breach notifications.
- All computing environments and systems must be monitored in accordance with Fleet policies and procedures specified in the Fleet handbook.
- Alerts must be reviewed to identify security incidents.
- Incident response procedures are invoked upon discovery of a valid security incident.
- Incident response team and management must comply with any additional requests by law enforcement in the event of a criminal investigation or national security, including but not limited to warranted data requests, subpoenas, and breach notifications.
### Incident response plan
#### Security Incident Response Team (SIRT)
The Security Incident Response Team (SIRT) is responsible for
* Reviewing analyzing, and logging all received reports and tracking their statuses.
* Performing investigations, creating and executing action plans, and post-incident activities.
* Collaboration with law enforcement agencies.
- Reviewing analyzing, and logging all received reports and tracking their statuses.
- Performing investigations, creating and executing action plans, and post-incident activities.
- Collaboration with law enforcement agencies.
Current members of the Fleet SIRT:
* CTO
* CEO
* VP of Customer Success
- CTO
- CEO
- VP of Customer Success
#### Incident Management Process
Fleet's incident response classifies security-related events into the following categories:
- **Events** - Any observable computer security-related occurrence in a system or network with a negative consequence. Examples:
- Hardware component failing, causing service outages.
- Software error causing service outages.
- General network or system instability.
* **Events** - Any observable computer security-related occurrence in a system
or network with a negative consequence. Examples:
- **Precursors** - A sign that an incident may occur in the future. Examples:
- Monitoring system showing unusual behavior.
- Audit log alerts indicated several failed login attempts.
- Suspicious emails that target specific Fleet staff members with administrative access to production systems.
- Alerts raised from a security control source based on its monitoring policy, such as:
- Google Workspace (user authentication activities)
- Fleet (internal instance)
- Syslog events from servers
* Hardware component failing, causing service outages.
* Software error causing service outages.
* General network or system instability.
- **Indications** - A sign that an incident may have occurred or may be occurring at the present time. Examples:
- Alerts for modified system files or unusual system accesses.
- Antivirus alerts for infected files or devices.
- Excessive network traffic directed at unexpected geographic locations.
* **Precursors** - A sign that an incident may occur in the future. Examples:
* Monitoring system showing unusual behavior.
* Audit log alerts indicated several failed login attempts.
* Suspicious emails that target specific Fleet staff members with
administrative access to production systems.
* Alerts raised from a security control source based on its monitoring
policy, such as
- Google Workspace (user authentication activities)
- Fleet (internal instance)
- Syslog events from servers
* **Indications** - A sign that an incident may have occurred or may be
occurring at the present time. Examples:
* Alerts for modified system files or unusual system accesses.
* Antivirus alerts for infected files or devices.
* Excessive network traffic directed at unexpected geographic locations.
* **Incidents** - A confirmed attack/indicator of compromise or a validated
violation of computer security policies or acceptable use policies, often
resulting in data breaches. Examples:
* Unauthorized disclosure of sensitive data
* Unauthorized change or destruction of sensitive data
* A data breach accomplished by an internal or external entity
* A Denial-of-Service (DoS) attack causing a critical service to become
- **Incidents** - A confirmed attack/indicator of compromise or a validated violation of computer security policies or acceptable use policies, often resulting in data breaches. Examples:
- Unauthorized disclosure of sensitive data
- Unauthorized change or destruction of sensitive data
- A data breach accomplished by an internal or external entity
- A Denial-of-Service (DoS) attack causing a critical service to become
unreachable
Fleet employees must report any unauthorized or suspicious activity seen on
@ -492,52 +447,23 @@ Incidents of a severity/impact rating higher than **MINOR** shall trigger the re
#### I - Identification and Triage
1. Immediately upon observation, Fleet members report suspected and known
Events, Precursors, Indications, and Incidents in one of the following ways:
1. Direct report to management, CTO, CEO, or
other
2. Email
3. Phone call
4. Slack
2. The individual receiving the report facilitates the collection of additional
information about the incident, as needed, and notifies the CTO
(if not already done).
3. The CTO determines if the issue is an Event, Precursor,
Indication, or Incident.
1. If the issue is an event, indication, or precursor, the CTO
forwards it to the appropriate resource for resolution.
1. Non-Technical Event (minor infringement): the CTO of the
designee creates an appropriate issue in GitHub and further investigates
the incident as needed.
2. Technical Event: Assign the issue to a technical resource for
resolution. This resource may also be a contractor or outsourced
technical resource in the event of a lack of resource or expertise in
the area.
2. If the issue is a security incident, the CTO activates the
Security Incident Response Team (SIRT) and notifies senior leadership by
email.
1. If a non-technical security incident is discovered, the SIRT completes
the investigation, implements preventative measures, and resolves the
security incident.
2. Once the investigation is completed, progress to Phase V, Follow-up.
3. If the issue is a technical security incident, commence to Phase II:
Containment.
4. The Containment, Eradication, and Recovery Phases are highly
technical. It is important to have them completed by a highly
qualified technical security resource with oversight by the SIRT team.
5. Each individual on the SIRT and the technical security resource
document all measures taken during each phase, including the start and
end times of all efforts.
6. The lead member of the SIRT team facilitates the initiation of an Incident
ticket in GitHub Security Project and documents all findings and details
in the ticket.
1. Immediately upon observation, Fleet members report suspected and known Events, Precursors, Indications, and Incidents in one of the following ways:
- Direct report to management, CTO, CEO, or other
- Email
- Phone call
- Slack
2. The individual receiving the report facilitates the collection of additional information about the incident, as needed, and notifies the CTO (if not already done).
3. The CTO determines if the issue is an Event, Precursor, Indication, or Incident.
- If the issue is an event, indication, or precursor, the CTO forwards it to the appropriate resource for resolution.
- Non-Technical Event (minor infringement): the CTO of the designee creates an appropriate issue in GitHub and further investigates the incident as needed.
- Technical Event: Assign the issue to a technical resource for resolution. This resource may also be a contractor or outsourced technical resource in the event of a lack of resource or expertise in the area.
- If the issue is a security incident, the CTO activates the Security Incident Response Team (SIRT) and notifies senior leadership by email.
- If a non-technical security incident is discovered, the SIRT completes the investigation, implements preventative measures, and resolves the security incident.
- Once the investigation is completed, progress to Phase V, Follow-up.
- If the issue is a technical security incident, commence to Phase II: Containment.
- The Containment, Eradication, and Recovery Phases are highly technical. It is important to have them completed by a highly qualified technical security resource with oversight by the SIRT team.
- Each individual on the SIRT and the technical security resource document all measures taken during each phase, including the start and end times of all efforts.
- The lead member of the SIRT team facilitates the initiation of an Incident ticket in GitHub Security Project and documents all findings and details in the ticket.
* The intent of the Incident ticket is to provide a summary of all
events, efforts, and conclusions of each Phase of this policy and
@ -577,27 +503,17 @@ appropriate.
2. Secure the blast radius (i.e., a physical or logical network perimeter or
access zone).
3. Perform the following forensic analysis preparation, as needed:
- Securely connect to the affected system over a trusted connection.
- Retrieve any volatile data from the affected system.
- Determine the relative integrity and the appropriateness of backing the system up.
- As necessary, take a snapshot of the disk image for further forensic, and if appropriate, back up the system.
- Change the password(s) to the affected system(s).
- Determine whether it is safe to continue operations with the affected system(s).
- If it is safe, allow the system to continue to functioning; and move to Phase V, Post Incident Analysis and Follow-up.
- If it is NOT safe to allow the system to continue operations, discontinue the system(s) operation and move to Phase III, Eradication.
- The individual completing this phase provides written communication to the SIRT.
1. Securely connect to the affected system over a trusted connection.
2. Retrieve any volatile data from the affected system.
3. Determine the relative integrity and the appropriateness of backing the
system up.
4. As necessary, take a snapshot of the disk image for further forensic,
and if appropriate, back up the system.
5. Change the password(s) to the affected system(s).
6. Determine whether it is safe to continue operations with the affected
system(s).
7. If it is safe, allow the system to continue to functioning; and move to
Phase V, Post Incident Analysis and Follow-up.
8. If it is NOT safe to allow the system to continue operations, discontinue
the system(s) operation and move to Phase III, Eradication.
9. The individual completing this phase provides written communication to
the SIRT.
4. Complete any documentation relative to the security incident containment on the
Incident ticket, using
[SANS IH Containment Form](https://www.sans.org/media/score/incident-forms/IH-Containment.pdf)
as a template.
4. Complete any documentation relative to the security incident containment on the Incident ticket, using [SANS IH Containment Form](https://www.sans.org/media/score/incident-forms/IH-Containment.pdf) as a template.
5. Continuously apprise Senior Management of progress.
6. Continue to notify affected Customers and Partners with relevant updates as
needed.
@ -611,28 +527,17 @@ resulting security exposures that are now on the affected system(s).
2. Strengthen the defenses surrounding the affected system(s), where possible (a
risk assessment may be needed and can be determined by the Head of Security).
This may include the following:
- An increase in network perimeter defenses.
- An increase in system monitoring defenses.
- Remediation ("fixing") any security issues within the affected system, such as removing unused services/general host hardening techniques.
1. An increase in network perimeter defenses.
2. An increase in system monitoring defenses.
3. Remediation ("fixing") any security issues within the affected system,
such as removing unused services/general host hardening techniques.
3. Conduct a detailed vulnerability assessment to verify all the holes/gaps that can be exploited are addressed.
- If additional issues or symptoms are identified, take appropriate preventative measures to eliminate or minimize potential future compromises.
3. Conduct a detailed vulnerability assessment to verify all the holes/gaps that
can be exploited are addressed.
1. If additional issues or symptoms are identified, take appropriate
preventative measures to eliminate or minimize potential future
compromises.
4. Update the Incident ticket with Eradication details, using
[SANS IH Eradication Form](https://www.sans.org/media/score/incident-forms/IH-Eradication.pdf)
as a template.
5. Update the documentation with the information learned from the vulnerability
assessment, including the cause, symptoms, and the method used to fix the
problem with the affected system(s).
4. Update the Incident ticket with Eradication details, using [SANS IH Eradication Form](https://www.sans.org/media/score/incident-forms/IH-Eradication.pdf) as a template.
5. Update the documentation with the information learned from the vulnerability assessment, including the cause, symptoms, and the method used to fix the problem with the affected system(s).
6. Apprise Senior Management of the progress.
7. Continue to notify affected Customers and Partners with relevant updates as
needed.
7. Continue to notify affected Customers and Partners with relevant updates as needed.
8. Move to Phase IV, Recovery.
#### IV - Recovery (Technical)
@ -641,26 +546,15 @@ The Recovery Phase represents the SIRT's effort to restore the affected
system(s) to operation after the resulting security exposures, if any, have
been corrected.
1. The technical team determines if the affected system(s) have been changed in
any way.
1. If they have, the technical team restores the system to its proper,
intended functioning ("last known good").
2. Once restored, the team validates that the system functions the way it
was intended/had functioned in the past. This may require the involvement
of the business unit that owns the affected system(s).
3. If the operation of the system(s) had been interrupted (i.e., the system(s)
had been taken offline or dropped from the network while triaged),
restart the restored and validated system(s) and monitor for behavior.
4. If the system had not been changed in any way but was taken offline
(i.e., operations had been interrupted), restart the system and monitor
for proper behavior.
5. Update the documentation with the detail that was determined during this
phase.
6. Apprise Senior Management of progress.
7. Continue to notify affected Customers and Partners with relevant updates
as needed.
8. Move to Phase V, Follow-up.
The technical team determines if the affected system(s) have been changed in any way.
1. If they have, the technical team restores the system to its proper, intended functioning ("last known good").
2. Once restored, the team validates that the system functions the way it was intended/had functioned in the past. This may require the involvement of the business unit that owns the affected system(s).
3. If the operation of the system(s) had been interrupted (i.e., the system(s) had been taken offline or dropped from the network while triaged), restart the restored and validated system(s) and monitor for behavior.
4. If the system had not been changed in any way but was taken offline (i.e., operations had been interrupted), restart the system and monitor for proper behavior.
5. Update the documentation with the detail that was determined during this phase.
6. Apprise Senior Management of progress.
7. Continue to notify affected Customers and Partners with relevant updates as needed.
8. Move to Phase V, Follow-up.
#### V - Post-Incident Analysis (Technical and Non-Technical)
@ -670,27 +564,16 @@ been improved. It is recommended all security incidents be reviewed
shortly after resolution to determine where response could be improved.
Timeframes may extend to one to two weeks post-incident.
1. Responders to the security incident (SIRT Team and technical security
resource) meet to review the documentation collected during the security
incident.
1. Responders to the security incident (SIRT Team and technical security resource) meet to review the documentation collected during the security incident.
2. A "lessons learned" section is written and attached to the Incident ticket.
1. Evaluate the cost and impact of the security incident on Fleet using
the documents provided by the SIRT and the technical security resource.
2. Determine what could be improved. This may include:
* Systems and processes adjustments
* Awareness training and documentation
* Implementation of additional controls
3. Communicate these findings to Senior Management for approval and
implementation of any recommendations made post-review of the security
incident.
4. Carry out recommendations approved by Senior Management; sufficient
budget, time, and resources should be committed to this activity.
3. Ensure all incident-related information is recorded and retained as described
in Fleet Auditing requirements and Data Retention standards.
- Evaluate the cost and impact of the security incident on Fleet using the documents provided by the SIRT and the technical security resource.
- Determine what could be improved. This may include:
- Systems and processes adjustments
- Awareness training and documentation
- Implementation of additional controls
- Communicate these findings to Senior Management for approval and implementation of any recommendations made post-review of the security incident.
- Carry out recommendations approved by Senior Management; sufficient budget, time, and resources should be committed to this activity.
3. Ensure all incident-related information is recorded and retained as described in Fleet Auditing requirements and Data Retention standards.
4. Close the security incident.
#### Periodic Evaluation
@ -717,11 +600,11 @@ Fleet Device Management is committed to conducting business in compliance with a
| Board of directors | Oversight over risk and internal control for information security, privacy, and compliance<br/> Consults with executive leadership to understand Fleet's security mission and risks and provides guidance to bring them into alignment |
| Executive leadership | Approves capital expenditures for information security<br/> Oversight over the execution of the information security risk management program<br/> Communication path to Fleet's board of directors. Meets with the board regularly, including at least one official meeting a year<br/> Aligns information security policy and posture based on Fleet's mission, strategic objectives, and risk appetite |
CTO | Oversight over information security in the software development process<br/> Responsible for the design, development, implementation, operation, maintenance and monitoring of development and commercial cloud hosting security controls<br/> Responsible for oversight over policy development <br/>Responsible for implementing risk management in the development process |
| Head of security | Oversight over the implementation of information security controls for infrastructure and IT processes<br/> Responsible for the design, development, implementation, operation, maintenance, and monitoring of IT security controls<br/> Communicate information security risks to executive leadership<br/> Report information security risks annually to Fleet's leadership and gains approvals to bring risks to acceptable levels<br/> Coordinate the development and maintenance of information security policies and standards<br/> Work with applicable executive leadership to establish an information security framework and awareness program<br/> Serve as liaison to the board of directors, law enforcement and legal department.<br/> Oversight over identity management and access control processes |
| Head of Security | Oversight over the implementation of information security controls for infrastructure and IT processes<br/> Responsible for the design, development, implementation, operation, maintenance, and monitoring of IT security controls<br/> Communicate information security risks to executive leadership<br/> Report information security risks annually to Fleet's leadership and gains approvals to bring risks to acceptable levels<br/> Coordinate the development and maintenance of information security policies and standards<br/> Work with applicable executive leadership to establish an information security framework and awareness program<br/> Serve as liaison to the board of directors, law enforcement and legal department.<br/> Oversight over identity management and access control processes |
| System owners | Manage the confidentiality, integrity, and availability of the information systems for which they are responsible in compliance with Fleet policies on information security and privacy.<br/> Approve of technical access and change requests for non-standard access |
| Employees, contractors, temporary workers, etc. | Acting at all times in a manner that does not place at risk the security of themselves, colleagues, and the information and resources they have use of<br/> Helping to identify areas where risk management practices should be adopted<br/> Adhering to company policies and standards of conduct Reporting incidents and observed anomalies or weaknesses |
| Head of people operations | Ensuring employees and contractors are qualified and competent for their roles<br/> Ensuring appropriate testing and background checks are completed<br/> Ensuring that employees and relevant contractors are presented with company policies <br/> Ensuring that employee performance and adherence to values is evaluated<br/> Ensuring that employees receive appropriate security training |
| Head of business operations | Responsible for oversight over third-party risk management process; responsible for review of vendor service contracts |
| Head of People Operations | Ensuring employees and contractors are qualified and competent for their roles<br/> Ensuring appropriate testing and background checks are completed<br/> Ensuring that employees and relevant contractors are presented with company policies <br/> Ensuring that employee performance and adherence to values is evaluated<br/> Ensuring that employees receive appropriate security training |
| Head of Business Operations | Responsible for oversight over third-party risk management process; responsible for review of vendor service contracts |
## Operations security and change management policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_
@ -732,17 +615,13 @@ CTO | Oversight over information sec
Fleet policy requires
1. all production changes, including but not limited to software deployment, feature toggle enablement, network infrastructure changes, and access control authorization updates, must be invoked through the approved change management process.
2.each production change must maintain complete traceability to fully document the request, including the requestor, date/time of change, actions taken, and results.
3. each production change must include proper approval.
* The approvers are determined based on the type of change.
* Approvers must be someone other than the author/executor of the change unless they are the DRI for that system.
* Approvals may be automatically granted if specific criteria are met.
The auto-approval criteria must be pre-approved by the Head of Security and
fully documented and validated for each request.
- All production changes, including but not limited to software deployment, feature toggle enablement, network infrastructure changes, and access control authorization updates, must be invoked through the approved change management process.
- Each production change must maintain complete traceability to fully document the request, including the requestor, date/time of change, actions taken, and results.
- Each production change must include proper approval.
- The approvers are determined based on the type of change.
- Approvers must be someone other than the author/executor of the change unless they are the DRI for that system.
- Approvals may be automatically granted if specific criteria are met.
- The auto-approval criteria must be pre-approved by the Head of Security and fully documented and validated for each request.
## Risk management policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_
@ -753,13 +632,10 @@ Fleet policy requires
Fleet policy requires:
1. A thorough risk assessment must be conducted to evaluate potential threats and vulnerabilities to the confidentiality, integrity, and availability of sensitive, confidential, and proprietary electronic information Fleet stores, transmits, and/or processes.
2. Risk assessments must be performed with any major change to Fleet's business or technical operations and/or supporting infrastructure no less than once per year.
3. Strategies shall be developed to mitigate or accept the risks identified in the risk assessment process.
4. The risk register is monitored quarterly to assess compliance with the above policy, and document newly discovered or created risks.
- A thorough risk assessment must be conducted to evaluate potential threats and vulnerabilities to the confidentiality, integrity, and availability of sensitive, confidential, and proprietary electronic information Fleet stores, transmits, and/or processes.
- Risk assessments must be performed with any major change to Fleet's business or technical operations and/or supporting infrastructure no less than once per year.
- Strategies shall be developed to mitigate or accept the risks identified in the risk assessment process.
- The risk register is monitored quarterly to assess compliance with the above policy, and document newly discovered or created risks.
### Acceptable Risk Levels
@ -782,28 +658,16 @@ All other risks must be individually reviewed and managed.
Fleet policy requires that:
1. Fleet software engineering and product development are required to follow security best practices. The product should be "Secure by Design" and "Secure by Default."
2. Fleet performs quality assurance activities. This may include:
* Peer code reviews prior to merging new code into the main development branch
(e.g., main branch)
* Thorough product testing before releasing it to production (e.g., unit testing
and integration testing)
- Peer code reviews prior to merging new code into the main development branch (e.g., main branch)
- Thorough product testing before releasing it to production (e.g., unit testing and integration testing)
3. Risk assessment activities (i.e., threat modeling) must be performed for a new product or extensive changes to an existing product.
4. Security requirements must be defined, tracked, and implemented.
5. Security analysis must be performed for any open source software and/or third-party components and dependencies included in Fleet software products.
6. Static application security testing (SAST) must be performed throughout development and before each release.
7. Dynamic application security testing (DAST) must be performed before each release.
8. All critical or high severity security findings must be remediated before each release.
9. All critical or high severity vulnerabilities discovered post-release must be remediated in the next release or as per the Fleet vulnerability management policy SLAs, whichever is sooner.
10. Any exception to the remediation of a finding must be documented and approved by the security team or CTO.
## Security policy management policy
@ -814,29 +678,17 @@ Fleet policy requires that:
| @Jostableford | 2024-03-14 |
Fleet policy requires that:
1. Fleet policies must be developed and maintained to meet all applicable compliance requirements and adhere to security best practices, including but not limited to:
- SOC 2
2. Fleet must annually review all policies.
3. Fleet maintains all policy changes must be approved by Fleet's CTO or CEO. Additionally,
* Major changes may require approval by Fleet CEO or designee;
* Changes to policies and procedures related to product development may
require approval by the CTO.
3. Fleet maintains all policy documents with version control.
4. Policy exceptions are handled on a case-by-case basis.
* All exceptions must be fully documented with business purpose and reasons
why the policy requirement cannot be met.
* All policy exceptions must be approved by Fleet Head of Security and CEO.
* An exception must have an expiration date no longer than one year from date
of exception approval and it must be reviewed and re-evaluated on or before
the expiration date.
- Fleet policies must be developed and maintained to meet all applicable compliance requirements and adhere to security best practices, including but not limited to:
- SOC 2
- Fleet must annually review all policies.
- Fleet maintains all policy changes must be approved by Fleet's CTO or CEO. Additionally:
- Major changes may require approval by Fleet CEO or designee;
- Changes to policies and procedures related to product development may require approval by the CTO.
- Fleet maintains all policy documents with version control.
- Policy exceptions are handled on a case-by-case basis.
- All exceptions must be fully documented with business purpose and reasons why the policy requirement cannot be met.
- All policy exceptions must be approved by Fleet Head of Security and CEO.
- An exception must have an expiration date no longer than one year from date of exception approval and it must be reviewed and re-evaluated on or before the expiration date.
## Third-party management policy
> _Created from [JupiterOne/security-policy-templates](https://github.com/JupiterOne/security-policy-templates). [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/)_
@ -845,23 +697,16 @@ Fleet policy requires that:
| -------------- | -------------- |
| @mikermcneil | 2022-06-01 |
Fleet makes every effort to assure all third-party organizations are
compliant and do not compromise the integrity, security, and privacy of Fleet
or Fleet Customer data. Third Parties include Vendors, Customers, Partners,
Subcontractors, and Contracted Developers.
Fleet makes every effort to assure all third-party organizations are compliant and do not compromise the integrity, security, and privacy of Fleet or Fleet Customer data. Third Parties include Vendors, Customers, Partners, Subcontractors, and Contracted Developers.
1. A list of approved vendors/partners must be maintained and reviewed annually.
2. Approval from management, procurement, and security must be in place before onboarding any new vendor or contractor that impacts Fleet production systems. Additionally, all changes to existing contract agreements must be reviewed and approved before implementation.
3. For any technology solution that needs to be integrated with Fleet production environment or operations, the security team must perform a Vendor Technology Review to understand and approve the risk. Periodic compliance assessment and SLA review may be required.
4. Fleet Customers or Partners should not be allowed access outside of their own environment, meaning they cannot access, modify, or delete any data belonging to other third parties.
5. Additional vendor agreements are obtained as required by applicable regulatory compliance requirements.
- A list of approved vendors/partners must be maintained and reviewed annually.
- Approval from management, procurement, and security must be in place before onboarding any new vendor or contractor that impacts Fleet production systems. Additionally, all changes to existing contract agreements must be reviewed and approved before implementation.
- For any technology solution that needs to be integrated with Fleet production environment or operations, the security team must perform a Vendor Technology Review to understand and approve the risk. Periodic compliance assessment and SLA review may be required.
- Fleet Customers or Partners should not be allowed access outside of their own environment, meaning they cannot access, modify, or delete any data belonging to other third parties.
- Additional vendor agreements are obtained as required by applicable regulatory compliance requirements.
## Anti-corruption policy
> Fleet is committed to ethical business practices and compliance with the law. All Fleeties are required to comply with the "Foreign Corrup Practices Act" and anti-bribery laws and regulations in applicable jurisdictions including, but not limited to, the "UK Bribery Act 2010", "European Commission on Anti-Corruption" and others. The policies set forth in [this document](https://docs.google.com/document/d/16iHhLhAV0GS2mBrDKIBaIRe_pmXJrA1y7-gTWNxSR6c/edit?usp=sharing) go over Fleet's anti-corruption policy in detail.
<meta name="maintainedBy" value="hollidayn">
<meta name="maintainedBy" value="jostableford">
<meta name="title" value="📜 Security policies">

View file

@ -20,7 +20,7 @@ Please also see [Application security](https://fleetdm.com/docs/using-fleet/appl
Please also see ["Data security"](https://fleetdm.com/handbook/business-operations/security-policies#data-management-policy)
| Question | Answer |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Should the need arise during an active relationship, how can our Data be removed from the Fleet's environment? | Customer data is primarially stored in RDS, S3, and Cloudwatch logs. Deleting these resources will remove the vast majority of customer data. Fleet can take further steps to remove data on demand, including deleting individual records in monitoring systems if requested. |
| Should the need arise during an active relationship, how can our Data be removed from the Fleet's environment? | Customer data is primarily stored in RDS, S3, and Cloudwatch logs. Deleting these resources will remove the vast majority of customer data. Fleet can take further steps to remove data on demand, including deleting individual records in monitoring systems if requested. |
| Does Fleet support secure deletion (e.g., degaussing/cryptographic wiping) of archived and backed-up data as determined by the tenant? | Since all data is encrypted at rest, Fleet's secure deletion practice is to delete the encryption key. Fleet does not host customer services on-premise, so hardware specific deletion methods (such as degaussing) do not apply. |
| Does Fleet have a Data Loss Prevention (DLP) solution or compensating controls established to mitigate the risk of data leakage? | In addition to data controls enforced by Google Workspace on corporate endpoints, Fleet applies appropiate security controls for data depending on the requirements of the data, including but not limited to minimum access requirements. |
| Can your organization provide a certificate of data destruction if required? | No, physical media related to a certificate of data destruction is managed by AWS. Media storage devices used to store customer data are classified by AWS as critical and treated accordingly, as high impact, throughout their life-cycles. AWS has exacting standards on how to install, service, and eventually destroy the devices when they are no longer useful. When a storage device has reached the end of its useful life, AWS decommissions media using techniques detailed in NIST 800-88. Media that stored customer data is not removed from AWS control until it has been securely decommissioned. |
@ -38,7 +38,7 @@ Please also see ["Data security"](https://fleetdm.com/handbook/business-operatio
Please also see [Encryption and key management](https://fleetdm.com/handbook/business-operations/security-policies#encryption-policy)
| Question | Answer |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Does Fleet have a cryptographic key management process (generation, exchange, storage, safeguards, use, vetting, and replacement), that is documented and currently implemented, for all system components? (e.g. database, system, web, etc.) | All data is encrypted at rest using methods appropiate for the system (ie KMS for AWS based resources). Data going over the internet is encrypted using TLS or other appropiate transport security. |
| Does Fleet have a cryptographic key management process (generation, exchange, storage, safeguards, use, vetting, and replacement), that is documented and currently implemented, for all system components? (e.g. database, system, web, etc.) | All data is encrypted at rest using methods appropriate for the system (ie KMS for AWS based resources). Data going over the internet is encrypted using TLS or other appropiate transport security. |
| Does Fleet allow customers to bring and their own encryption keys? | By default, Fleet does not allow for this, but if absolutely required, Fleet can accommodate this request. |
| Does Fleet have policy regarding key rotation ? Does rotation happens after every fixed time period or only when there is evidence of key leak ? | TLS certificates are managed by AWS Certificate Manager and are rotated automatically annually. |

View file

@ -136,7 +136,7 @@ Fleet announces [support for Windows and Linux devices](https://fleetdm.com/anno
To provide clarity about decision-making, [responsibility](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility), and resources, everyone at Fleet has a manager, and [every manager](https://fleetdm.com/handbook/company#management) has direct reports. Fleet's organizational chart is accessible company-wide as a sub-tab in ["🧑‍🚀 Fleeties" (private google doc)](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0). On the other sub-tabs, you can also check out a world map of where everyone is located, hiring stats, and fun facts about each team member.
- 🔦 [Business Operations](https://fleetdm.com/handbook/business-operations): The Business Operations department is directly responsible for these traditional functions: People, Finance, tax, compliance, Legal, and IT.
- 🏹 [Customer Success](https://fleetdm.com/handbook/customer-success): 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.
- 🌦️ [Customer Success](https://fleetdm.com/handbook/customer-success): 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.
- 🐋 [Sales](https://fleetdm.com/handbook/sales): The Sales department is directly responsible for attaining the revenue goals of Fleet and helping customers deliver on their objectives.
- 🫧 [Demand](https://fleetdm.com/handbook/demand): The Demand department is directly responsible for growing awareness of Fleet and nurturing the community through participation in events, conversations, and other programs.
- 🚀 [Engineering](https://fleetdm.com/handbook/engineering): The Engineering department at Fleet is directly responsible for writing and maintaining the code for Fleet's core product.
@ -145,7 +145,7 @@ To provide clarity about decision-making, [responsibility](https://fleetdm.com/h
## Advisors
While most improvements at Fleet are driven by informal conversations with customers and open-source contributors, the company also has a few dozen advisors and investors, including
[Sid](https://about.gitlab.com/blog/2022/10/14/one-third-of-what-we-learned-about-ipos-in-taking-gitlab-public/) [Sijbrandij](https://about.gitlab.com/handbook/ceo/#sijbrandij-pronunciation-hint) _(GitLab)_, [Dylan Field](https://en.wikipedia.org/wiki/Dylan_Field) _(Figma)_, [Mike Arpaia](https://www.youtube.com/watch?v=zfCak2UIOD8) _(osquery)_, and [other smart people who are eager to help](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit). If you have a question for one of them, Fleet's CEO is happy to introduce you. ([Just ask](https://fleetdm.com/handbook/company/ceo).)
[Sid](https://about.gitlab.com/blog/2022/10/14/one-third-of-what-we-learned-about-ipos-in-taking-gitlab-public/) [Sijbrandij](https://about.gitlab.com/handbook/ceo/#sijbrandij-pronunciation-hint) _(GitLab)_, [Dylan Field](https://en.wikipedia.org/wiki/Dylan_Field) _(Figma)_, [Mike Arpaia](https://www.youtube.com/watch?v=zfCak2UIOD8) _(osquery)_, and [other smart people who are eager to help](https://docs.google.com/spreadsheets/d/15knBE2-PrQ1Ad-QcIk0mxCN-xFsATKK9hcifqrm0qFQ/edit). If you have a question for one of them, Fleet's CEO is happy to introduce you. ([Just ask](https://fleetdm.com/handbook/company/leadership#contact-the-ceo).)
<!--
#### Stubs

View file

@ -69,7 +69,7 @@ Fleet uses advertising to spread awareness through a broader audience and foster
### Events
It's important for Fleet to engage at events. This provides an opportunity to directly engage with potential users and contributors, build relationships, gather feedback, and create a stronger sense of community and trust.
It's important for Fleet to engage at [events](https://docs.google.com/spreadsheets/d/1YQXAX2Q_WnGkAwMYjMbQpV3nbCj7gOBbv7Y0u4twxzQ/edit#gid=1931288160). This provides an opportunity to directly engage with potential users and contributors, build relationships, gather feedback, and create a stronger sense of community and trust.
### Podcast
Fleet has created the [ExpedITioners podcast](https://expeditioners.podbean.com/) to open discussions and help IT and security professionals get ahead of the curve and prepare themselves and their organizations for what lies ahead.
@ -395,7 +395,7 @@ For more developed thoughts about __spending guidelines and limits__, please rea
### Brex
#### Non-travel purchases that exceed a Brex cardholder's limit
For non-travel purchases that would require an increase in the Brex cardholder's limit, please [make a request](https://fleetdm.com/handbook/business-operations#contact-us) with following information:
For non-travel purchases that would require an increase in the Brex cardholder's limit ($2,000 by default), please [make a request](https://fleetdm.com/handbook/business-operations#contact-us) with following information:
- The nature of the purchase (i.e. SaaS subscription and what it's used for)
- The cost of the purchase and whether it is a fixed or variable (i.e. use-based) cost.
- Whether it is a one time purchase or a recurring purchase and at what frequency the purchase will re-occur (annually, monthly, etc.)

View file

@ -36,31 +36,6 @@
# - 🛠️ Technical: You understand the software development processes. You understand that software quality matters.
# - 🟣 Openness: You are flexible and open to new ideas and ways of working.
# - Bonus: Cybersecurity or IT background.
- jobTitle: 🌐 Apprentice
department: 🌐 Digital Experience
hiringManagerName: Sam Pfluger
hiringManagerGithubUsername: sampfluger88
hiringManagerLinkedInUrl: https://www.linkedin.com/in/sampfluger88/
responsibilities: |
- 👥 Manage multiple calendars and schedules using Google Calendar and various forms of communication simultaneously.
- 🧑‍🔬 Perform executive assistance processes as described in [https://fleetdm.com/handbook/digital-experience](https://fleetdm.com/handbook/digital-experience).
- 📖 Maintain and update the structure and content of the company handbook.
- 🗣️ Act as secondary/backup point of contact for other departments for Digital Experience initiatives.
- 🗓️ Schedule travel arrangements for the CEO and other executives as needed.
- ✍️ Help implement and drive change management for any new or modified processes and tools across the team and/or the organization.
- 📣 Record and communicate relevant information and decisions to the Digital Experience team and other departments.
- 📈 Collect and report Digital Experience KPIs.
experience: |
- 🏃‍♂️ Strong desire to build a technical and operational-based skill set.
- 🚀 Detail-oriented, highly organized, and able to move quickly to solve complex problems using boring solutions.
- 🦉 Deep understanding of Google Suite (Gmail, Google Calendar, Google Sheets, Google Docs, etc.)
- 🫀 Experience dealing with sensitive personal information of team members and customers.
- 🛠️ Strong written and oral communication skills for general and technical topics.
- 💭 Capable of understanding and translating technical concepts and personas.
- 🤝 Ability to work in a process-driven team-based environment.
- 🟣 Openness: You are flexible and open to new ideas and ways of working.
- Bonus: Customer service/support background.
- jobTitle: 🚀 Quality Assurance Engineer
department: Engineering
hiringManagerName: George Karr

View file

@ -18,21 +18,27 @@
moreInfoUrl: https://play.goconsensus.com/s4e490bb9
buzzwords: [Device trust,Zero trust,Layer 7 device trust,Beyondcorp,Device attestation,Conditional access]
waysToUse:
- description: Create a calendar event and auto-remediate all failing policies when users are free. Coming soon (2024-04-01).
moreInfoUrl: https://github.com/fleetdm/fleet/issues/17230
- description: Automatically manage the behavior of endpoints that are at higher risk of vulnerabilities or data loss due to their configuration or patch level.
- description: Block access to corporate apps for users whose devices with unexpected settings, like disabled screen lock, passwords that are too short, unencrypted hard disks, and more
- description: Quickly implement conditional access based on device health using osquery and a simple device health REST API.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14920
- description: Control and restore access to applications by automatically restricting access when devices do not meet particular security requirements.
moreInfoUrl: https://duo.com/docs/device-health
- description: Control which laptop and desktop devices can access corporate apps and websites based on what vulnerabilities it might be exposed to based on how the device is configured, whether it's up to date, its MDM enrollment status, and anything else you can build in a SQL query of Fleet's 300 data tables representing information about enrolled host systems.
- description: Control which laptop and desktop devices can access corporate apps and websites based on what vulnerabilities it might be exposed to based on how the device is configured, whether it's up to date, its MDM enrollment status, and anything else you can build in a SQL query of Fleet's 300 data tables representing information about enrolled host systems. Coming soon (2024-06-31).
moreInfoUrl: https://github.com/fleetdm/fleet/issues/16236
- description: Implement multivariate device trust
moreInfoUrl: https://youtu.be/5sFOdpMLXQg?feature=shared&t=1445
- description: Implement your own version of Google's zero trust model (BeyondCorp)
moreInfoUrl: https://cloud.google.com/beyondcorp
- description: Get endpoint data into ServiceNow and make your asset management teams happy
moreInfoUrl: https://www.youtube.com/watch?v=aVbU6_9JoM0
- industryName: Maintenance windows
friendlyName: Fleet in your calendar
description: Create a calendar event to auto-remediate failing policies when your end users are free. Coming soon (2024-04-01)
documentationUrl: https://github.com/fleetdm/fleet/issues/17230
tier: Premium
productCategories: [Endpoint operations]
pricingTableCategories: [Endpoint operations]
#
# ╔═╗╔═╗╦═╗╦╔═╗╔╦╗ ╔═╗═╗ ╦╔═╗╔═╗╦ ╦╔╦╗╦╔═╗╔╗╔
# ╚═╗║ ╠╦╝║╠═╝ ║ ║╣ ╔╩╦╝║╣ ║ ║ ║ ║ ║║ ║║║║
@ -435,8 +441,7 @@
- industryName: GitOps
friendlyName: Manage endpoints in git
documentationUrl: https://github.com/fleetdm/fleet-gitops
description: Fork the best practices repo and use the GitHub Action to hook it up to your Fleet instance in minutes. Coming soon (2024-03-31)
moreInfoUrl: https://github.com/fleetdm/fleet/issues/13643
description: Fork the best practices repo and use the GitHub Action to hook it up to your Fleet instance in minutes.
productCategories: [Endpoint operations,Device management,Vulnerability management]
pricingTableCategories: [Deployment]
usualDepartment: IT
@ -552,6 +557,7 @@
- description: Target profiles to specific hosts using SQL.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14715
- description: Automatically re-deploy configuration profiles when they're not installed.
- description: Deploy configuration profiles on iOS/iPadOS. Coming soon (2024-06-31).
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Self service
@ -567,7 +573,7 @@
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Low-level MDM commands for macOS and Windows (e.g. remote restart)
- industryName: Low-level MDM commands for macOS, iOS/iPadOS*, and Windows (e.g. remote restart)
documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-commands
tier: Free
usualDepartment: IT
@ -576,6 +582,7 @@
waysToUse:
- description: See a list of the upcoming MDM commands and scripts in unified queue. Coming soon (2024-03-31)
moreInfoUrl: https://github.com/fleetdm/fleet/issues/15920
- description: MDM commands for iOS/iPadOS are coming soon (2024-06-31).
- industryName: Native macOS update reminders
description: Send low-level MDM commands to tell end users to update their OS.
moreInfoUrl: https://developer.apple.com/documentation/devicemanagement/schedule_an_os_update
@ -583,13 +590,14 @@
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Zero-touch setup for macOS computers
- industryName: Zero-touch setup for macOS, iOS/iPadOS*, and Windows
documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience
tier: Premium
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
waysToUse:
- description: Zero-touch for iOS/iPadOS is coming soon (2024-06-31).
- description: Ship a macOS workstation to the end user's home and have them automatically enroll to Fleet during out-of-the-box setup.
- description: Ship a Windows workstation to the end user's home and have them automatically enroll to Fleet during out-of-the-box setup.
- description: Customize the out-of-the-box setup experience for your end users.
@ -602,13 +610,15 @@
pricingTableCategories: [Device management]
waysToUse:
- description: Enforce macOS updates via Nudge.
- description: Progressively enhance from Nudge to DDM-based OS updates. Coming soon (2024-03-31).
moreInfoUrl: https://github.com/fleetdm/fleet/issues/17295
- description: Automatically update Windows after the end user reaches a deadline.
- industryName: Cross-platform remote lock and wipe
documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-commands
waysToUse:
- description: High-level remote lock for macOS, Windows, and Linux. Coming soon (2024-03-31)
- description: High-level remote lock for macOS, Windows, and Linux.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/9949
- description: High-level remote wipe for macOS, Windows, and Linux. Coming soon (2024-03-31)
- description: High-level remote wipe for macOS, Windows, and Linux.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/9951
tier: Premium
usualDepartment: IT
@ -616,13 +626,34 @@
pricingTableCategories: [Device management]
- industryName: Deploy security agents on macOS, Windows, and Linux computers.
documentationUrl: https://github.com/fleetdm/fleet/issues/14921
description:
description: Easily configure and install SentinelOne, Crowdstrike, and other security tools.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14921
tier: Premium
comingSoonOn: 2024-04-22 #customer-reedtimmer,customer-flacourtia
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Deploy and update apps on macOS, Windows, and Linux computers.
description: Install Zoom, Slack, Chrome, and other apps without interrupting your end users.
tier: Premium
comingSoonOn: 2024-06-31
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Self-service portal for users to install apps
description: Add optional apps for your end users through Fleet Desktop (for macOS, Windows, and Linux).
tier: Premium
comingSoonOn: 2024-06-31
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: License apps on macOS and iOS/iPadOS through Volume Purchasing Program (VPP).
description: Offer licenses for Photoshop and other App Sore apps for your end users.
tier: Premium
comingSoonOn: 2024-06-31
usualDepartment: IT
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Puppet module
documentationUrl: https://fleetdm.com/docs/using-fleet/puppet-module
friendlyName: Map macOS settings to computers with Puppet module
@ -756,11 +787,11 @@
waysToUse:
- description: Automatically set admin access to Fleet based on your IDP
- industryName: Trigger a workflow based on a failing policy
documentationUrl: https://fleetdm.com/docs/using-fleet/automations#automations
documentationUrl: https://fleetdm.com/docs/using-fleet/automations#policy-automations
productCategories: [Endpoint operations,Device management]
pricingTableCategories: [Integrations]
usualDepartment: IT
tier: Premium
tier: Free
- industryName: Role-based access control
documentationUrl: https://fleetdm.com/docs/using-fleet/manage-access#manage-access
productCategories: [Endpoint operations,Device management,Vulnerability management]
@ -797,9 +828,9 @@
demos:
- description: See a list of all vulneribilities across your hosts. Coming soon (2024-03-31)
moreInfoUrl: https://github.com/fleetdm/fleet/issues/15919
- description: AI generated CVSS v4 context. Coming soon (2024-03-31)
- description: AI generated CVSS v4 context. Coming soon (2024-12-31).
waysToUse:
- description: Easily communicate to executives regarding the progress of patching vulnerable software. Only show vulnerabilities that you care about. Coming soon (2024-03-31) #Customer-faltona and customer-rialto
- description: Easily communicate to executives regarding the progress of patching vulnerable software. Only show vulnerabilities that you care about. Coming soon (2024-12-31) #Customer-faltona and customer-rialto
# ╦ ╦╦ ╦╦ ╔╗╔╔═╗╦═╗╔═╗╔╗ ╦╦ ╦╔╦╗╦ ╦ ╔═╗╔═╗╔═╗╦═╗╔═╗╔═╗ ╔═╗╔═╗╔═╗╔═╗ ╔═╗╔╗╔╔╦╗ ╔═╗╦ ╦╔═╗╔═╗
# ╚╗╔╝║ ║║ ║║║║╣ ╠╦╝╠═╣╠╩╗║║ ║ ║ ╚╦╝ ╚═╗║ ║ ║╠╦╝║╣ ╚═╗ ─── ║╣ ╠═╝╚═╗╚═╗ ╠═╣║║║ ║║ ║ ╚╗╔╝╚═╗╚═╗
# ╚╝ ╚═╝╩═╝╝╚╝╚═╝╩╚═╩ ╩╚═╝╩╩═╝╩ ╩ ╩ ╚═╝╚═╝╚═╝╩╚═╚═╝╚═╝ ╚═╝╩ ╚═╝╚═╝ ╩ ╩╝╚╝═╩╝ ╚═╝ ╚╝ ╚═╝╚═╝

View file

@ -109,4 +109,4 @@ The following stubs are included only to make links backward compatible.
Please see [Handbook/customer-success#respond-to-messages-and-alerts](https://www.fleetdm.com/handbook/customer-success#respond-to-messages-and-alerts)
<meta name="maintainedBy" value="zayhanlon">
<meta name="title" value="🏹 Customer Success">
<meta name="title" value="🌦️ Customer Success">

View file

@ -32,6 +32,20 @@ Each PR to the website is manually checked for quality and tested before before
3. Check the change in relation to all breakpoints and [browser compatibility](https://fleetdm.com/handbook/digital-experience#check-browser-compatibility-for-fleetdm-com), Tests are carried out on [supported browsers](https://fleetdm.com/docs/using-fleet/supported-browsers) before website changes go live.
### Update the host count of a premium subscription
When a self-service license dispenser customer reaches out to upgrade a license via the contact form, a member of the [Demand department](https://fleetdm.com/handbook/demand) will create a confidential issue detailing the request and add it to the new requests column of Ditigal Experience kanban board. A member of this team will then log into Stripe using the shared login, and upgrade the customer's subscription.
To update the host count on a user's subscription:
1. Log in to the [Stripe dashboard](https://dashboard.stripe.com/dashboard) and search for the customer's email address.
2. Click on their subscription and select the "Update subscription" option in the "Actions" dropdown
3. Update the quantity of the user's subscription to be their desired host count.
4. Turn the "Proration charges" option on and select the "Charge proration amount immediately" option.
5. Under "Payment" select "Email invoice to the customer", and set the payment due date to be 15 days, and make sure the "Invoice payment page" option is checked.
6. Select "Update subscription" to send the user an updated invoice for their subscription. Once the customer pays their new invoice, the Fleet website will update the user's subscription and generate a new Fleet Premium license with an updated host count.
7. Let the person who created the request know what actions were taken so they can communicate them to the customer.
### Test fleetdm.com locally
When making changes to the Fleet website, you can test your changes by running the website locally. To do this, you'll need the following:
@ -152,33 +166,6 @@ 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.
### Grant role-specific license to a team member (RevOps)
Certain new team members, especially in go-to-market (GTM) roles, will need paid access to paid tools like Salesforce and LinkedIn Sales Navigator immediately on their first day with the company. Gong licenses that other departments need may [request them from BizOps](https://fleetdm.com/handbook/business-operations#contact-us) and we will make sure there is no license redundancy in that department. The table below can be used to determine which paid licenses they will need, based on their role:
| Role | Salesforce CRM | Salesforce "Inbox" | LinkedIn _(paid)_ | Gong _(paid)_ | Zoom _(paid)_|
|:-----------------|:---|:---|:----|:---|:---|
| 🐋 AE | ✅ | ✅ | ✅ | ✅ | ✅
| 🐋 CSM | ✅ | ✅ | ❌ | ✅ | ✅
| 🐋 SC | ✅ | ✅ | ❌ | ❌ | ✅
| ⚗️ PM | ❌ | ❌ | ❌ | ✅ | ✅
| ⚗️ PD | ❌ | ❌ | ❌ | ✅ | ✅
| 🔦 CEO | ✅ | ✅ | ✅ | ✅ | ✅
| Other roles | ❌ | ❌ | ❌ | ❌ | ❌
> **Warning:** Do NOT buy LinkedIn Recruiter. AEs and SDRs should use their personal Brex card to purchase the monthly [Core Sales Navigator](https://business.linkedin.com/sales-solutions/compare-plans) plan. Fleet does not use a company wide Sales Navigator account. The goal of Sales Navigator is to access to profile views and data, not InMail. Fleet does not send InMail.
### Add a seat to Salesforce
Here are the steps we take to grant appropriate Salesforce licenses to a new hire:
- Go to ["My Account"](https://fleetdm.lightning.force.com/lightning/n/standard-OnlineSalesHome).
- View contracts -> pick current contract.
- Add the desired number of licenses.
- Sign DocuSign sent to the email.
- The order will be processed in ~30m.
- Once the basic license has been added, you can create a new user using the new team member's `@fleetdm.com` email and assign a license to it.
- To also assign a user an "Inbox license", go to the ["Setup" page](https://fleetdm.lightning.force.com/lightning/setup/SetupOneHome/home) and select "User > Permission sets". Find the [inbox permission set](https://fleetdm.lightning.force.com/lightning/setup/PermSets/page?address=%2F005%3Fid%3D0PS4x000002uUn2%26isUserEntityOverride%3D1%26SetupNode%3DPermSets%26sfdcIFrameOrigin%3Dhttps%253A%252F%252Ffleetdm.lightning.force.com%26clc%3D1) and assign it to the new team member.
### 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.

View file

@ -48,16 +48,6 @@
description: "Prepare the CEO office minutes calendar event and meeting agenda"
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#prepare-for-ceo-office-minutes"
dri: "sampfluger88"
-
task: "Revenue report" # TODO tie this to a responsibility
startedOn: "2024-02-12"
frequency: "Weekly"
description: "At the start of every week, check the Salesforce reports for past due invoices, non-invoiced opportunities, and past due renewals. Report any findings to in the `#g-sales` channel by mentioning Alex Mitchell and Mike McNeil."
moreInfoUrl:
dri: "hughestaylor"
autoIssue:
labels: [ "#g-digital-experience" ]
repo: "confidential"
-
task: "Refresh event calendar"
startedOn: "2023-12-31"
@ -96,13 +86,6 @@
description: "Process the CEO's calendar"
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#process-the-ceos-calendar"
dri: "sampfluger88"
-
task: "Fleet IT warehouse management"
startedOn: "2023-07-29"
frequency: "Weekly"
description: "Fleet IT warehouse management"
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#fleet-it-warehouse-management"
dri: "sampfluger88"
-
task: "Send weekly update"
startedOn: "2023-09-15"

View file

@ -38,23 +38,14 @@ What happens next? The engineering output and architecture DRI reviews engineeri
If there are product changes (i.e. interface, documentation, or dependency changes), the story is added to the "New requests" column on the drafting board.
If there are no product changes, and the DRI decides to prioritize the story, the story is added to the "Specified" column on drafting board so that it can be estimated.
If there are no product changes, and the DRI decides to prioritize the story, the story is added to the "Specified" column on the drafting board so that it can be estimated.
> We prefer the term engineering-initiated stories over technical debt because the user story format helps keep us focused on our users and contributors.
### Manage release branches
Every three weeks, release a minor version of Fleet from the `main` branch.
Every week between minor releases, release a patch release with fixes for released bugs.
The DRI for publishing a release is responsible for creating the next patch branch. Create a patch branch off the latest tagged release of Fleet. Submit PRs for released bug fixes directly to the target patch branch to avoid merge conflicts later in the release cycle. After merging into the patch branch, submit another PR to `main` containing the same fix and resolve any merge conflicts.
> It is the responsibility of the person merging the fix into the patch branch to make sure the fix is also merged into `main`.
### Fix a bug
If the bug is labeled `~unreleased bug`, branch off and put your PR into `main`.
If the bug is labeled `~unreleased bug`, branch off and put your PR into `main`. These issues can be closed as soon as they complete QA.
If the bug is labeled `~released bug`, branch off and put your PR into the upcoming patch branch `patch-fleet-v4.x.x`. If you are unsure which branch to use, confirm with your manager. Don't forget to also submit a second PR to `main` after the bug is confirmed fixed.
If the bug is labeled `~released bug`, branch off the tag for the latest release of Fleet and put your PR into `main`. For example, `git checkout fleet-v4.48.2`, then `git checkout -b my-bug-fix-branch`. These issues are not closed until the next release of Fleet. This approach makes sure the bug fix is not built on top of unreleased feature code, which can cause merge conflicts during patch releases.
### Begin a merge freeze
To ensure release quality, Fleet has a freeze period for testing beginning the Tuesday before the release at 9:00 AM Pacific. Effective at the start of the freeze period, new feature work will not be merged into `main`.

View file

@ -122,12 +122,6 @@ module "ses-free" {
domain = "free.fleetdm.com"
}
module "waf-free" {
source = "github.com/fleetdm/fleet//terraform/addons/waf-alb?ref=tf-mod-addon-waf-alb-v1.0.0"
name = local.customer_free
lb_arn = module.free.byo-db.alb.lb_arn
}
module "migrations_free" {
depends_on = [
module.geolite2

View file

@ -397,12 +397,6 @@ module "ses" {
domain = "dogfood.fleetdm.com"
}
module "waf" {
source = "github.com/fleetdm/fleet//terraform/addons/waf-alb?ref=tf-mod-addon-waf-alb-v1.0.0"
name = local.customer
lb_arn = module.main.byo-vpc.byo-db.alb.lb_arn
}
# module "saml_auth_proxy" {
# # source = "github.com/fleetdm/fleet//terraform/addons/saml-auth-proxy?ref=main"
# # public_alb_security_group_id = module.main.byo-vpc.byo-db.alb.security_group_id

View file

@ -0,0 +1,5 @@
- name: Collect software permissions (system)
description: "Research for #16899"
query: SELECT * from tcc_system;
interval: 3600 # 1 hour
platform: darwin

View file

@ -0,0 +1,5 @@
- name: Collect software permissions (user)
description: "Research for #16899"
query: SELECT * from tcc_user;
interval: 3600 # 1 hour
platform: darwin

View file

@ -21,4 +21,5 @@
critical: false
description: This policy checks if the end user is required to enter a password, with at least 10 characters, to unlock the host.
resolution: "As an IT admin, deploy a Windows profile with the DevicePasswordEnabled and MinDevicePasswordLength option documented here: https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-devicelock"
platform: windows
platform: windows

View file

@ -9,7 +9,49 @@ team_settings:
secrets:
- secret: $DOGFOOD_WORKSTATIONS_CANARY_ENROLL_SECRET
agent_options:
path: ../lib/agent-options.yml
config:
decorators:
load:
- SELECT uuid AS host_uuid FROM system_info;
- SELECT hostname AS hostname FROM system_info;
options:
disable_distributed: false
distributed_interval: 10
distributed_plugin: tls
distributed_tls_max_attempts: 3
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
overrides:
platforms:
darwin:
auto_table_construction:
tcc_system:
path: /Library/Application Support/com.apple.TCC/TCC.db
query: 'select service, client, client_type, auth_value, auth_reason, policy_id, indirect_object_identifier, indirect_object_identifier_type, last_modified from access'
columns:
- service
- client
- client_type
- auth_value
- auth_reason
- policy_id
- indirect_object_identifier
- indirect_object_identifier_type
- last_modified
tcc_user:
path: /Users/%/Library/Application Support/com.apple.TCC/TCC.db
query: 'select service, client, client_type, auth_value, auth_reason, policy_id, indirect_object_identifier, indirect_object_identifier_type, last_modified from access'
columns:
- service
- client
- client_type
- auth_value
- auth_reason
- policy_id
- indirect_object_identifier
- indirect_object_identifier_type
- last_modified
controls:
enable_disk_encryption: true
macos_settings:
@ -68,3 +110,5 @@ queries:
- path: ../lib/collect-failed-login-attempts.queries.yml
- path: ../lib/collect-usb-devices.queries.yml
- path: ../lib/collect-vs-code-extensions.queries.yml
- path: ../lib/collect-software-permissions-system.queries.yml
- path: ../lib/collect-software-permissions-user.queries.yml

View file

@ -14,7 +14,7 @@
{
"name": "uid",
"description": "[User ID](https://superuser.com/a/1108201)",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -70,7 +70,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -572,7 +571,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "type",
@ -1800,7 +1798,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -2461,7 +2458,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "tid",
@ -2630,7 +2626,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "tid",
@ -3626,7 +3621,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -3970,7 +3964,7 @@
{
"name": "path",
"description": "Path to extension folder. Defaults to '' on ChromeOS",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -4067,7 +4061,7 @@
{
"name": "state",
"description": "1 if this extension is enabled",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -4167,7 +4161,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -4746,7 +4739,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "device_id",
@ -5975,7 +5967,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -6113,7 +6104,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "variable",
@ -6157,7 +6147,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "device",
@ -6351,7 +6340,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "device",
@ -6422,7 +6410,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "device",
@ -6805,7 +6792,7 @@
{
"name": "type",
"description": "The interface type of the disk.",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -6814,7 +6801,7 @@
{
"name": "id",
"description": "The unique identifier of the drive on the system.",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -6835,7 +6822,7 @@
{
"name": "disk_size",
"description": "Size of the disk.",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -6868,7 +6855,7 @@
{
"name": "name",
"description": "The label of the disk object.",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -9350,7 +9337,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "version",
@ -9609,7 +9595,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "version",
@ -9773,7 +9758,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -9948,7 +9932,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -10059,7 +10042,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "fan",
@ -10492,7 +10474,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "target_path",
@ -11099,7 +11080,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -11321,7 +11301,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "action",
@ -11582,7 +11561,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "version",
@ -11643,7 +11621,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "boot_uuid",
@ -11714,7 +11691,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -11766,7 +11742,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "version",
@ -12578,7 +12553,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "filter_name",
@ -12984,7 +12958,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -13775,7 +13748,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "label",
@ -15355,7 +15327,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -15485,7 +15456,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "device_name",
@ -15780,7 +15750,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "md_device_name",
@ -15832,7 +15801,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -16078,7 +16046,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -16140,7 +16107,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -16220,7 +16186,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -16310,7 +16275,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -16507,7 +16471,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -16595,7 +16558,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "memory_total",
@ -16701,7 +16663,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -16859,7 +16820,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "processor_number",
@ -17344,7 +17304,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -17405,7 +17364,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "action",
@ -17600,7 +17558,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "handle",
@ -18844,7 +18801,7 @@
{
"name": "uid",
"description": "User ID for the policy. Returns `-1` if the policy applies to all users.",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -18853,7 +18810,7 @@
{
"name": "policy_identifier",
"description": "Policy identifier, such as `ProfilePayload:1d33ef8c-da1c-4534-8458-95a4d43d849e:minLength`.",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -18862,7 +18819,7 @@
{
"name": "policy_content",
"description": "Policy content, such as `policyAttributePassword matches '.{10,}'`.",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -18871,7 +18828,7 @@
{
"name": "policy_description",
"description": "Policy description, such as `Contain at least 10 characters.`",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -19141,7 +19098,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -19512,7 +19468,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "package",
@ -19573,7 +19528,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "package",
@ -19661,7 +19615,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "package",
@ -20419,7 +20372,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "pid",
@ -20731,12 +20683,11 @@
"evented": true,
"cacheable": false,
"notes": "This table will only include events for changes and files in directories that existed before the fleetd agent starts.",
"examples": [],
"columns": [
{
"name": "operation",
"description": "Operation type",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20745,7 +20696,7 @@
{
"name": "pid",
"description": "Process ID",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -20754,7 +20705,7 @@
{
"name": "ppid",
"description": "Parent process ID",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -20763,7 +20714,7 @@
{
"name": "time",
"description": "Time of execution in UNIX time",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -20772,7 +20723,7 @@
{
"name": "executable",
"description": "The executable path",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20781,7 +20732,7 @@
{
"name": "partial",
"description": "True if this is a partial event (i.e.: this process existed before we started osquery)",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20790,7 +20741,7 @@
{
"name": "cwd",
"description": "The current working directory of the process",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20799,7 +20750,7 @@
{
"name": "path",
"description": "The path associated with the event",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20808,7 +20759,7 @@
{
"name": "dest_path",
"description": "The canonical path associated with the event",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20817,7 +20768,7 @@
{
"name": "uid",
"description": "The uid of the process performing the action",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20826,7 +20777,7 @@
{
"name": "gid",
"description": "The gid of the process performing the action",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20835,7 +20786,7 @@
{
"name": "auid",
"description": "Audit user ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20844,7 +20795,7 @@
{
"name": "euid",
"description": "Effective user ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20853,7 +20804,7 @@
{
"name": "egid",
"description": "Effective group ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20862,7 +20813,7 @@
{
"name": "fsuid",
"description": "Filesystem user ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20871,7 +20822,7 @@
{
"name": "fsgid",
"description": "Filesystem group ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20880,7 +20831,7 @@
{
"name": "suid",
"description": "Saved user ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20889,7 +20840,7 @@
{
"name": "sgid",
"description": "Saved group ID of the process using the file",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -20898,7 +20849,7 @@
{
"name": "uptime",
"description": "Time of execution in system uptime",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -20907,7 +20858,7 @@
{
"name": "eid",
"description": "Event ID",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": true,
"required": false,
@ -21881,7 +21832,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "target_name",
@ -22332,7 +22282,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "path",
@ -22637,7 +22586,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "package",
@ -22716,7 +22664,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "name",
@ -23029,7 +22976,6 @@
"evented": false,
"cacheable": true,
"notes": "",
"examples": [],
"columns": [
{
"name": "label",
@ -23206,7 +23152,6 @@
"evented": false,
"cacheable": false,
"notes": "- For macOS, this only fetches results for osquery's current logged-in user context. The user must also have recently logged in.\n- For ChromeOS, this table requires the [fleetd Chrome extension](https://fleetdm.com/docs/using-fleet/chromeos).\n- For ChromeOS, this table is only available for Chrome 73+.\n",
"examples": [],
"columns": [
{
"name": "enabled",
@ -23239,7 +23184,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "time",
@ -23477,7 +23421,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "minimum_password_age",
@ -23700,7 +23643,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "type",
@ -24067,7 +24009,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "shmid",
@ -24895,7 +24836,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "action",
@ -25343,7 +25283,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "time",
@ -25617,7 +25556,7 @@
{
"name": "hostname",
"description": "Network hostname including domain. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25635,7 +25574,7 @@
{
"name": "cpu_type",
"description": "CPU type",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25658,7 +25597,7 @@
{
"name": "cpu_brand",
"description": "CPU brand string, contains vendor and model",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25718,7 +25657,7 @@
{
"name": "physical_memory",
"description": "Total physical memory in bytes",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25727,7 +25666,7 @@
{
"name": "hardware_vendor",
"description": "Hardware vendor. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25736,7 +25675,7 @@
{
"name": "hardware_model",
"description": "Hardware model. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25759,7 +25698,7 @@
{
"name": "hardware_serial",
"description": "The device's serial number. For ChromeOS, this is only available if the extension was force-installed by an enterprise policy",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25824,7 +25763,7 @@
{
"name": "computer_name",
"description": "Friendly computer name (optional). For ChromeOS, if the extension wasn't force-installed by an enterprise policy this will default to 'ChromeOS' only",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -25877,7 +25816,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "id",
@ -26787,7 +26725,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "uid",
@ -26904,7 +26841,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "uid",
@ -26938,7 +26874,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "time",
@ -27273,7 +27208,6 @@
"evented": false,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "color_depth",
@ -27581,7 +27515,7 @@
{
"name": "name",
"description": "Extension Name",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27590,7 +27524,7 @@
{
"name": "uuid",
"description": "Extension UUID",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27599,7 +27533,7 @@
{
"name": "version",
"description": "Extension version",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27608,7 +27542,7 @@
{
"name": "path",
"description": "Extension path",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27617,7 +27551,7 @@
{
"name": "publisher",
"description": "Publisher Name",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27626,7 +27560,7 @@
{
"name": "publisher_id",
"description": "Publisher ID",
"type": "TEXT",
"type": "text",
"notes": "",
"hidden": false,
"required": false,
@ -27635,7 +27569,7 @@
{
"name": "installed_at",
"description": "Installed Timestamp",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -27644,7 +27578,7 @@
{
"name": "prerelease",
"description": "Pre release version",
"type": "INTEGER",
"type": "integer",
"notes": "",
"hidden": false,
"required": false,
@ -27653,7 +27587,7 @@
{
"name": "uid",
"description": "The local user that owns the plugin",
"type": "BIGINT",
"type": "bigint",
"notes": "",
"hidden": false,
"required": false,
@ -29831,7 +29765,6 @@
"evented": true,
"cacheable": false,
"notes": "",
"examples": [],
"columns": [
{
"name": "target_path",

View file

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
@ -269,6 +270,7 @@ func (ds *Datastore) DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Conte
}
func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error {
// TODO(roberto): this seems confusing to me, we should have a separate datastore method.
if strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
return ds.deleteMDMAppleDeclaration(ctx, profileUUID)
}
@ -322,7 +324,6 @@ func (ds *Datastore) DeleteMDMAppleConfigProfileByTeamAndIdentifier(ctx context.
teamID = ptr.Uint(0)
}
// TODO: add deletion of declarations here or separate method?
res, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles WHERE team_id = ? AND identifier = ?`, teamID, profileIdentifier)
if err != nil {
return ctxerr.Wrap(ctx, err)
@ -3688,10 +3689,10 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) {
const stmt = `
SELECT
md5((count(0) + group_concat(hex(mad.checksum)
COALESCE(MD5((count(0) + GROUP_CONCAT(HEX(mad.checksum)
ORDER BY
mad.uploaded_at DESC separator ''))) AS checksum,
max(mad.created_at) AS latest_created_timestamp
mad.uploaded_at DESC separator ''))), '') AS checksum,
COALESCE(MAX(mad.created_at), NOW()) AS latest_created_timestamp
FROM
host_mdm_apple_declarations hmad
JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid
@ -3947,10 +3948,12 @@ ON DUPLICATE KEY UPDATE
return ctxerr.Wrap(ctx, err, "updating host declarations")
}
func (ds *Datastore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error {
func (ds *Datastore) MDMAppleSetPendingDeclarationsAs(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
stmt := `
UPDATE host_mdm_apple_declarations
SET status = ?
SET
status = ?,
detail = ?
WHERE
operation_type = ?
AND status = ?
@ -3958,13 +3961,16 @@ func (ds *Datastore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hos
`
_, err := ds.writer(ctx).ExecContext(
ctx, stmt, fleet.MDMDeliveryVerifying,
ctx, stmt,
// SET ...
status, detail,
// WHERE ...
fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending, hostUUID,
)
return ctxerr.Wrap(ctx, err, "updating host declaration status to verifying")
}
func (ds *Datastore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType, rawJSON string) error {
func (ds *Datastore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType string, rawJSON json.RawMessage) error {
const stmt = `
INSERT INTO
mdm_apple_declarative_requests (

View file

@ -69,6 +69,8 @@ func TestMDMApple(t *testing.T) {
{"TestMDMAppleDeleteHostDEPAssignments", testMDMAppleDeleteHostDEPAssignments},
{"LockUnlockWipeMacOS", testLockUnlockWipeMacOS},
{"ScreenDEPAssignProfileSerialsForCooldown", testScreenDEPAssignProfileSerialsForCooldown},
{"MDMAppleDDMDeclarationsToken", testMDMAppleDDMDeclarationsToken},
{"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs},
}
for _, c := range cases {
@ -4674,6 +4676,116 @@ func testScreenDEPAssignProfileSerialsForCooldown(t *testing.T, ds *Datastore) {
require.Empty(t, assign)
}
func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
ctx := context.Background()
toks, err := ds.MDMAppleDDMDeclarationsToken(ctx, "not-exists")
require.NoError(t, err)
require.Empty(t, toks.DeclarationsToken)
decl, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
Identifier: "decl-1",
Name: "decl-1",
})
require.NoError(t, err)
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
require.NoError(t, err)
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, "not-exists")
require.NoError(t, err)
require.Empty(t, toks.DeclarationsToken)
require.NotZero(t, toks.Timestamp)
host1, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
TeamID: nil,
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host1, true)
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
require.NoError(t, err)
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
require.NoError(t, err)
require.NotEmpty(t, toks.DeclarationsToken)
require.NotZero(t, toks.Timestamp)
oldTok := toks.DeclarationsToken
decl2, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
Identifier: "decl-2",
Name: "decl-2",
})
require.NoError(t, err)
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
require.NoError(t, err)
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
require.NoError(t, err)
require.NotEmpty(t, toks.DeclarationsToken)
require.NotZero(t, toks.Timestamp)
require.NotEqual(t, oldTok, toks.DeclarationsToken)
oldTok = toks.DeclarationsToken
err = ds.DeleteMDMAppleConfigProfile(ctx, decl.DeclarationUUID)
require.NoError(t, err)
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
require.NoError(t, err)
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
require.NoError(t, err)
require.NotEmpty(t, toks.DeclarationsToken)
require.NotZero(t, toks.Timestamp)
require.NotEqual(t, oldTok, toks.DeclarationsToken)
}
func testMDMAppleSetPendingDeclarationsAs(t *testing.T, ds *Datastore) {
ctx := context.Background()
for i := 0; i < 10; i++ {
_, err := ds.NewMDMAppleDeclaration(ctx, &fleet.MDMAppleDeclaration{
Identifier: fmt.Sprintf("decl-%d", i),
Name: fmt.Sprintf("decl-%d", i),
})
require.NoError(t, err)
}
checkStatus := func(declarations []fleet.HostMDMAppleProfile, wantStatus fleet.MDMDeliveryStatus, wantDetail string) {
for _, d := range declarations {
require.Equal(t, &wantStatus, d.Status)
require.Equal(t, wantDetail, d.Detail)
}
}
h, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
TeamID: nil,
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, h, true)
uuids, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
require.NoError(t, err)
require.Equal(t, h.UUID, uuids[0])
profs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Len(t, profs, 10)
checkStatus(profs, fleet.MDMDeliveryPending, "")
err = ds.MDMAppleSetPendingDeclarationsAs(ctx, h.UUID, &fleet.MDMDeliveryFailed, "mock error")
require.NoError(t, err)
profs, err = ds.GetHostMDMAppleProfiles(ctx, h.UUID)
require.NoError(t, err)
checkStatus(profs, fleet.MDMDeliveryFailed, "mock error")
}
func TestMDMAppleProfileVerification(t *testing.T) {
ds := CreateMySQLDS(t)
ctx := context.Background()

View file

@ -521,6 +521,7 @@ var additionalHostRefsByUUID = map[string]string{
"host_mdm_apple_profiles": "host_uuid",
"host_mdm_apple_bootstrap_packages": "host_uuid",
"host_mdm_windows_profiles": "host_uuid",
"host_mdm_apple_declarations": "host_uuid",
}
func (ds *Datastore) DeleteHost(ctx context.Context, hid uint) error {

View file

@ -6558,6 +6558,12 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
`, host.UUID)
require.NoError(t, err)
_, err = ds.writer(context.Background()).Exec(`
INSERT INTO host_mdm_apple_declarations (host_uuid, declaration_uuid)
VALUES (?, uuid())
`, host.UUID)
require.NoError(t, err)
err = ds.NewActivity( // automatically creates the host_activities entry
context.Background(),
user1,

View file

@ -633,17 +633,20 @@ WHERE
func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
if host == nil {
return nil, errors.New("host cannot be nil")
return nil, ctxerr.New(ctx, "cannot get bitlocker status for nil host")
}
if host.Platform != "windows" {
// Generally, the caller should have already checked this, but just in case we log and
// return nil
level.Debug(ds.logger).Log("msg", "cannot get bitlocker status for non-windows host", "host_id", host.ID)
return nil, nil
// the caller should have already checked this
return nil, ctxerr.Errorf(ctx, "cannot get bitlocker status for non-windows host %d", host.ID)
}
if host.MDMInfo != nil && host.MDMInfo.IsServer {
if host.MDMInfo == nil {
// the caller should have already checked
return nil, ctxerr.Errorf(ctx, "cannot get bitlocker status because no mdm info for host %d", host.ID)
}
if host.MDMInfo.IsServer {
// It is currently expected that server hosts do not have a bitlocker status so we can skip
// the query and return nil. We log for potential debugging in case this changes in the future.
level.Debug(ds.logger).Log("msg", "no bitlocker status for server host", "host_id", host.ID)
@ -700,9 +703,10 @@ WHERE
}
if dest.Status == "" {
// If we have no status, we treat it as enforcing since we know disk encryption is enabled and log for potential debugging
level.Debug(ds.logger).Log("msg", "no bitlocker status found for host", "host_id", host.ID)
dest.Status = fleet.DiskEncryptionEnforcing
// This is unexpected. We know that disk encryption is enabled so we treat it failed to draw
// attention to the issue and log potential debugging
level.Debug(ds.logger).Log("msg", "no bitlocker status found for host", "host_id", host.ID, "mdm_info", fmt.Sprintf("%+v", host.MDMInfo))
dest.Status = fleet.DiskEncryptionFailed
}
return &fleet.HostMDMDiskEncryption{

View file

@ -157,6 +157,10 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) {
h, err := ds.Host(ctx, id)
require.NoError(t, err)
require.NotNil(t, h)
mdmInfo, err := ds.GetHostMDM(ctx, id)
require.NoError(t, err)
require.NotNil(t, mdmInfo)
h.MDMInfo = mdmInfo
bls, err := ds.GetMDMWindowsBitLockerStatus(ctx, h)
require.NoError(t, err)
require.NotNil(t, bls)

View file

@ -5,9 +5,10 @@ import (
"database/sql"
"encoding/json"
"fmt"
"golang.org/x/text/unicode/norm"
"strings"
"golang.org/x/text/unicode/norm"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
@ -116,6 +117,11 @@ func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
return ctxerr.Wrapf(ctx, err, "deleting mdm_windows_configuration_profiles for team %d", tid)
}
_, err = tx.ExecContext(ctx, `DELETE FROM mdm_apple_declarations WHERE team_id=?`, tid)
if err != nil {
return ctxerr.Wrapf(ctx, err, "deleting mdm_apple_declarations for team %d", tid)
}
return nil
})
}

View file

@ -97,6 +97,13 @@ func testTeamsGetSetDelete(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
dec, err := ds.NewMDMAppleDeclaration(context.Background(), &fleet.MDMAppleDeclaration{
Identifier: "decl-1",
Name: "decl-1",
TeamID: &team.ID,
})
require.NoError(t, err)
err = ds.DeleteTeam(context.Background(), team.ID)
require.NoError(t, err)
@ -114,6 +121,9 @@ func testTeamsGetSetDelete(t *testing.T, ds *Datastore) {
_, err = ds.GetMDMWindowsConfigProfile(context.Background(), wcp.ProfileUUID)
require.ErrorAs(t, err, &nfe)
_, err = ds.GetMDMAppleConfigProfile(context.Background(), dec.DeclarationUUID)
require.ErrorAs(t, err, &nfe)
require.NoError(t, ds.DeletePack(context.Background(), newP.Name))
})
}

View file

@ -87,6 +87,7 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeCreatedDeclarationProfile{},
ActivityTypeDeletedDeclarationProfile{},
ActivityTypeEditedDeclarationProfile{},
}
type ActivityDetails interface {
@ -1370,6 +1371,25 @@ func (a ActivityTypeDeletedDeclarationProfile) Documentation() (activity string,
}`
}
type ActivityTypeEditedDeclarationProfile struct {
TeamID *uint `json:"team_id"`
TeamName *string `json:"team_name"`
}
func (a ActivityTypeEditedDeclarationProfile) ActivityName() string {
return "edited_declaration_profile"
}
func (a ActivityTypeEditedDeclarationProfile) Documentation() (activity string, details string, detailsExample string) {
return `Generated when a user edits the macOS declarations of a team (or no team) via the fleetctl CLI.`,
`This activity contains the following fields:
- "team_id": The ID of the team that the declarations apply to, ` + "`null`" + ` if they apply to devices that are not in a team.
- "team_name": The name of the team that the declarations apply to, ` + "`null`" + ` if they apply to devices that are not in a team.`, `{
"team_id": 123,
"team_name": "Workstations"
}`
}
// LogRoleChangeActivities logs activities for each role change, globally and one for each change in teams.
func LogRoleChangeActivities(ctx context.Context, ds Datastore, adminUser *User, oldGlobalRole *string, oldTeamRoles []UserTeam, user *User) error {
if user.GlobalRole != nil && (oldGlobalRole == nil || *oldGlobalRole != *user.GlobalRole) {

View file

@ -597,7 +597,7 @@ func (r *MDMAppleRawDeclaration) ValidateUserProvided() error {
// Check against types we don't allow
if r.Type == `com.apple.configuration.softwareupdate.enforcement.specific` {
return NewInvalidArgumentError(r.Type, "Declaration profile cant include OS updates settings. To control these settings, go to OS updates.")
return NewInvalidArgumentError(r.Type, "Declaration profile cant include OS updates settings. OS updates coming soon!")
}
if _, forbidden := ForbiddenDeclTypes[r.Type]; forbidden {

View file

@ -1173,7 +1173,7 @@ type Datastore interface {
UpdateDEPAssignProfileRetryPending(ctx context.Context, jobID uint, serials []string) error
// InsertMDMAppleDDMRequest inserts a DDM request.
InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType, rawJSON string) error
InsertMDMAppleDDMRequest(ctx context.Context, hostUUID, messageType string, rawJSON json.RawMessage) error
// MDMAppleDDMDeclarationsToken returns the token used to synchronize declarations for the
// specified host UUID.
@ -1191,9 +1191,10 @@ type Datastore interface {
// It also takes care of cleaning up all host declarations that are
// pending removal.
MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*MDMAppleHostDeclaration) error
// MDMAppleSetDeclarationsAsVerifying updates all
// ("pending", "install") declarations for a host to be ("verifying", "install")
MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error
// MDMAppleSetPendingDeclarationsAs updates all ("pending", "install")
// declarations for a host to be ("verifying", status), where status is
// the provided value.
MDMAppleSetPendingDeclarationsAs(ctx context.Context, hostUUID string, status *MDMDeliveryStatus, detail string) error
///////////////////////////////////////////////////////////////////////////////
// Microsoft MDM

View file

@ -30,6 +30,11 @@ func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.Privat
return p7.Decrypt(cert, key)
}
func prefixMatches(val []byte, prefix string) bool {
return len(val) >= len(prefix) &&
bytes.EqualFold([]byte(prefix), val[:len(prefix)])
}
// GetRawProfilePlatform identifies the platform type of a profile bytes by
// examining its initial content:
//
@ -45,22 +50,37 @@ func GetRawProfilePlatform(profile []byte) string {
return ""
}
prefixMatches := func(prefix []byte) bool {
return len(trimmedProfile) >= len(prefix) &&
bytes.EqualFold(prefix, trimmedProfile[:len(prefix)])
}
if prefixMatches([]byte("<?xml")) || prefixMatches([]byte(`{`)) {
if prefixMatches(trimmedProfile, "<?xml") || prefixMatches(trimmedProfile, `{`) {
return "darwin"
}
if prefixMatches([]byte("<replace")) || prefixMatches([]byte("<add")) {
if prefixMatches(trimmedProfile, "<replace") || prefixMatches(trimmedProfile, "<add") {
return "windows"
}
return ""
}
// GuessProfileExtension determines the likely file extension of a profile
// based on its content.
//
// It returns a string representing the determined file extension ("xml",
// "json", or "") based on the profile's content.
func GuessProfileExtension(profile []byte) string {
trimmedProfile := bytes.TrimSpace(profile)
switch {
case prefixMatches(trimmedProfile, "<?xml"),
prefixMatches(trimmedProfile, "<replace"),
prefixMatches(trimmedProfile, "<add"):
return "xml"
case prefixMatches(trimmedProfile, "{"):
return "json"
default:
return ""
}
}
const (
// FleetdConfigProfileName is the value for the PayloadDisplayName used by

View file

@ -198,3 +198,59 @@ func TestGetRawProfilePlatform(t *testing.T) {
})
}
}
func TestGuessProfileExtension(t *testing.T) {
testCases := []struct {
name string
profile []byte
expected string
}{
{
name: "XML with <?xml prefix",
profile: []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
expected: "xml",
},
{
name: "XML with <replace prefix",
profile: []byte("<replace value=\"something\"/>"),
expected: "xml",
},
{
name: "XML with <add prefix",
profile: []byte("<add key=\"somekey\" value=\"somevalue\"/>"),
expected: "xml",
},
{
name: "JSON with { prefix",
profile: []byte("{ \"key\": \"value\" }"),
expected: "json",
},
{
name: "Empty string",
profile: []byte(""),
expected: "",
},
{
name: "Text with no recognizable prefix",
profile: []byte("This is just some text."),
expected: "",
},
{
name: "XML with spaces before prefix",
profile: []byte(" <?xml version=\"1.0\" encoding=\"UTF-8\"?>"),
expected: "xml",
},
{
name: "JSON with spaces before prefix",
profile: []byte(" { \"key\": \"value\" }"),
expected: "json",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := GuessProfileExtension(tc.profile)
require.Equal(t, tc.expected, result, "Expected result does not match actual result")
})
}
}

View file

@ -780,7 +780,7 @@ type GetDEPAssignProfileExpiredCooldownsFunc func(ctx context.Context) (map[uint
type UpdateDEPAssignProfileRetryPendingFunc func(ctx context.Context, jobID uint, serials []string) error
type InsertMDMAppleDDMRequestFunc func(ctx context.Context, hostUUID string, messageType string, rawJSON string) error
type InsertMDMAppleDDMRequestFunc func(ctx context.Context, hostUUID string, messageType string, rawJSON json.RawMessage) error
type MDMAppleDDMDeclarationsTokenFunc func(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error)
@ -792,7 +792,7 @@ type MDMAppleBatchSetHostDeclarationStateFunc func(ctx context.Context) ([]strin
type MDMAppleStoreDDMStatusReportFunc func(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error
type MDMAppleSetDeclarationsAsVerifyingFunc func(ctx context.Context, hostUUID string) error
type MDMAppleSetPendingDeclarationsAsFunc func(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error
type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error
@ -2060,8 +2060,8 @@ type DataStore struct {
MDMAppleStoreDDMStatusReportFunc MDMAppleStoreDDMStatusReportFunc
MDMAppleStoreDDMStatusReportFuncInvoked bool
MDMAppleSetDeclarationsAsVerifyingFunc MDMAppleSetDeclarationsAsVerifyingFunc
MDMAppleSetDeclarationsAsVerifyingFuncInvoked bool
MDMAppleSetPendingDeclarationsAsFunc MDMAppleSetPendingDeclarationsAsFunc
MDMAppleSetPendingDeclarationsAsFuncInvoked bool
WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc
WSTEPStoreCertificateFuncInvoked bool
@ -4889,7 +4889,7 @@ func (s *DataStore) UpdateDEPAssignProfileRetryPending(ctx context.Context, jobI
return s.UpdateDEPAssignProfileRetryPendingFunc(ctx, jobID, serials)
}
func (s *DataStore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID string, messageType string, rawJSON string) error {
func (s *DataStore) InsertMDMAppleDDMRequest(ctx context.Context, hostUUID string, messageType string, rawJSON json.RawMessage) error {
s.mu.Lock()
s.InsertMDMAppleDDMRequestFuncInvoked = true
s.mu.Unlock()
@ -4931,11 +4931,11 @@ func (s *DataStore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID s
return s.MDMAppleStoreDDMStatusReportFunc(ctx, hostUUID, updates)
}
func (s *DataStore) MDMAppleSetDeclarationsAsVerifying(ctx context.Context, hostUUID string) error {
func (s *DataStore) MDMAppleSetPendingDeclarationsAs(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
s.mu.Lock()
s.MDMAppleSetDeclarationsAsVerifyingFuncInvoked = true
s.MDMAppleSetPendingDeclarationsAsFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleSetDeclarationsAsVerifyingFunc(ctx, hostUUID)
return s.MDMAppleSetPendingDeclarationsAsFunc(ctx, hostUUID, status, detail)
}
func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error {

View file

@ -442,15 +442,18 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i
d := fleet.NewMDMAppleDeclaration(data, tmID, name, rawDecl.Type, rawDecl.Identifier)
// TODO(roberto): Is this already handled in NewMDMAppleDeclaration? Could we add the labels as well?
// TODO(roberto): this should be part of fleet.NewMDMAppleDeclaration
d.Labels = validatedLabels
d.TeamID = tmID
decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d)
if err != nil {
return nil, err
}
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
return nil, ctxerr.Wrap(ctx, err, "bulk set pending host declarations")
}
var (
actTeamID *uint
actTeamName *string
@ -1749,6 +1752,34 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
profs = append(profs, mdmProf)
}
if !skipBulkPending {
// check for duplicates with existing profiles, skipBulkPending signals that the caller
// is responsible for ensuring that the profiles names are unique (e.g., MDMAppleMatchPreassignment)
allProfs, _, err := svc.ds.ListMDMConfigProfiles(ctx, tmID, fleet.ListOptions{PerPage: 0})
if err != nil {
return ctxerr.Wrap(ctx, err, "list mdm config profiles")
}
for _, p := range allProfs {
if byName[p.Name] {
switch {
case strings.HasPrefix(p.ProfileUUID, "a"):
// do nothing, all existing mobileconfigs will be replaced and we've already checked
// the new mobileconfigs for duplicates
continue
case strings.HasPrefix(p.ProfileUUID, "w"):
err := fleet.NewInvalidArgumentError("PayloadDisplayName", fmt.Sprintf(
"Couldnt edit custom_settings. A Windows configuration profile shares the same name as a macOS configuration profile (PayloadDisplayName): %q", p.Name))
return ctxerr.Wrap(ctx, err, "duplicate xml and mobileconfig by name")
default:
err := fleet.NewInvalidArgumentError("PayloadDisplayName", fmt.Sprintf(
"Couldnt edit custom_settings. More than one configuration profile have the same name (PayloadDisplayName): %q", p.Name))
return ctxerr.Wrap(ctx, err, "duplicate json and mobileconfig by name")
}
}
byName[p.Name] = true
}
}
if dryRun {
return nil
}
@ -2603,6 +2634,8 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
if err := svc.ds.CleanMacOSMDMLock(r.Context, cmdResult.UDID); err != nil {
return nil, ctxerr.Wrap(r.Context, err, "cleaning macOS host lock/wipe status")
}
return nil, nil
}
// We explicitly get the request type because it comes empty. There's a
@ -2639,8 +2672,11 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
return nil, svc.ds.UpdateHostLockWipeStatusFromAppleMDMResult(r.Context, cmdResult.UDID, cmdResult.CommandUUID, requestType, cmdResult.Status == fleet.MDMAppleStatusAcknowledged)
}
case "DeclarativeManagement":
// set "pending-install" profiles to "verifying"
err := svc.ds.MDMAppleSetDeclarationsAsVerifying(r.Context, cmdResult.UDID)
// set "pending-install" profiles to "verifying" or "failed"
// depending on the status of the DeviceManagement command
status := mdmAppleDeliveryStatusFromCommandStatus(cmdResult.Status)
detail := fmt.Sprintf("%s. Make sure the host is on macOS 13 or higher.", apple_mdm.FmtErrorChain(cmdResult.ErrorChain))
err := svc.ds.MDMAppleSetPendingDeclarationsAs(r.Context, cmdResult.UDID, status, detail)
return nil, ctxerr.Wrap(r.Context, err, "update declaration status on DeclarativeManagement ack")
}
@ -2745,7 +2781,6 @@ func ReconcileAppleDeclarations(
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) error {
// batch set declarations as pending
changedHosts, err := ds.MDMAppleBatchSetHostDeclarationState(ctx)
if err != nil {
@ -3237,7 +3272,7 @@ func (svc *MDMAppleDDMService) DeclarativeManagement(r *mdm.Request, dm *mdm.Dec
}
level.Debug(svc.logger).Log("msg", "ddm request received", "endpoint", dm.Endpoint)
if err := svc.ds.InsertMDMAppleDDMRequest(r.Context, dm.UDID, dm.Endpoint, string(dm.Data)); err != nil {
if err := svc.ds.InsertMDMAppleDDMRequest(r.Context, dm.UDID, dm.Endpoint, dm.Data); err != nil {
return nil, ctxerr.Wrap(r.Context, err, "insert ddm request history")
}
@ -3324,7 +3359,7 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostU
func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, endpoint string, hostUUID string) ([]byte, error) {
parts := strings.Split(endpoint, "/")
if len(parts) != 3 {
return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.New(ctx, fmt.Sprintf("unrecognized declarations endpoint: %s", endpoint)))
return nil, nano_service.NewHTTPStatusError(http.StatusBadRequest, ctxerr.Errorf(ctx, "unrecognized declarations endpoint: %s", endpoint))
}
level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", parts[1], "identifier", parts[2])
@ -3334,7 +3369,7 @@ func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, e
case "configuration":
return svc.handleConfigurationDeclaration(ctx, parts, hostUUID)
default:
return nil, newNotFoundError()
return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, ctxerr.Errorf(ctx, "declaration type not supported: %s", parts[1]))
}
}
@ -3414,10 +3449,6 @@ func (svc *MDMAppleDDMService) handleDeclarationStatus(ctx context.Context, dm *
}
}
if len(updates) == 0 {
return nil
}
// MDMAppleStoreDDMStatusReport takes care of cleaning ("pending", "remove")
// pairs for the host.
//

View file

@ -1430,6 +1430,9 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
return nil
}
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
type testCase struct {
name string
@ -1741,6 +1744,9 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string) error {
return nil
}
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})

View file

@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"golang.org/x/text/unicode/norm"
"io"
"net/http"
"os"
@ -14,6 +13,8 @@ import (
"strings"
"time"
"golang.org/x/text/unicode/norm"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/pkg/spec"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@ -22,6 +23,8 @@ import (
kithttp "github.com/go-kit/kit/transport/http"
)
const batchSize = 100
// Client is used to consume Fleet APIs from Go code
type Client struct {
*baseClient
@ -283,7 +286,8 @@ func (c *Client) runAppConfigChecks(fn func(ac *fleet.EnrichedAppConfig) error)
// getProfilesContents takes file paths and creates a slice of profile payloads
// ready to batch-apply.
func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fleet.MDMProfileBatchPayload, error) {
fileNameMap := make(map[string]struct{}, len(profiles))
// map to check for duplicate names
extByName := make(map[string]string, len(profiles))
result := make([]fleet.MDMProfileBatchPayload, 0, len(profiles))
for _, profile := range profiles {
@ -303,10 +307,10 @@ func getProfilesContents(baseDir string, profiles []fleet.MDMProfileSpec) ([]fle
}
name = strings.TrimSpace(mc.Name)
}
if _, isDuplicate := fileNameMap[name]; isDuplicate {
return nil, errors.New("Couldn't edit windows_settings.custom_settings. More than one configuration profile have the same name (Windows .xml file name or macOS PayloadDisplayName).")
if e, isDuplicate := extByName[name]; isDuplicate {
return nil, errors.New(fmtDuplicateNameErrMsg(name, e, ext))
}
fileNameMap[name] = struct{}{}
extByName[name] = ext
result = append(result, fleet.MDMProfileBatchPayload{
Name: name,
Contents: fileContents,
@ -1099,11 +1103,19 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, logFn func(format string,
numPolicies := len(config.Policies)
logFn("[+] syncing %d policies\n", numPolicies)
if !dryRun {
// Note: We are reusing the spec flow here for adding/updating policies, instead of creating a new flow for GitOps.
if err := c.ApplyPolicies(config.Policies); err != nil {
return fmt.Errorf("error applying policies: %w", err)
totalApplied := 0
for i := 0; i < len(config.Policies); i += batchSize {
end := i + batchSize
if end > len(config.Policies) {
end = len(config.Policies)
}
totalApplied += end - i
// Note: We are reusing the spec flow here for adding/updating policies, instead of creating a new flow for GitOps.
if err := c.ApplyPolicies(config.Policies[i:end]); err != nil {
return fmt.Errorf("error applying policies: %w", err)
}
logFn("[+] synced %d policies\n", totalApplied)
}
logFn("[+] synced %d policies\n", numPolicies)
}
}
var policiesToDelete []uint
@ -1123,8 +1135,17 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, logFn func(format string,
if len(policiesToDelete) > 0 {
logFn("[-] deleting %d policies\n", len(policiesToDelete))
if !dryRun {
if err := c.DeletePolicies(config.TeamID, policiesToDelete); err != nil {
return fmt.Errorf("error deleting policies: %w", err)
totalDeleted := 0
for i := 0; i < len(policiesToDelete); i += batchSize {
end := i + batchSize
if end > len(policiesToDelete) {
end = len(policiesToDelete)
}
totalDeleted += end - i
if err := c.DeletePolicies(config.TeamID, policiesToDelete[i:end]); err != nil {
return fmt.Errorf("error deleting policies: %w", err)
}
logFn("[-] deleted %d policies\n", totalDeleted)
}
}
}
@ -1132,6 +1153,7 @@ func (c *Client) doGitOpsPolicies(config *spec.GitOps, logFn func(format string,
}
func (c *Client) doGitOpsQueries(config *spec.GitOps, logFn func(format string, args ...interface{}), dryRun bool) error {
batchSize := 100
// Get the ids and names of current queries to figure out which ones to delete
queries, err := c.GetQueries(config.TeamID, nil)
if err != nil {
@ -1141,11 +1163,19 @@ func (c *Client) doGitOpsQueries(config *spec.GitOps, logFn func(format string,
numQueries := len(config.Queries)
logFn("[+] syncing %d queries\n", numQueries)
if !dryRun {
// Note: We are reusing the spec flow here for adding/updating queries, instead of creating a new flow for GitOps.
if err := c.ApplyQueries(config.Queries); err != nil {
return fmt.Errorf("error applying queries: %w", err)
appliedCount := 0
for i := 0; i < len(config.Queries); i += batchSize {
end := i + batchSize
if end > len(config.Queries) {
end = len(config.Queries)
}
appliedCount += end - i
// Note: We are reusing the spec flow here for adding/updating queries, instead of creating a new flow for GitOps.
if err := c.ApplyQueries(config.Queries[i:end]); err != nil {
return fmt.Errorf("error applying queries: %w", err)
}
logFn("[+] synced %d queries\n", appliedCount)
}
logFn("[+] synced %d queries\n", numQueries)
}
}
var queriesToDelete []uint
@ -1165,8 +1195,17 @@ func (c *Client) doGitOpsQueries(config *spec.GitOps, logFn func(format string,
if len(queriesToDelete) > 0 {
logFn("[-] deleting %d queries\n", len(queriesToDelete))
if !dryRun {
if err := c.DeleteQueries(queriesToDelete); err != nil {
return fmt.Errorf("error deleting queries: %w", err)
deleteCount := 0
for i := 0; i < len(queriesToDelete); i += batchSize {
end := i + batchSize
if end > len(queriesToDelete) {
end = len(queriesToDelete)
}
deleteCount += end - i
if err := c.DeleteQueries(queriesToDelete[i:end]); err != nil {
return fmt.Errorf("error deleting queries: %w", err)
}
logFn("[-] deleted %d queries\n", deleteCount)
}
}
}

View file

@ -24,6 +24,7 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-kit/kit/log/level"
"github.com/gocarina/gocsv"
)
@ -1029,17 +1030,30 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
if !ac.MDM.WindowsEnabledAndConfigured {
break
}
if license.IsPremium(ctx) {
hde, err := svc.ds.GetMDMWindowsBitLockerStatus(ctx, host)
// we include disk encryption status only for premium so initialize it to default struct
host.MDM.OSSettings.DiskEncryption = fleet.HostMDMDiskEncryption{}
// ensure host mdm info is loaded (we don't know if our caller populated it)
err := svc.ensureHostMDMInfo(ctx, host)
switch {
case err != nil && fleet.IsNotFound(err):
// assume host is unmanaged, log for debugging, and move on
level.Debug(svc.logger).Log("msg", "cannot determine bitlocker status because no mdm info for host", "host_id", host.ID)
case err != nil:
return nil, ctxerr.Wrap(ctx, err, "get host mdm bitlocker status")
case hde != nil:
host.MDM.OSSettings.DiskEncryption = *hde
return nil, ctxerr.Wrap(ctx, err, "ensure host mdm info")
default:
host.MDM.OSSettings.DiskEncryption = fleet.HostMDMDiskEncryption{}
hde, err := svc.ds.GetMDMWindowsBitLockerStatus(ctx, host)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host mdm bitlocker status")
}
if hde != nil {
// overwrite the default disk encryption status
host.MDM.OSSettings.DiskEncryption = *hde
}
}
}
profs, err := svc.ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host mdm windows profiles")
@ -1124,6 +1138,21 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
}, nil
}
func (svc *Service) ensureHostMDMInfo(ctx context.Context, host *fleet.Host) error {
if host.MDMInfo == nil {
mdmInfo, err := svc.ds.GetHostMDM(ctx, host.ID)
if err != nil {
return err
}
host.MDMInfo = mdmInfo
}
if host.MDMInfo == nil {
// this should not happen, but just in case
return ctxerr.New(ctx, fmt.Sprintf("nil mdm info for host %d", host.ID))
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Host Query Report
////////////////////////////////////////////////////////////////////////////////

View file

@ -407,6 +407,7 @@ func TestHostDetailsOSSettings(t *testing.T) {
ds.GetMDMWindowsBitLockerStatusFuncInvoked = false
ds.GetHostMDMAppleProfilesFuncInvoked = false
ds.GetHostMDMWindowsProfilesFuncInvoked = false
ds.GetHostMDMFuncInvoked = false
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true, WindowsEnabledAndConfigured: true}}, nil
@ -423,6 +424,10 @@ func TestHostDetailsOSSettings(t *testing.T) {
ds.GetHostMDMWindowsProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMWindowsProfile, error) {
return nil, nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
hmdm := fleet.HostMDM{Enrolled: true, IsServer: false}
return &hmdm, nil
}
}
for _, c := range cases {
@ -442,6 +447,11 @@ func TestHostDetailsOSSettings(t *testing.T) {
switch c.host.Platform {
case "windows":
require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked)
if c.licenseTier == fleet.TierPremium {
require.True(t, ds.GetHostMDMFuncInvoked)
} else {
require.False(t, ds.GetHostMDMFuncInvoked)
}
if c.wantStatus != "" {
require.True(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked)
require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status)
@ -500,6 +510,10 @@ func TestHostDetailsOSSettingsWindowsOnly(t *testing.T) {
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
return &fleet.HostLockWipeStatus{}, nil
}
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
hmdm := fleet.HostMDM{Enrolled: true, IsServer: false}
return &hmdm, nil
}
ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
hostDetail, err := svc.getHostDetails(test.UserContext(ctx, test.UserAdmin), &fleet.Host{ID: 42, Platform: "windows"}, fleet.HostDetailOptions{
@ -510,6 +524,7 @@ func TestHostDetailsOSSettingsWindowsOnly(t *testing.T) {
require.NotNil(t, hostDetail)
require.True(t, ds.AppConfigFuncInvoked)
require.False(t, ds.GetHostMDMAppleProfilesFuncInvoked)
require.True(t, ds.GetHostMDMFuncInvoked)
require.True(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked)
require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status)
require.Equal(t, fleet.DiskEncryptionVerified, *hostDetail.MDM.OSSettings.DiskEncryption.Status)
@ -1500,7 +1515,6 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
if hostID == teamHostID {
return teamHost, nil
}
return globalHost, nil
}
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {

View file

@ -10017,13 +10017,149 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 2)
// now cause deletions and verify that results are deleted
// now update the query and verify that results are deleted
updatedQuery := "SELECT * FROM some_new_table;"
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{Query: &updatedQuery}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, updatedQuery, modifyQueryResp.Query.Query)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
Platform: ptr.String("linux"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "linux", modifyQueryResp.Query.Platform)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform to the same value and verify that results are not deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
Platform: ptr.String("linux"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "linux", modifyQueryResp.Query.Platform)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.9.1"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.9.1", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version to another value and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.11.0"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.11.0", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version to the same value and verify that results are not deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.11.0"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.11.0", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the query via specs and change the min_osquery_version, results should be deleted.
osqueryInfoQuerySpec := &fleet.QuerySpec{
Name: osqueryInfoQuery.Name,
Description: osqueryInfoQuery.Description,
Query: osqueryInfoQuery.Query,
Interval: osqueryInfoQuery.Interval,
ObserverCanRun: osqueryInfoQuery.ObserverCanRun,
Platform: osqueryInfoQuery.Platform,
MinOsqueryVersion: osqueryInfoQuery.MinOsqueryVersion,
AutomationsEnabled: osqueryInfoQuery.AutomationsEnabled,
Logging: osqueryInfoQuery.Logging,
DiscardData: osqueryInfoQuery.DiscardData,
}
osqueryInfoQuerySpec.MinOsqueryVersion = "5.12.0"
var applyResp applyQuerySpecsResponse
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// don't change platform or min_osquery_version and results should not be deleted
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform and results should be deleted.
osqueryInfoQuerySpec.Platform = "darwin"
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Update logging type, which should cause results deletion
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging)

View file

@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"strings"
"testing"
"time"
@ -16,8 +15,8 @@ import (
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
@ -34,9 +33,6 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
"DataAssetReference": "com.fleet.asset.bash" %s
}
}`
// TODO: figure out the best way to do this. We might even consider
// starting a different test suite.
t.Cleanup(func() { s.cleanupDeclarations(t) })
newDeclBytes := func(i int, payload ...string) []byte {
var p string
@ -66,7 +62,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
}}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Declaration profile cant include OS updates settings. To control these settings, go to OS updates.")
require.Contains(t, errMsg, "Declaration profile cant include OS updates settings. OS updates coming soon!")
// Types from our list of forbidden types should fail
for ft := range fleet.ForbiddenDeclTypes {
@ -92,7 +88,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMBatchUpload() {
{Name: "bad2", Contents: newDeclBytes(2, `"baz": "bing"`)},
}}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "A declaration profile with this name already exists.")
require.Contains(t, errMsg, "More than one configuration profile have the same name")
// Same identifier should fail
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
@ -201,8 +197,6 @@ func (s *integrationMDMTestSuite) TestMDMAppleDeviceManagementRequests() {
return strings.ToUpper(csum)
}
t.Cleanup(func() { s.cleanupDeclarations(t) })
insertDeclaration := func(t *testing.T, decl fleet.MDMAppleDeclaration) {
stmt := `
INSERT INTO mdm_apple_declarations (
@ -352,30 +346,9 @@ INSERT INTO host_mdm_apple_declarations (
}
}
checkRequestsDatabase := func(t *testing.T, messageType, enrollmentID string, expectedCount int) {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var count int
if err := sqlx.GetContext(
context.Background(),
q,
&count,
"SELECT count(*) AS count FROM mdm_apple_declarative_requests WHERE enrollment_id = ? AND message_type = ?",
enrollmentID,
messageType,
); err != nil {
return err
}
require.Equal(t, expectedCount, count, "unexpected db row count for declaration requests")
return nil
})
}
var currDeclToken string // we'll use this to track the expected token across tests
t.Run("Tokens", func(t *testing.T) {
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 0)
// get tokens, timestamp should be the same as the declaration and token should be non-empty
r, err := mdmDevice.DeclarativeManagement("tokens")
require.NoError(t, err)
@ -399,7 +372,6 @@ INSERT INTO host_mdm_apple_declarations (
}
insertDeclaration(t, noTeamDeclsByUUID["456"])
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["456"])
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 1)
// get tokens again, timestamp and token should have changed
r, err = mdmDevice.DeclarativeManagement("tokens")
@ -407,11 +379,9 @@ INSERT INTO host_mdm_apple_declarations (
parsed = parseTokensResp(r)
checkTokensResp(t, parsed, then.Add(1*time.Minute), currDeclToken)
currDeclToken = parsed.SyncTokens.DeclarationsToken
checkRequestsDatabase(t, "tokens", mdmDevice.UUID, 2)
})
t.Run("DeclarationItems", func(t *testing.T) {
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 0)
r, err := mdmDevice.DeclarativeManagement("declaration-items")
require.NoError(t, err)
checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID))
@ -432,7 +402,6 @@ INSERT INTO host_mdm_apple_declarations (
}
insertDeclaration(t, noTeamDeclsByUUID["789"])
insertHostDeclaration(t, mdmDevice.UUID, noTeamDeclsByUUID["789"])
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 1)
// get tokens again, timestamp and token should have changed
r, err = mdmDevice.DeclarativeManagement("tokens")
@ -444,20 +413,16 @@ INSERT INTO host_mdm_apple_declarations (
r, err = mdmDevice.DeclarativeManagement("declaration-items")
require.NoError(t, err)
checkDeclarationItemsResp(t, parseDeclarationItemsResp(r), currDeclToken, mapDeclsByChecksum(noTeamDeclsByUUID))
checkRequestsDatabase(t, "declaration-items", mdmDevice.UUID, 2)
})
t.Run("Status", func(t *testing.T) {
checkRequestsDatabase(t, "status", mdmDevice.UUID, 0)
_, err := mdmDevice.DeclarativeManagement("status", fleet.MDMAppleDDMStatusReport{})
require.NoError(t, err)
checkRequestsDatabase(t, "status", mdmDevice.UUID, 1)
})
t.Run("Declaration", func(t *testing.T) {
want := noTeamDeclsByUUID["123"]
declarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier)
checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 0)
r, err := mdmDevice.DeclarativeManagement(declarationPath)
require.NoError(t, err)
@ -482,23 +447,24 @@ INSERT INTO host_mdm_apple_declarations (
want = noTeamDeclsByUUID["abc"]
r, err = mdmDevice.DeclarativeManagement(fmt.Sprintf("declaration/%s/%s", "configuration", want.Identifier))
require.NoError(t, err)
checkRequestsDatabase(t, declarationPath, mdmDevice.UUID, 1)
// try getting a non-existent declaration, should fail 404
nonExistantDeclarationPath := fmt.Sprintf("declaration/%s/%s", "configuration", "nonexistent")
checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 0)
_, err = mdmDevice.DeclarativeManagement(nonExistantDeclarationPath)
require.Error(t, err)
require.ErrorContains(t, err, "404 Not Found")
checkRequestsDatabase(t, nonExistantDeclarationPath, mdmDevice.UUID, 1)
// try getting an unsupported declaration, should fail 404
unsupportedDeclarationPath := fmt.Sprintf("declaration/%s/%s", "asset", "nonexistent")
_, err = mdmDevice.DeclarativeManagement(unsupportedDeclarationPath)
require.Error(t, err)
require.ErrorContains(t, err, "404 Not Found")
// typo should fail as bad request
typoDeclarationPath := fmt.Sprintf("declarations/%s/%s", "configurations", want.Identifier)
checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 0)
_, err = mdmDevice.DeclarativeManagement(typoDeclarationPath)
require.Error(t, err)
require.ErrorContains(t, err, "400 Bad Request")
checkRequestsDatabase(t, typoDeclarationPath, mdmDevice.UUID, 1)
assertDeclarationResponse(r, want)
})
@ -507,27 +473,29 @@ INSERT INTO host_mdm_apple_declarations (
func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
t := s.T()
ctx := context.Background()
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
logger := kitlog.NewJSONLogger(os.Stdout)
// TODO: use endpoints once those are available.
addDeclaration := func(identifier string, teamID uint) {
stmt := `
INSERT INTO mdm_apple_declarations
(declaration_uuid, team_id, identifier, name, raw_json, checksum)
VALUES
(UUID(), ?, ?, UUID(), ?, HEX(MD5(raw_json)) )`
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, stmt, teamID, identifier, declarationForTest(identifier))
return err
})
addDeclaration := func(identifier string, teamID uint, labelNames []string) string {
fields := map[string][]string{
"labels": labelNames,
}
if teamID > 0 {
fields["team_id"] = []string{fmt.Sprintf("%d", teamID)}
}
body, headers := generateNewProfileMultipartRequest(
t, identifier+".json", declarationForTest(identifier), s.token, fields,
)
res := s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusOK, headers)
var resp newMDMConfigProfileResponse
err := json.NewDecoder(res.Body).Decode(&resp)
require.NoError(t, err)
require.NotEmpty(t, resp.ProfileUUID)
require.Equal(t, "d", string(resp.ProfileUUID[0]))
return resp.ProfileUUID
}
deleteDeclaration := func(identifier string, teamID uint) {
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations WHERE team_id = ? AND identifier = ?", teamID, identifier)
return err
})
deleteDeclaration := func(declUUID string) {
var deleteResp deleteMDMConfigProfileResponse
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/configuration_profiles/%s", declUUID), nil, http.StatusOK, &deleteResp)
}
// create a team
@ -540,10 +508,6 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
require.NotZero(t, createTeamResp.Team.ID)
team = createTeamResp.Team
// TODO: figure out the best way to do this. We might even consider
// starting a different test suite.
t.Cleanup(func() { s.cleanupDeclarations(t) })
checkNoCommands := func(d *mdmtest.TestAppleMDMClient) {
cmd, err := d.Idle()
require.NoError(t, err)
@ -602,18 +566,18 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
// trigger the reconciler, no error
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// declarativeManagement command is not sent.
checkNoCommands(device)
// add global declarations
addDeclaration("I1", 0)
addDeclaration("I2", 0)
d1UUID := addDeclaration("I1", 0, nil)
addDeclaration("I2", 0, nil)
// reconcile again, this time new declarations were added
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// TODO: check command is pending
@ -622,15 +586,15 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
checkDDMSync(device)
// reconcile again, commands for the uploaded declarations are already sent
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// no new commands are sent
checkNoCommands(device)
// delete a declaration
deleteDeclaration("I1", 0)
deleteDeclaration(d1UUID)
// reconcile again
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// a DDM sync is triggered
checkDDMSync(device)
@ -638,7 +602,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
// add a new host
_, deviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t)
// reconcile again
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// DDM sync is triggered only for the new host
checkNoCommands(device)
@ -649,7 +613,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHost.ID}}, http.StatusOK)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// DDM sync is triggered only for the transferred host
@ -658,18 +622,18 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
checkNoCommands(deviceTwo)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// nobody receives commands this time
checkNoCommands(device)
checkNoCommands(deviceTwo)
// add declarations to the team
addDeclaration("I1", team.ID)
addDeclaration("I2", team.ID)
addDeclaration("I1", team.ID, nil)
addDeclaration("I2", team.ID, nil)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// DDM sync is triggered for the host in the team
checkDDMSync(device)
@ -681,7 +645,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHostThree.ID}}, http.StatusOK)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// DDM sync is triggered only for the new host
checkNoCommands(device)
@ -689,15 +653,12 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
checkDDMSync(deviceThree)
// no new commands after another reconciliation
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
checkNoCommands(device)
checkNoCommands(deviceTwo)
checkNoCommands(deviceThree)
// TODO: use proper APIs for this
// add a new label + label declaration
addDeclaration("I3", team.ID)
label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: t.Name(), Query: "select 1;"})
require.NoError(t, err)
// update label with host membership
@ -713,25 +674,11 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
},
)
// update declaration <-> label mapping
mysql.ExecAdhocSQL(
t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
`INSERT INTO
mdm_declaration_labels (apple_declaration_uuid, label_name, label_id)
VALUES ((SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ? and identifier = ?), ?, ?)`,
team.ID,
"I3",
label.Name,
label.ID,
)
return err
},
)
// add a new label + label declaration
addDeclaration("I3", team.ID, []string{label.Name})
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// DDM sync is triggered only for the host with the label
checkNoCommands(device)
@ -742,12 +689,6 @@ func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
t := s.T()
ctx := context.Background()
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
logger := kitlog.NewJSONLogger(os.Stdout)
// TODO: figure out the best way to do this. We might even consider
// starting a different test suite.
t.Cleanup(func() { s.cleanupDeclarations(t) })
assertHostDeclarations := func(hostUUID string, wantDecls []*fleet.MDMAppleHostDeclaration) {
var gotDecls []*fleet.MDMAppleHostDeclaration
@ -768,7 +709,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile profiles
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// declarations are ("install", "pending") after the cron run
@ -851,7 +792,7 @@ func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile profiles
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
assertHostDeclarations(mdmHost.UUID, []*fleet.MDMAppleHostDeclaration{
{Identifier: "I1", Status: &fleet.MDMDeliveryVerified, OperationType: fleet.MDMOperationTypeInstall},
@ -881,6 +822,191 @@ func (s *integrationMDMTestSuite) TestAppleDDMStatusReport() {
})
}
func (s *integrationMDMTestSuite) TestDDMUnsupportedDevice() {
t := s.T()
ctx := context.Background()
fleetHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
getProfiles := func(h *fleet.Host) map[string]*fleet.HostMDMAppleProfile {
profs, err := s.ds.GetHostMDMAppleProfiles(ctx, h.UUID)
require.NoError(t, err)
out := make(map[string]*fleet.HostMDMAppleProfile, len(profs))
for _, p := range profs {
p := p
out[p.Identifier] = &p
}
return out
}
declarations := []fleet.MDMProfileBatchPayload{
{Name: "N1.json", Contents: declarationForTest("I1")},
{Name: "N2.json", Contents: declarationForTest("I2")},
}
// add global declarations
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile declarations
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
// declaration is pending
profs := getProfiles(fleetHost)
require.Equal(t, &fleet.MDMDeliveryPending, profs["I1"].Status)
require.Equal(t, &fleet.MDMDeliveryPending, profs["I2"].Status)
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
// simulate an error returned by devices that don't support DDM
errChain := []mdm.ErrorChain{
{
ErrorCode: 4,
ErrorDomain: "RMErrorDomain",
LocalizedDescription: "Feature Disabled: DeclarativeManagement is disabled.",
},
}
cmd, err = mdmDevice.Err(cmd.CommandUUID, errChain)
require.NoError(t, err)
require.Nil(t, cmd)
// profiles are failed
profs = getProfiles(fleetHost)
require.Equal(t, &fleet.MDMDeliveryFailed, profs["I1"].Status)
require.Contains(t, profs["I1"].Detail, "Feature Disabled")
require.Equal(t, &fleet.MDMDeliveryFailed, profs["I2"].Status)
require.Contains(t, profs["I2"].Detail, "Feature Disabled")
}
func (s *integrationMDMTestSuite) TestDDMNoDeclarationsLeft() {
t := s.T()
_, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
res, err := mdmDevice.DeclarativeManagement("tokens")
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
var tok fleet.MDMAppleDDMTokensResponse
err = json.NewDecoder(res.Body).Decode(&tok)
require.NoError(t, err)
require.Empty(t, tok.SyncTokens.DeclarationsToken)
require.NotEmpty(t, tok.SyncTokens.Timestamp)
res, err = mdmDevice.DeclarativeManagement("declaration-items")
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
var items fleet.MDMAppleDDMDeclarationItemsResponse
err = json.NewDecoder(res.Body).Decode(&items)
require.NoError(t, err)
require.Empty(t, items.DeclarationsToken)
require.Empty(t, items.Declarations.Activations)
require.Empty(t, items.Declarations.Configurations)
require.Empty(t, items.Declarations.Assets)
require.Empty(t, items.Declarations.Management)
}
func (s *integrationMDMTestSuite) TestDDMTransactionRecording() {
t := s.T()
ctx := context.Background()
type record struct {
EnrollmentID string `db:"enrollment_id"`
MessageType string `db:"message_type"`
RawJSON *json.RawMessage `db:"raw_json"`
}
verifyTransactionRecord := func(want record) {
var got record
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(
ctx, q, &got,
`SELECT
enrollment_id, message_type, raw_json
FROM mdm_apple_declarative_requests
ORDER BY id DESC
LIMIT 1`,
)
})
if got.RawJSON != nil {
fmt.Println(string(*got.RawJSON))
}
require.Equal(t, want, got)
}
declarations := []fleet.MDMProfileBatchPayload{
{Name: "N1.json", Contents: declarationForTest("I1")},
{Name: "N2.json", Contents: declarationForTest("I2")},
}
// add global declarations
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: declarations}, http.StatusNoContent)
// reconcile declarations
err := ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, s.logger)
require.NoError(t, err)
_, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
_, err = mdmDevice.DeclarativeManagement("tokens")
require.NoError(t, err)
verifyTransactionRecord(record{
MessageType: "tokens",
EnrollmentID: mdmDevice.UUID,
RawJSON: nil,
})
res, err := mdmDevice.DeclarativeManagement("declaration-items")
require.NoError(t, err)
verifyTransactionRecord(record{
MessageType: "declaration-items",
EnrollmentID: mdmDevice.UUID,
RawJSON: nil,
})
var items fleet.MDMAppleDDMDeclarationItemsResponse
require.NoError(t, json.NewDecoder(res.Body).Decode(&items))
var i1ServerToken string
for _, d := range items.Declarations.Configurations {
if d.Identifier == "I1" {
i1ServerToken = d.ServerToken
}
}
// a second device requests tokens
_, mdmDeviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t)
_, err = mdmDeviceTwo.DeclarativeManagement("tokens")
require.NoError(t, err)
verifyTransactionRecord(record{
MessageType: "tokens",
EnrollmentID: mdmDeviceTwo.UUID,
RawJSON: nil,
})
_, err = mdmDevice.DeclarativeManagement("declaration/configuration/I1")
require.NoError(t, err)
verifyTransactionRecord(record{
MessageType: "declaration/configuration/I1",
EnrollmentID: mdmDevice.UUID,
RawJSON: nil,
})
report := fleet.MDMAppleDDMStatusReport{}
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: "I1", ServerToken: i1ServerToken},
}
_, err = mdmDevice.DeclarativeManagement("status", report)
require.NoError(t, err)
verifyTransactionRecord(record{
MessageType: "status",
EnrollmentID: mdmDevice.UUID,
RawJSON: ptr.RawMessage(
json.RawMessage(
fmt.Sprintf(
`{"StatusItems":{"management":{"declarations":{"activations":null,"configurations":[{"active":true,"identifier":"I1","valid":"valid","server-token":"%s"}],"assets":null,"management":null}}},"Errors":null}`,
i1ServerToken,
),
),
),
})
}
func declarationForTest(identifier string) []byte {
return []byte(fmt.Sprintf(`
{
@ -892,18 +1018,13 @@ func declarationForTest(identifier string) []byte {
}`, identifier))
}
func (s *integrationMDMTestSuite) cleanupDeclarations(t *testing.T) {
ctx := context.Background()
// TODO: figure out the best way to do this. We might even consider
// starting a different test suite.
// delete declarations to not affect other tests
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations")
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations")
return err
})
func declarationForTestWithType(identifier string, dType string) []byte {
return []byte(fmt.Sprintf(`
{
"Type": "%s",
"Payload": {
"Echo": "foo"
},
"Identifier": "%s"
}`, dType, identifier))
}

View file

@ -87,6 +87,7 @@ type integrationMDMTestSuite struct {
mdmStorage *mysql.NanoMDMStorage
worker *worker.Worker
mdmCommander *apple_mdm.MDMAppleCommander
logger kitlog.Logger
}
func (s *integrationMDMTestSuite) SetupSuite() {
@ -164,10 +165,15 @@ func (s *integrationMDMTestSuite) SetupSuite() {
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
cronLog = kitlog.NewNopLogger()
}
serverLogger := kitlog.NewJSONLogger(os.Stdout)
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
serverLogger = kitlog.NewNopLogger()
}
config := TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
},
Logger: serverLogger,
FleetConfig: &fleetCfg,
MDMStorage: mdmStorage,
DEPStorage: depStorage,
@ -247,9 +253,6 @@ func (s *integrationMDMTestSuite) SetupSuite() {
},
APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589",
}
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
config.Logger = kitlog.NewNopLogger()
}
users, server := RunServerForTestsWithDS(s.T(), s.ds, &config)
s.server = server
s.users = users
@ -263,6 +266,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
s.profileSchedule = profileSchedule
s.mdmStorage = mdmStorage
s.mdmCommander = mdmCommander
s.logger = serverLogger
fleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := s.fleetDMNextCSRStatus.Swap(http.StatusOK)
@ -347,6 +351,16 @@ func (s *integrationMDMTestSuite) TearDownTest() {
_, err := q.ExecContext(ctx, "DELETE FROM mdm_windows_enrollments")
return err
})
// clear any lingering declarations
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations")
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations")
return err
})
}
func (s *integrationMDMTestSuite) mockDEPResponse(handler http.Handler) {
@ -8721,8 +8735,6 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
require.NoError(t, err)
t.Cleanup(func() { s.cleanupDeclarations(t) })
assertAppleProfile := func(filename, name, ident string, teamID uint, labelNames []string, wantStatus int, wantErrMsg string) string {
fields := map[string][]string{
"labels": labelNames,
@ -10849,6 +10861,11 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
`{"team_id": null, "team_name": null}`,
0,
)
s.lastActivityOfTypeMatches(
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
`{"team_id": null, "team_name": null}`,
0,
)
// apply to both team id and name
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil},
@ -10863,6 +10880,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: mobileconfigForTest("N1", "I2")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
// profiles with reserved macOS identifiers
@ -10871,6 +10889,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: p, Contents: mobileconfigForTest(p, p)},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: payload identifier %s is not allowed", p))
@ -10881,6 +10900,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTestWithContent("N1", "I1", "II1", p, "")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadType(s): %s", p))
@ -10891,19 +10911,48 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTestWithContent("N1", "I1", p, "random", "")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, fmt.Sprintf("Validation Failed: unsupported PayloadIdentifier(s): %s", p))
}
// profiles with forbidden declaration types
for dt := range fleet.ForbiddenDeclTypes {
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTestWithType("D1", dt)},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Only configuration declarations that dont require an asset reference are supported", dt)
}
// and one more for the software update declaration
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTestWithType("D1", "com.apple.configuration.softwareupdate.enforcement.specific")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Declaration profile cant include OS updates settings. OS updates coming soon!")
// invalid JSON
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: []byte(`{"foo":}`)},
}}, http.StatusBadRequest, "team_id", strconv.Itoa(int(tm.ID)))
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "The file should include valid JSON")
// profiles with reserved Windows location URIs
// bitlocker
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: syncml.FleetBitLockerTargetLocURI, Contents: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetBitLockerTargetLocURI))},
{Name: "N3", Contents: syncMLForTest("./Foo/Bar")},
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Custom configuration profiles can't include BitLocker settings. To control these settings, use the mdm.enable_disk_encryption option.")
// os updates
@ -10933,6 +10982,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID)), "dry_run", "true")
s.assertConfigProfilesByIdentifier(&tm.ID, "I1", false)
s.assertWindowsConfigProfilesByName(&tm.ID, "N1", false)
@ -10941,6 +10991,7 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
{Name: "N4", Contents: declarationForTest("D1")},
}}, http.StatusNoContent, "team_id", strconv.Itoa(int(tm.ID)))
s.assertConfigProfilesByIdentifier(&tm.ID, "I1", true)
s.assertWindowsConfigProfilesByName(&tm.ID, "N2", true)
@ -10954,6 +11005,50 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
0,
)
s.lastActivityOfTypeMatches(
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name),
0,
)
// names cannot be duplicated across platforms
declBytes := json.RawMessage(`{
"Type": "com.apple.configuration.decl.foo",
"Identifier": "com.fleet.config.foo",
"Payload": {
"ServiceType": "com.apple.bash",
"DataAssetReference": "com.fleet.asset.bash"
}}`)
mcBytes := mobileconfigForTest("N1", "I1")
winBytes := syncMLForTest("./Foo/Bar")
for _, p := range []struct {
payload []fleet.MDMProfileBatchPayload
expectErr string
}{
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: winBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (Windows .xml file name or macOS .mobileconfig PayloadDisplayName).",
},
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: declBytes}, {Name: "N1", Contents: winBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (macOS .json file name or Windows .xml file name).",
},
{
payload: []fleet.MDMProfileBatchPayload{{Name: "N1", Contents: mcBytes}, {Name: "N1", Contents: declBytes}},
expectErr: "More than one configuration profile have the same name 'N1' (macOS .json file name or macOS .mobileconfig PayloadDisplayName).",
},
} {
// team profiles
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, p.expectErr)
// no team profiles
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: p.payload}, http.StatusUnprocessableEntity)
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, p.expectErr)
}
}
func (s *integrationMDMTestSuite) TestBatchSetMDMProfilesBackwardsCompat() {
@ -12066,6 +12161,14 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() {
var hr getHostResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hr)
require.Nil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)
// create a non-server host that is enrolled in MDM
host2 := createOrbitEnrolledHost(t, "windows", "non-server-host", s.ds)
require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, host2.ID, false, true, "http://example.com", false, fleet.WellKnownMDMFleet, ""))
hr = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host2.ID), nil, http.StatusOK, &hr)
require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)
require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status)
}

View file

@ -1527,6 +1527,10 @@ func (svc *Service) BatchSetMDMProfiles(
return ctxerr.Wrap(ctx, err, "validating Windows profiles")
}
if err := svc.validateCrossPlatformProfileNames(ctx, appleProfiles, windowsProfiles, appleDecls); err != nil {
return ctxerr.Wrap(ctx, err, "validating cross-platform profile names")
}
if dryRun {
return nil
}
@ -1568,10 +1572,80 @@ func (svc *Service) BatchSetMDMProfiles(
}); err != nil {
return ctxerr.Wrap(ctx, err, "logging activity for edited windows profile")
}
if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedDeclarationProfile{
TeamID: tmID,
TeamName: tmName,
}); err != nil {
return ctxerr.Wrap(ctx, err, "logging activity for edited macos declarations")
}
return nil
}
func (svc *Service) validateCrossPlatformProfileNames(ctx context.Context, appleProfiles []*fleet.MDMAppleConfigProfile, windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration) error {
// map all profile names to check for duplicates, regardless of platform; key is name, value is one of
// ".mobileconfig" or ".json" or ".xml"
extByName := make(map[string]string, len(appleProfiles)+len(windowsProfiles)+len(appleDecls))
for i, p := range appleProfiles {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("appleProfiles[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".mobileconfig", v))
return ctxerr.Wrap(ctx, err, "duplicate mobileconfig profile by name")
}
extByName[p.Name] = ".mobileconfig"
}
for i, p := range windowsProfiles {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("windowsProfiles[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".xml", v))
return ctxerr.Wrap(ctx, err, "duplicate xml by name")
}
extByName[p.Name] = ".xml"
}
for i, p := range appleDecls {
if v, ok := extByName[p.Name]; ok {
err := fleet.NewInvalidArgumentError(fmt.Sprintf("appleDecls[%d]", i), fmtDuplicateNameErrMsg(p.Name, ".json", v))
return ctxerr.Wrap(ctx, err, "duplicate json by name")
}
extByName[p.Name] = ".json"
}
return nil
}
func fmtDuplicateNameErrMsg(name, fileType1, fileType2 string) string {
var part1 string
switch fileType1 {
case ".xml":
part1 = "Windows .xml file name"
case ".mobileconfig":
part1 = "macOS .mobileconfig PayloadDisplayName"
case ".json":
part1 = "macOS .json file name"
}
var part2 string
switch fileType2 {
case ".xml":
part2 = "Windows .xml file name"
case ".mobileconfig":
part2 = "macOS .mobileconfig PayloadDisplayName"
case ".json":
part2 = "macOS .json file name"
}
base := fmt.Sprintf(`Couldnt edit custom_settings. More than one configuration profile have the same name '%s'`, name)
detail := ` (%s).`
switch {
case part1 == part2:
return fmt.Sprintf(base+detail, part1)
case part1 != "" && part2 != "":
return fmt.Sprintf(base+detail, fmt.Sprintf("%s or %s", part1, part2))
case part1 != "" || part2 != "":
return fmt.Sprintf(base+detail, part1+part2)
default:
return base + "." // should never happen
}
}
func (svc *Service) authorizeBatchProfiles(ctx context.Context, tmID *uint, tmName *string) (*uint, *string, error) {
if tmID != nil && tmName != nil {
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
@ -1631,14 +1705,7 @@ func getAppleProfiles(
}
// Check for DDM files
isJSON := func(b []byte) bool {
var js json.RawMessage
return json.Unmarshal(b, &js) == nil
}
// TODO(roberto): As a mini optimization, GetRawDeclarationValues could replace isJSON.
if isJSON(prof.Contents) {
if mdm.GuessProfileExtension(prof.Contents) == "json" {
rawDecl, err := fleet.GetRawDeclarationValues(prof.Contents)
if err != nil {
return nil, nil, err
@ -1659,26 +1726,7 @@ func getAppleProfiles(
}
}
v, ok := byName[mdmDecl.Name]
switch {
case !ok:
byName[mdmDecl.Name] = "declaration"
case v == "mobileconfig":
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A configuration profile with this name already exists."),
"duplicate mobileconfig profile by name")
case v == "declaration":
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A declaration profile with this name already exists."),
"duplicate declaration profile by name")
default:
// this should never happen but just in case
return nil, nil, ctxerr.Wrap(ctx,
fleet.NewInvalidArgumentError(mdmDecl.Name, "A profile with this name already exists."),
"duplicate profile by name")
}
v, ok = byIdent[mdmDecl.Identifier]
v, ok := byIdent[mdmDecl.Identifier]
switch {
case !ok:
byIdent[mdmDecl.Identifier] = "declaration"

View file

@ -3,6 +3,8 @@ package service
import (
"context"
"fmt"
"slices"
"strings"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@ -325,7 +327,6 @@ func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPayload) (*fleet.Query, error) {
// Load query first to determine if the user can modify it.
query, err := svc.ds.Query(ctx, id)
shouldDiscardQueryResults, shouldDeleteStats := false, false
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, err
@ -344,6 +345,8 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
})
}
shouldDiscardQueryResults, shouldDeleteStats := false, false
if p.Name != nil {
query.Name = *p.Name
}
@ -361,9 +364,15 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
query.Interval = *p.Interval
}
if p.Platform != nil {
if !comparePlatforms(query.Platform, *p.Platform) {
shouldDiscardQueryResults = true
}
query.Platform = *p.Platform
}
if p.MinOsqueryVersion != nil {
if query.MinOsqueryVersion != *p.MinOsqueryVersion {
shouldDiscardQueryResults = true
}
query.MinOsqueryVersion = *p.MinOsqueryVersion
}
if p.AutomationsEnabled != nil {
@ -405,6 +414,17 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
return query, nil
}
func comparePlatforms(platform1, platform2 string) bool {
if platform1 == platform2 {
return true
}
p1s := strings.Split(platform1, ",")
slices.Sort(p1s)
p2s := strings.Split(platform2, ",")
slices.Sort(p2s)
return slices.Compare(p1s, p2s) == 0
}
////////////////////////////////////////////////////////////////////////////////
// Delete Query
////////////////////////////////////////////////////////////////////////////////
@ -627,7 +647,9 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe
if (query.DiscardData && query.DiscardData != dbQuery.DiscardData) ||
(query.Logging != dbQuery.Logging && query.Logging != fleet.LoggingSnapshot) ||
query.Query != dbQuery.Query {
query.Query != dbQuery.Query ||
query.MinOsqueryVersion != dbQuery.MinOsqueryVersion ||
!comparePlatforms(query.Platform, dbQuery.Platform) {
queriesToDiscardResults[dbQuery.ID] = struct{}{}
}
}

View file

@ -714,3 +714,66 @@ func TestQueryReportReturnsNilIfDiscardDataIsTrue(t *testing.T) {
require.NoError(t, err)
require.Nil(t, results)
}
func TestComparePlatforms(t *testing.T) {
for _, tc := range []struct {
name string
p1 string
p2 string
expected bool
}{
{
name: "equal single value",
p1: "linux",
p2: "linux",
expected: true,
},
{
name: "different single value",
p1: "macos",
p2: "linux",
expected: false,
},
{
name: "equal multiple values",
p1: "linux,windows",
p2: "linux,windows",
expected: true,
},
{
name: "equal multiple values out of order",
p1: "linux,windows",
p2: "windows,linux",
expected: true,
},
{
name: "different multiple values",
p1: "linux,windows",
p2: "linux,windows,darwin",
expected: false,
},
{
name: "no values set",
p1: "",
p2: "",
expected: true,
},
{
name: "no values set",
p1: "",
p2: "linux",
expected: false,
},
{
name: "single and multiple values",
p1: "linux",
p2: "windows,linux",
expected: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
actual := comparePlatforms(tc.p1, tc.p2)
require.Equal(t, tc.expected, actual)
})
}
}

View file

@ -78,6 +78,7 @@ usage() {
echo " -r, --release_notes Update the release notes in the named release on github and exit (requires changelog output from running the script previously)."
echo " -s, --start_version Set the target starting version (can also be the first positional arg) for the release, defaults to latest release on github"
echo " -t, --target_date Set the target date for the release, defaults to today if not provided"
echo " -u, --publish_release Set's release from draft to release, deploys to dogfood."
echo " -v, --target_version Set the target version for the release"
echo ""
echo "Environment Variables:"
@ -219,6 +220,42 @@ update_release_notes() {
fi
}
publish() {
gh release edit --draft=false --latest $next_tag
gh workflow run dogfood-deploy.yml -f DOCKER_IMAGE=fleetdm/fleet:$next_ver
show_spinner 200
echo "========================================================================="
echo "Update osquery Slack Fleet channel topic to say the correct version $next_ver"
echo "========================================================================="
dogfood_deploy=`gh run list --workflow=dogfood-deploy.yml --status in_progress -L 1 --json url | jq -r '.[] | .url'`
cd tools/fleetctl-npm && npm publish
issues=`gh issue list -m $target_milestone --json number | jq -r '.[] | .number'`
for iss in $issues; do
echo "Closing #$iss"
gh issue close $iss
done
echo "Closing milestone"
gh api repos/fleetdm/fleet/milestones/$target_milestone_number -f state=closed
# Slack
slack_hook_url=https://hooks.slack.com/services
app_id=T019PP37ALW
general_channel_id=B06RZ60NUHX/tzaDZOvFCSvS2HC6rECi3Mvu
help_infra_channel_id=B06RLDFLC75/biuacbLxWRsDhv0hLA2qnLbX
help_eng_channel_id=B06RDTMUP1U/x2R36PXvW13KE6daxMiUK6W7
announce_text=":cloud: :rocket: The latest version of Fleet is $target_milestone.\nMore info: https://github.com/fleetdm/fleet/releases/tag/$next_tag\nUpgrade now: https://fleetdm.com/docs/deploying/upgrading-fleet"
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$announce_text\"}" \
$slack_hook_url/$app_id/$general_channel_id
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$announce_text\nDogfood Deployed $dogfood_deploy\"}" \
$slack_hook_url/$app_id/$help_infra_channel_id
}
# Validate we have all commands required to perform this script
check_required_binaries
@ -232,6 +269,7 @@ start_version=""
target_date=""
target_version=""
print_info=false
publish_release=false
release_notes=false
# Parse long options manually
@ -245,6 +283,7 @@ for arg in "$@"; do
"--minor") set -- "$@" "-m" ;;
"--open_api_key") set -- "$@" "-o" ;;
"--print") set -- "$@" "-p" ;;
"--publish_release") set -- "$@" "-u" ;;
"--release_notes") set -- "$@" "-r" ;;
"--start_version") set -- "$@" "-s" ;;
"--target_date") set -- "$@" "-t" ;;
@ -254,7 +293,7 @@ for arg in "$@"; do
done
# Extract options and their arguments using getopts
while getopts "cdfhmo:prs:t:v:" opt; do
while getopts "cdfhmo:prs:t:uv:" opt; do
case "$opt" in
c) cherry_pick_resolved=true ;;
d) dry_run=true ;;
@ -266,6 +305,7 @@ while getopts "cdfhmo:prs:t:v:" opt; do
r) release_notes=true ;;
s) start_version=$OPTARG ;;
t) target_date=$OPTARG ;;
u) publish_release=true ;;
v) target_version=$OPTARG ;;
?) usage; exit 1 ;;
esac
@ -356,10 +396,15 @@ if [ "$force" = "false" ]; then
;;
esac
fi
# 4.47.2
start_milestone="${start_version:1}"
# 4.47.3
target_milestone="${next_ver:1}"
# 79
target_milestone_number=`gh api repos/:owner/:repo/milestones | jq -r ".[] | select(.title==\"$target_milestone\") | .number"`
# patch-fleet-v4.47.3
target_patch_branch="patch-fleet-$next_ver"
# fleet-v4.47.3
next_tag="fleet-$next_ver"
if [ "$print_info" = "true" ]; then
@ -378,6 +423,11 @@ if [[ "$target_milestone_number" == "" ]]; then
fi
echo "Found milestone $target_milestone with number $target_milestone_number"
if [ "$publish_release" = "true" ]; then
publish
exit 0
fi
failed=false
if [ "$cherry_pick_resolved" = "false" ]; then
@ -550,6 +600,20 @@ if [[ "$failed" == "false" ]]; then
echo -e "${output}" >> temp_changelog
echo "" >> temp_changelog
cp CHANGELOG.md old_changelog
cat temp_changelog
echo
echo "About to write changelog"
if [ "$force" = "false" ]; then
read -r -p "Does the above changelog look good (edit temp_changelog now to make changes) (n exits)? [y/N] " response
case "$response" in
[yY][eE][sS]|[yY])
echo
;;
*)
exit 1
;;
esac
fi
cat temp_changelog > CHANGELOG.md
cat old_changelog >> CHANGELOG.md
rm -f old_changelog
@ -561,6 +625,15 @@ if [[ "$failed" == "false" ]]; then
fi
git checkout -b $update_changelog_patch_branch
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"`
unameOut="$(uname -s)"
case "${unameOut}" in
Linux*) echo "$version_files" | xargs sed -i "s/$escaped_start_version/$target_milestone/g";;
Darwin*) echo "$version_files" | xargs sed -i '' "s/$escaped_start_version/$target_milestone/g";;
*) echo "unknown distro to parse version"
esac
git add terraform charts infrastructure tools
git commit -m "Adding changes for patch $target_milestone"
git push origin $update_changelog_patch_branch -f
gh pr create -f -B $target_patch_branch
@ -622,7 +695,7 @@ if [[ "$failed" == "false" ]]; then
echo `gh pr view $update_changelog_patch_branch --json url | jq -r .url`
echo
waiting=true
while waiting; do
while $waiting; do
pr_state=`gh pr view $update_changelog_patch_branch --json state | jq -r .state`
if [[ "$pr_state" == "MERGED" ]]; then
waiting=false
@ -632,6 +705,19 @@ if [[ "$failed" == "false" ]]; then
done
git pull origin $target_patch_branch
echo "About to tag to $next_tag"
if [ "$force" = "false" ]; then
read -r -p "Did all steps succeed and is the tag ready to push? [y/N] " response
case "$response" in
[yY][eE][sS]|[yY])
echo
;;
*)
exit 1
;;
esac
fi
git tag $next_tag
git push origin $next_tag

View file

@ -31,7 +31,6 @@ module.exports = {
let util = require('util');
let topLvlRepoPath = path.resolve(sails.config.appPath, '../');
require('assert')(sails.config.custom.versionOfOsquerySchemaToUseWhenGeneratingDocumentation, 'Please set sails.config.custom.sails.config.custom.versionOfOsquerySchemaToUseWhenGeneratingDocumentation to the version of osquery to use, for example \'5.8.1\'.');
let VERSION_OF_OSQUERY_SCHEMA_TO_USE = sails.config.custom.versionOfOsquerySchemaToUseWhenGeneratingDocumentation;
@ -133,6 +132,9 @@ module.exports = {
// Examples are parsed as markdown, so we wrap the example in a code
// fence so it renders as a code block.
expandedTableToPush.examples = '```\n' + examplesFromOsquerySchema[examplesFromOsquerySchema.length - 1] + '\n```';
} else {
// If this table has an examples value that is an empty array, we'll completly remove it from the merged schema.
delete expandedTableToPush.examples;
}
if(includeLastModifiedAtValue) {
expandedTableToPush.lastModifiedAt = rawOsqueryTablesLastModifiedAt;
@ -165,6 +167,9 @@ module.exports = {
if (examplesFromOsquerySchema.length > 0) {
// Examples are parsed as markdown, so we wrap the example in a code fence so it renders as a code block.
expandedTableToPush.examples = '```\n' + examplesFromOsquerySchema[examplesFromOsquerySchema.length - 1] + '\n```';
} else {
// If this table has an examples value that is an empty array, we'll completly remove it from the merged schema.
delete expandedTableToPush.examples;
}
}
if(fleetOverridesForTable.notes !== undefined) {
@ -206,10 +211,6 @@ module.exports = {
} else {// If the Fleet overrides JSON has column data for this table, we'll find the matching column and use the values from the Fleet overrides in the final schema.
let columnHasFleetOverrides = _.find(fleetOverridesForTable.columns, {'name': osquerySchemaColumn.name});
if(!columnHasFleetOverrides) {// If this column has no Fleet overrides, we'll add it to the final schema unchanged
let columnWithNoOverrides = _.clone(osquerySchemaColumn);
if(osquerySchemaColumn.type !== undefined) {
columnWithNoOverrides.type = osquerySchemaColumn.type.toUpperCase();
}
mergedTableColumns.push(osquerySchemaColumn);
} else { // If this table has Fleet overrides, we'll adjust the value in the merged schema
let fleetColumn = _.clone(osquerySchemaColumn);
@ -234,7 +235,7 @@ module.exports = {
}
}
if(columnHasFleetOverrides.type !== undefined) {
fleetColumn.type = _.clone(columnHasFleetOverrides.type.toUpperCase());
fleetColumn.type = _.clone(columnHasFleetOverrides.type.toLowerCase());
}
if(columnHasFleetOverrides.required !== undefined) {
fleetColumn.required = _.clone(columnHasFleetOverrides.required);
@ -276,6 +277,7 @@ module.exports = {
if(typeof overrideColumnToAdd.type !== 'string') {
throw new Error(`The osquery tables could not be merged with the Fleet overrides. The "type" for the "${fleetOverrideColumn.name}" column of the "${fleetOverridesForTable.name}" table is an invalid type (${typeof fleetOverrideColumn.type}). To resolve, change the value of a column's "type" to be a string.`);
}//•
overrideColumnToAdd.type = overrideColumnToAdd.type.toLowerCase();
} else {
throw new Error(`The osquery tables could not be merged with the Fleet overrides. The "${fleetOverrideColumn.name}" column added to the merged schema for the "${fleetOverridesForTable.name}" table is missing a "type" in the Fleet overrides schema. To resolve, add a type for this column to the Fleet overrides schema.`);
}
@ -347,6 +349,7 @@ module.exports = {
} else if(typeof columnToValidate.type !== 'string') {
throw new Error(`Could not add a table from the Fleet overrides schema. The "type" of the "${columnToValidate.name}" column of the "${fleetOverrideToPush.name}" table at ${path.resolve(topLvlRepoPath+'/schema/tables', fleetOverrideToPush.name+'.yml')} has an invalid value. (expected a string, but got a ${typeof columnToValidate.type}) To resolve, change the value of the column's "type" be a string.`);
}//•
columnToValidate.type = columnToValidate.type.toLowerCase();
if(!columnToValidate.description) {
throw new Error(`Could not add a new table from the Fleet overrides schema. The "${columnToValidate.name}" column of the "${fleetOverrideToPush.name}" table is missing a "description". To resolve add a "description" property to the "${columnToValidate.name}" column at ${path.resolve(topLvlRepoPath+'/schema/tables', fleetOverrideToPush.name+'.yml')}`);

View file

@ -0,0 +1,92 @@
/**
* <logo-carousel>
* -----------------------------------------------------------------------------
* A row of logos that scroll infinitly to the left.
*
* @type {Component}
*
* -----------------------------------------------------------------------------
*/
parasails.registerComponent('logoCarousel', {
// ╔═╗╦═╗╔═╗╔═╗╔═╗
// ╠═╝╠╦╝║ ║╠═╝╚═╗
// ╩ ╩╚═╚═╝╩ ╚═╝
props: [],
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: function (){
return {
//…
};
},
// ╦ ╦╔╦╗╔╦╗╦
// ╠═╣ ║ ║║║║
// ╩ ╩ ╩ ╩ ╩╩═╝
template: `
<div purpose="logos" class="mx-auto d-flex flex-row align-items-center">
<div purpose="logo-carousel">
<div purpose="logo-row" class="d-flex flex-row align-items-center">
<img alt="Notion logo" src="/images/logo-notion-68x32@2x.png">
<img alt="Pinterest logo" src="/images/logo-pinterest-98x32@2x.png">
<img alt="Gusto logo" src="/images/logo-gusto-64x32@2x.png">
<img alt="Epic Games logo" style="height: 32px" src="/images/logo-epic-games-28x32@2x.png">
<img alt="Rivian logo" src="/images/logo-rivian-120x32@2x.png">
<img alt="Deloitte logo" src="/images/logo-deloitte-97x32@2x.png">
<img alt="Flywire logo" src="/images/logo-flywire-69x32@2x.png">
<img alt="Snowflake logo" src="/images/logo-snowflake-101x32@2x.png">
<img alt="Uber logo" src="/images/logo-uber-65x32@2x.png">
<img alt="Atlassian logo" src="/images/logo-atlassian-140x32@2x.png">
<img alt="Toast logo" src="/images/logo-toast-91x32@2x.png">
<img alt="Fastly logo" src="/images/logo-fastly-60x32@2x.png">
<img alt="Hashicorp logo" src="/images/logo-hashicorp-103x32@2x.png">
<img alt="Dropbox logo" src="/images/logo-dropbox-122x32@2x.png">
<img alt="Reddit logo" src="/images/logo-reddit-80x32@2x.png">
</div>
<div purpose="logo-row" class="d-flex flex-row align-items-center">
<img alt="Notion logo" src="/images/logo-notion-68x32@2x.png">
<img alt="Pinterest logo" src="/images/logo-pinterest-98x32@2x.png">
<img alt="Gusto logo" src="/images/logo-gusto-64x32@2x.png">
<img alt="Epic Games logo" style="height: 32px" src="/images/logo-epic-games-28x32@2x.png">
<img alt="Rivian logo" src="/images/logo-rivian-120x32@2x.png">
<img alt="Deloitte logo" src="/images/logo-deloitte-97x32@2x.png">
<img alt="Flywire logo" src="/images/logo-flywire-69x32@2x.png">
<img alt="Snowflake logo" src="/images/logo-snowflake-101x32@2x.png">
<img alt="Uber logo" src="/images/logo-uber-65x32@2x.png">
<img alt="Atlassian logo" src="/images/logo-atlassian-140x32@2x.png">
<img alt="Toast logo" src="/images/logo-toast-91x32@2x.png">
<img alt="Fastly logo" src="/images/logo-fastly-60x32@2x.png">
<img alt="Hashicorp logo" src="/images/logo-hashicorp-103x32@2x.png">
<img alt="Dropbox logo" src="/images/logo-dropbox-122x32@2x.png">
<img alt="Reddit logo" src="/images/logo-reddit-80x32@2x.png">
</div>
<div purpose="fade-left"></div>
<div purpose="fade-right"></div>
</div>
</div>
`,
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
//…
},
mounted: async function(){
//…
},
beforeDestroy: function() {
//…
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
//…
}
});

View file

@ -0,0 +1,66 @@
/**
* <ajax-button>
*
* App-wide styles for our ajax buttons.
*/
[parasails-component='logo-carousel'] {
margin-bottom: 80px;
max-width: 1200px;
padding-left: 0px;
padding-right: 0px;
width: 100%;
[purpose='logo-carousel'] {
justify-content: center;
display: flex;
align-items: center;
position: relative;
width: 100%;
overflow: hidden;
[purpose='logo-row'] {
white-space: nowrap;
animation: scroll-horizontal 80s linear infinite;
}
img {
vertical-align: middle;
height: 32px;
margin-right: 64px;
}
[purpose='fade-left'] {
height: 32px;
width: 80px;
position: absolute;
left: 0px;
bottom: 0px;
animation: none;
background: linear-gradient(90deg, #FFF 0%, rgba(255, 255, 255, 0.00) 100%);
}
[purpose='fade-right'] {
height: 32px;
width: 80px;
position: absolute;
right: 0px;
bottom: 0px;
animation: none;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%);
}
}
@media (max-width: 575px) {
margin-bottom: 64px;
[purpose='logo-carousel'] {
img {
margin-right: 48px;
}
}
}
@keyframes scroll-horizontal {
0% {
transform: translateX(50%);
}
100% {
transform: translateX(-50%);
}
}
}

View file

@ -26,6 +26,7 @@
@import 'components/call-to-action.component.less';
@import 'components/scrollable-tweets.component.less';
@import 'components/parallax-city.component.less';
@import 'components/logo-carousel.component.less';
// Per-page styles
@import 'pages/homepage.less';

View file

@ -304,22 +304,23 @@ html, body {
}
}
}
[purpose='admin-nav'] {
justify-content: center;
a {
margin-left: 30px;
margin-right: 30px;
color: @core-fleet-black-75;
&:hover {
color: @core-fleet-black;
}
}
span {
color: @core-fleet-black;
font-weight: 600;
margin-right: 30px;
}
[purpose='admin-nav'] {
justify-content: center;
[purpose='admin-link'] {
margin-left: 30px;
margin-right: 30px;
color: @core-fleet-black-75;
&:hover {
color: @core-vibrant-blue;
}
}
span {
color: @core-fleet-black;
font-weight: 600;
margin-right: 30px;
}
}
// Footer styles

View file

@ -9,6 +9,8 @@
a {
color: @core-fleet-black-75;
text-decoration: underline;
text-underline-offset: 2px;
line-height: 150%;
}
[purpose='customer-login-container'] {
max-width: 560px;
@ -23,12 +25,24 @@
padding-left: 30px;
padding-right: 30px;
text-align: center;
margin-bottom: 40px;
}
[purpose='register-link'] {
margin-bottom: 8px;
a {
float: right;
color: @core-fleet-black-75;
text-decoration: underline;
font-size: 14px;
}
}
[purpose='customer-portal-form'] {
max-width: 560px;
border-radius: 16px;
padding: 30px;
margin-bottom: 40px;
padding: 20px 32px 32px 32px;
label {
color: @core-fleet-black;
font-weight: 700;
margin-bottom: 4px;
}
@ -62,27 +76,6 @@
}
}
[purpose='features-list'] {
word-wrap: overflow;
padding-left: 40px;
font-size: 14px;
ul {
list-style-type: none;
padding-inline-start: 0px;
}
li {
padding-bottom: 12px;
color: @core-fleet-black-75;
}
img {
display: inline;
height: 16px;
margin-right: 8px;
}
}
@media (max-width: 768px) {
padding-top: 60px;
[purpose='customer-portal-form'] {

View file

@ -7,17 +7,31 @@
a {
color: @core-fleet-black-75;
text-decoration: underline;
text-underline-offset: 2px;
}
[purpose='page-heading'] {
padding-left: 30px;
padding-right: 30px;
text-align: center;
margin-bottom: 40px;
}
[purpose='login-link'] {
margin-bottom: 4px;
a {
float: right;
color: @core-fleet-black-75;
text-decoration: underline;
font-size: 14px;
}
}
[purpose='customer-portal-form'] {
max-width: 560px;
border-radius: 16px;
padding: 30px;
margin-bottom: 40px;
padding: 20px 32px 32px 32px;
label {
color: @core-fleet-black;
font-weight: 700;
margin-bottom: 4px;
}
@ -25,9 +39,6 @@
height: 40px;
border-radius: 6px;
}
.card-body {
padding: 2em;
}
.selectbox {
position: relative;
@ -69,26 +80,6 @@
}
}
[purpose='features-list'] {
padding-left: 40px;
font-size: 14px;
word-wrap: overflow;
ul {
list-style-type: none;
padding-inline-start: 0px;
}
li {
padding-bottom: 12px;
color: @core-fleet-black-75;
white-space: nowrap;
}
img {
display: inline;
height: 16px;
margin-right: 8px;
}
}
@media (max-width: 768px) {
padding-top: 60px;

View file

@ -68,57 +68,11 @@
}
[purpose='hero-logos'] {
margin-bottom: 80px;
max-width: 1200px;
padding-left: 60px;
padding-right: 60px;
overflow-x: hidden;
width: 100%;
}
[purpose='logo-carousel'] {
display: flex;
justify-content: space-around;
align-items: center;
position: relative;
width: 100%;
overflow: hidden;
[purpose='logo-row'] {
white-space: nowrap;
animation: scroll-horizontal 80s linear infinite;
}
img {
vertical-align: middle;
height: 32px;
margin-right: 64px;
}
[purpose='fade-left'] {
height: 32px;
width: 80px;
position: absolute;
left: 0px;
bottom: 0px;
animation: none;
background: linear-gradient(90deg, #FFF 0%, rgba(255, 255, 255, 0.00) 100%);
}
[purpose='fade-right'] {
height: 32px;
width: 80px;
position: absolute;
right: 0px;
bottom: 0px;
animation: none;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%);
}
}
@keyframes scroll-horizontal {
0% {
transform: translateX(-25%);
}
100% {
transform: translateX(-125%);
}
}
[purpose='homepage-content'] {
max-width: 1200px;
@ -1143,26 +1097,6 @@
font-size: 16px;
}
}
[purpose='hero-logos'] {
[purpose='wayfair-logo'] {
margin-right: 0px;
}
[purpose='uber-logo'] {
margin-left: 0px;
margin-right: auto;
}
[purpose='atlassian-logo'] {
margin-left: auto;
margin-right: 0px;
}
[purpose='fastly-logo'] {
margin-left: auto;
margin-right: auto;
}
[purpose='gusto-logo'] {
margin-left: 0px;
}
}
[purpose='platform-block'] {
margin-bottom: 100px;
}
@ -1285,11 +1219,6 @@
padding-left: 20px;
padding-right: 20px;
}
[purpose='logo-carousel'] {
img {
margin-right: 48px;
}
}
[purpose='video-modal'] {
[purpose='modal-content'] {
width: 95vw;
@ -1387,11 +1316,6 @@
padding-right: 20px;
padding-left: 20px;
}
[purpose='hero-logos'] {
img {
display: inline;
}
}
[purpose='platform-block'] {
margin-bottom: 80px;
}

View file

@ -8,10 +8,16 @@
font-weight: 800;
line-height: 150%;
}
[purpose='logo-container'] {
max-width: 524px;
margin-left: auto;
margin-right: auto;
}
[purpose='page-container'] {
padding-top: 80px;
padding-left: 64px;
padding-right: 64px;
max-width: unset;
display: flex;
flex-direction: column;
}
@ -67,6 +73,7 @@
padding-top: 60px;
padding-left: 40px;
padding-right: 40px;
max-width: 600px;
}
}
@ -79,9 +86,11 @@
}
@media (max-width: 575px) {
[purpose='logo-container'] {
max-width: 100%;
}
[purpose='start-cards'] {
flex-direction: column;
padding-bottom: 120px;
}
[purpose='card']:first-of-type {
margin-right: unset;

View file

@ -124,15 +124,14 @@ module.exports.custom = {
'CHANGELOG.md': 'lukeheath',
// 🫧 Website (fleetdm.com)
'website': 'mikermcneil',// (catch-all)
'website': 'eashaw',// (catch-all)
'website/assets': 'eashaw', // « Eric is DRI for website frontend code
'website/views': 'eashaw',
'website/api': 'mikermcneil',//« Website backend, scripts, deps
'website/api/controllers/webhooks/receive-from-github.js': 'mikermcneil',// github bot (webhook)
'website/api/controllers/imagine': 'eashaw',// landing pages
'website/config': 'mikermcneil',
'website/api': 'eashaw',//« Website backend, scripts, deps
'website/api/controllers/webhooks/receive-from-github.js': 'eashaw',// github bot (webhook)
'website/config': 'eashaw',
'website/config/routes.js': 'eashaw',//« Website redirects and URLs
'website/scripts': 'mikermcneil',
'website/scripts': 'eashaw',
'website/package.json': 'eashaw',
// 🫧 Vulnerability dashboard
@ -204,7 +203,7 @@ module.exports.custom = {
'website/assets/images/articles': ['spokanemac', 'mike-j-thomas', 'mike-j-thomas', 'eashaw', 'mikermcneil'],
// Website (fleetdm.com)
'website': 'mikermcneil',// (default for website)
'website': ['mikermcneil', 'eashaw'],// (default for website)
'website/views': 'eashaw',
'website/generators': 'eashaw',
'website/assets': 'eashaw',
@ -244,6 +243,7 @@ module.exports.custom = {
'handbook/README.md': 'mikermcneil', // See https://github.com/fleetdm/fleet/pull/13195
'handbook/company': 'mikermcneil',
'handbook/company/product-groups.md': ['lukeheath', 'sampfluger88','mikermcneil'],
'handbook/company/open-positions.yml': ['@sampfluger88','mikermcneil'],
'handbook/digital-experience': ['sampfluger88','mikermcneil'],
'handbook/business-operations': ['sampfluger88','mikermcneil'],
'handbook/engineering': ['sampfluger88','mikermcneil', 'lukeheath'],
@ -251,7 +251,6 @@ module.exports.custom = {
'handbook/sales': ['sampfluger88','mikermcneil'],
'handbook/demand': ['sampfluger88','mikermcneil'],
'handbook/customer-success': ['sampfluger88','mikermcneil'],
'/handbook/company/testimonials.yml': ['eashaw', 'mike-j-thomas', 'sampfluger88', 'mikermcneil'],
// GitHub issue templates

View file

@ -705,51 +705,52 @@ module.exports = {
// Iterate through the columns of the table, we'll add a row to the markdown table element for each column in this schema table
for(let column of _.sortBy(table.columns, 'name')) {
if(!column.hidden) { // If the column is hidden, we won't add it to the final table.
// Create an object for this column to add to the osqueryTables config.
let columnInfoForQueryReports = {
name: column.name
};
let columnDescriptionForTable = '';// Set the initial value of the description that will be added to the table for this column.
if(column.description) {
columnDescriptionForTable = column.description;
// Convert the markdown description for this table into HTML for tooltips on /try-fleet/explore-data/* pages
columnInfoForQueryReports.description = await sails.helpers.strings.toHtml.with({mdString: column.description, addIdsToHeadings: false});
}
tableInfoForQueryReports.columns.push(columnInfoForQueryReports);
// Replacing pipe characters and newlines with html entities in column descriptions to keep it from breaking markdown tables.
columnDescriptionForTable = columnDescriptionForTable.replace(/\|/g, '&#124;').replace(/\n/gm, '&#10;');
keywordsForSyntaxHighlighting.push(column.name);
if(column.required) { // If a column has `"required": true`, we'll add a note to the description that will be added to the table
columnDescriptionForTable += '<br> **Required in `WHERE` clause** ';
}
if(column.requires_user_context) { // If a column has `"requires_user_context": true`, we'll add a note to the description that will be added to the table
columnDescriptionForTable += '<br> **Defaults to root** &nbsp;&nbsp;[Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table?utm_source=fleetdm.com&utm_content=table-'+encodeURIComponent(table.name)+')';
}
if(column.platforms) { // If a column has an array of platforms, we'll add a note to the final column description
let platformString = '<br> **Only available on ';// start building a string to add to the column's description
if(column.platforms.length > 3) {// FUTURE: add support for more than three platform values in columns.
throw new Error('Support for more than three platforms has not been implemented yet.');
}
if(column.platforms.length === 3) { // Because there are only four options for platform, we can safely assume that there will be at most 3 platforms, so we'll just handle this one of three ways
// If there are three, we'll add a string with an oxford comma. e.g., "On macOS, Windows, and Linux"
platformString += `${column.platforms[0]}, ${column.platforms[1]}, and ${column.platforms[2]}`;
} else if(column.platforms.length === 2) {
// If there are two values in the platforms array, it will be formated as "[Platform 1] and [Platform 2]"
platformString += `${column.platforms[0]} and ${column.platforms[1]}`;
} else {
// Otherwise, there is only one value in the platform array and we'll add that value to the column's description
platformString += column.platforms[0];
}
platformString += '** ';
columnDescriptionForTable += platformString; // Add the platform string to the column's description.
}
tableMdString += ' | '+column.name+' | '+ column.type +' | '+columnDescriptionForTable+'|\n';
// Create an object for this column to add to the osqueryTables config.
let columnInfoForQueryReports = {
name: column.name
};
let columnDescriptionForTable = '';// Set the initial value of the description that will be added to the table for this column.
if(column.description) {
columnDescriptionForTable = column.description;
// Convert the markdown description for this table into HTML for tooltips on /try-fleet/explore-data/* pages
columnInfoForQueryReports.description = await sails.helpers.strings.toHtml.with({mdString: column.description, addIdsToHeadings: false});
}
tableInfoForQueryReports.columns.push(columnInfoForQueryReports);
// Replacing pipe characters and newlines with html entities in column descriptions to keep it from breaking markdown tables.
columnDescriptionForTable = columnDescriptionForTable.replace(/\|/g, '&#124;').replace(/\n/gm, '&#10;');
keywordsForSyntaxHighlighting.push(column.name);
if(column.required) { // If a column has `"required": true`, we'll add a note to the description that will be added to the table
columnDescriptionForTable += '<br> **Required in `WHERE` clause** ';
}
if(column.requires_user_context) { // If a column has `"requires_user_context": true`, we'll add a note to the description that will be added to the table
columnDescriptionForTable += '<br> **Defaults to root** &nbsp;&nbsp;[Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table?utm_source=fleetdm.com&utm_content=table-'+encodeURIComponent(table.name)+')';
}
if(column.hidden) { // If a column has `"hidden": true`, we'll add a note to the description that will be added to the table
columnDescriptionForTable += '<br> **Not returned in `SELECT * FROM '+table.name+'`.**';
}
if(column.platforms) { // If a column has an array of platforms, we'll add a note to the final column description
let platformString = '<br> **Only available on ';// start building a string to add to the column's description
if(column.platforms.length > 3) {// FUTURE: add support for more than three platform values in columns.
throw new Error('Support for more than three platforms in columns has not been implemented yet. If this column is supported on all platforms, you can omit the platforms array entirely.');
}
if(column.platforms.length === 3) { // Because there are only four options for platform, we can safely assume that there will be at most 3 platforms, so we'll just handle this one of three ways
// If there are three, we'll add a string with an oxford comma. e.g., "On macOS, Windows, and Linux"
platformString += `${column.platforms[0]}, ${column.platforms[1]}, and ${column.platforms[2]}`;
} else if(column.platforms.length === 2) {
// If there are two values in the platforms array, it will be formated as "[Platform 1] and [Platform 2]"
platformString += `${column.platforms[0]} and ${column.platforms[1]}`;
} else {
// Otherwise, there is only one value in the platform array and we'll add that value to the column's description
platformString += column.platforms[0];
}
platformString += '** ';
columnDescriptionForTable += platformString; // Add the platform string to the column's description.
}
tableMdString += ' | '+column.name+' | '+ column.type +' | '+columnDescriptionForTable+'|\n';
}
if(table.examples) { // If this table has a examples value (These will be in the Fleet schema JSON) We'll add the examples to the markdown string.
tableMdString += '\n### Example\n\n'+table.examples+'\n';

View file

@ -207,8 +207,16 @@
<a purpose="mobile-dropdown-link" href="/pricing" class="d-flex align-items-center">Pricing</a>
<% if(_.has(me, 'id')) {%>
<hr>
<a purpose="mobile-nav-btn" href="/logout" class="d-flex mt-2 text-decoration-none">Log out</a>
<a purpose="mobile-dropdown-link" href="/logout" class="d-flex mt-2 text-decoration-none">Log out</a>
<% }%>
<%if(me && me.isSuperAdmin && showAdminLinks) {%>
<hr>
<a purpose="mobile-dropdown-toggle" class="d-flex align-items-center mr-4 collapsed" data-toggle="collapse" data-target="#mobileNavbarToggleAdmin">Admin</a>
<div id="mobileNavbarToggleAdmin" purpose="mobile-dropdown" class="collapse" data-parent="#mobileDropdowns">
<a purpose="mobile-dropdown-link" href="/admin/generate-license">License generator</a>
<a purpose="mobile-dropdown-link" href="/admin/email-preview">HTML Email preview tool</a>
</div>
<%}%>
<a purpose="glass-header-btn" style="padding: 4px 16px; line-height: 24px; width: 100px" class="btn btn-sm btn-primary align-items-center d-flex mt-4" href="/contact">Talk to us</a>
</div>
</div>
@ -275,14 +283,13 @@
</div>
<% } %>
<%if(me && me.isSuperAdmin && showAdminLinks) {%>
<div purpose="admin-nav" class="d-flex flex-row align-items-center py-2 px-1">
<div purpose="admin-nav" class="d-lg-flex d-none flex-row align-items-center justify-content-center py-2 px-1">
<div class="justify-self-start">
<span>Admin pages</span>
</div>
<div class="d-flex flex-row align-self-end">
<a style="text-decoration: none; line-height: 23px;" href="/admin/generate-license">License generator</a>
<a style="text-decoration: none; line-height: 23px;" href="/admin/email-preview">HTML Email preview tool</a>
<a style="text-decoration: none; line-height: 23px;" href="/admin/sandbox-waitlist">Manage Fleet Sandbox waitlist</a>
<div class="d-flex flex-row align-self-end justify-content-between">
<a purpose="admin-link" style="text-decoration: none; line-height: 23px;" href="/admin/generate-license">License generator</a>
<a purpose="admin-link" style="text-decoration: none; line-height: 23px;" href="/admin/email-preview">HTML Email preview tool</a>
</div>
</div>
<%} %>
@ -479,6 +486,7 @@
<script src="/js/components/call-to-action.component.js"></script>
<script src="/js/components/cloud-error.component.js"></script>
<script src="/js/components/js-timestamp.component.js"></script>
<script src="/js/components/logo-carousel.component.js"></script>
<script src="/js/components/modal.component.js"></script>
<script src="/js/components/open-positions.component.js"></script>
<script src="/js/components/parallax-city.component.js"></script>

View file

@ -2,17 +2,19 @@
<div :purpose="[showCustomerLogin ? 'customer-login-container' : 'login-container']" class="container-fluid pb-5 px-lg-0 px-3">
<div purpose="page-heading" v-if="showCustomerLogin">
<h1>Welcome to Fleet</h1>
<p class="pb-2">We just need a few details in order to get started.</p>
<p class="mb-0">We just need a few details in order to get started.</p>
</div>
<div purpose="page-heading" v-else>
<h1>Welcome to Fleet</h1>
<p class="pb-2">Sign in to your Fleet account.</p>
<p class="mb-0">Sign in to your Fleet account.</p>
</div>
<div purpose="customer-portal-form" class="card card-body">
<div purpose="customer-portal-form" class="card card-body mb-5">
<div purpose="register-link" v-if="showCustomerLogin">
<a href="/register">Create an account</a>
</div>
<ajax-form class="customers-login" action="login" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-data="formData" :form-rules="formRules" :form-errors.sync="formErrors" @submitted="submittedForm()">
<div class="form-group">
<label for="email">Email</label>
<span style="float: right" class="text-right small" v-if="showCustomerLogin"><a href="/register">Create an account</a></span>
<input type="email" class="form-control" :class="[formErrors.emailAddress ? 'is-invalid' : '']" v-model.trim="formData.emailAddress" autocomplete="email" focus-first>
<div class="invalid-feedback" v-if="formErrors.emailAddress">Please provide a valid email address.</div>
</div>
@ -30,6 +32,7 @@
</ajax-form>
<span class="text-center small"><a href="/customers/forgot-password">Forgot your password?</a></span>
</div>
<logo-carousel></logo-carousel>
</div>
</div>
<%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %>

View file

@ -1,14 +1,16 @@
<div id="signup" v-cloak>
<div style="max-width: 560px;" class="container-fluid pb-5 px-lg-0 px-3">
<div purpose="page-heading">
<h1 class="text-center">Welcome to Fleet</h1>
<p class="text-center pb-2">We just need a few details in order to get started.</p>
<h1>Welcome to Fleet</h1>
<p class="mb-0">We just need a few details in order to get started.</p>
</div>
<div purpose="customer-portal-form" class="card card-body">
<div purpose="login-link">
<a href="/login">I have an account</a>
</div>
<ajax-form action="signup" class="self-service-register" :syncing.sync="syncing" :cloud-error.sync="cloudError" :form-errors.sync="formErrors" :form-data="formData" :form-rules="formRules" @submitted="submittedSignUpForm()">
<div class="form-group">
<label for="email-address">Work email *</label>
<span style="float: right" class="text-right small"><a href="/login">I have an account</a></span>
<input class="form-control" id="email-address" :class="[formErrors.emailAddress ? 'is-invalid' : '']" v-model.trim="formData.emailAddress" @input="typeClearOneFormError('emailAddress')">
<div class="invalid-feedback" v-if="formErrors.emailAddress" focus-first>This doesnt appear to be a valid email address</div>
</div>
@ -67,7 +69,7 @@
<ajax-button purpose="submit-button" type="button" :syncing="syncing" class="btn btn-block btn-lg btn-primary mt-4" v-if="cloudError" @click="clickResetForm()">Try again</ajax-button>
</ajax-form>
</div>
<logo-carousel></logo-carousel>
</div>
</div>
<%- /* Expose locals as `window.SAILS_LOCALS` :: */ exposeLocalsToBrowser() %>

View file

@ -19,70 +19,8 @@
</div>
</div>
<%/* Row of logos */%>
<div purpose="hero-logos" class="mx-auto d-flex flex-row align-items-center justify-content-center">
<div purpose="logo-carousel">
<div purpose="logo-row" class="d-flex flex-row align-items-center">
<img alt="Notion logo" src="/images/logo-notion-68x32@2x.png">
<img alt="Pinterest logo" src="/images/logo-pinterest-98x32@2x.png">
<img alt="Gusto logo" src="/images/logo-gusto-64x32@2x.png">
<img alt="Epic Games logo" style="height: 32px" src="/images/logo-epic-games-28x32@2x.png">
<img alt="Rivian logo" src="/images/logo-rivian-120x32@2x.png">
<img alt="Deloitte logo" src="/images/logo-deloitte-97x32@2x.png">
<img alt="Flywire logo" src="/images/logo-flywire-69x32@2x.png">
<img alt="Snowflake logo" src="/images/logo-snowflake-101x32@2x.png">
<img alt="Uber logo" src="/images/logo-uber-65x32@2x.png">
<img alt="Atlassian logo" src="/images/logo-atlassian-140x32@2x.png">
<img alt="Toast logo" src="/images/logo-toast-91x32@2x.png">
<img alt="Fastly logo" src="/images/logo-fastly-60x32@2x.png">
<img alt="Hashicorp logo" src="/images/logo-hashicorp-103x32@2x.png">
<img alt="Dropbox logo" src="/images/logo-dropbox-122x32@2x.png">
<img alt="Reddit logo" src="/images/logo-reddit-80x32@2x.png">
</div>
<div purpose="logo-row" class="d-flex flex-row align-items-center">
<img alt="Notion logo" src="/images/logo-notion-68x32@2x.png">
<img alt="Pinterest logo" src="/images/logo-pinterest-98x32@2x.png">
<img alt="Gusto logo" src="/images/logo-gusto-64x32@2x.png">
<img alt="Epic Games logo" style="height: 32px" src="/images/logo-epic-games-28x32@2x.png">
<img alt="Rivian logo" src="/images/logo-rivian-120x32@2x.png">
<img alt="Deloitte logo" src="/images/logo-deloitte-97x32@2x.png">
<img alt="Flywire logo" src="/images/logo-flywire-69x32@2x.png">
<img alt="Snowflake logo" src="/images/logo-snowflake-101x32@2x.png">
<img alt="Uber logo" src="/images/logo-uber-65x32@2x.png">
<img alt="Atlassian logo" src="/images/logo-atlassian-140x32@2x.png">
<img alt="Toast logo" src="/images/logo-toast-91x32@2x.png">
<img alt="Fastly logo" src="/images/logo-fastly-60x32@2x.png">
<img alt="Hashicorp logo" src="/images/logo-hashicorp-103x32@2x.png">
<img alt="Dropbox logo" src="/images/logo-dropbox-122x32@2x.png">
<img alt="Reddit logo" src="/images/logo-reddit-80x32@2x.png">
</div>
<div purpose="logo-row" class="d-flex flex-row align-items-center">
<img alt="Notion logo" src="/images/logo-notion-68x32@2x.png">
<img alt="Pinterest logo" src="/images/logo-pinterest-98x32@2x.png">
<img alt="Gusto logo" src="/images/logo-gusto-64x32@2x.png">
<img alt="Epic Games logo" style="height: 32px" src="/images/logo-epic-games-28x32@2x.png">
<img alt="Rivian logo" src="/images/logo-rivian-120x32@2x.png">
<img alt="Deloitte logo" src="/images/logo-deloitte-97x32@2x.png">
<img alt="Flywire logo" src="/images/logo-flywire-69x32@2x.png">
<img alt="Snowflake logo" src="/images/logo-snowflake-101x32@2x.png">
<img alt="Uber logo" src="/images/logo-uber-65x32@2x.png">
<img alt="Atlassian logo" src="/images/logo-atlassian-140x32@2x.png">
<img alt="Toast logo" src="/images/logo-toast-91x32@2x.png">
<img alt="Fastly logo" src="/images/logo-fastly-60x32@2x.png">
<img alt="Hashicorp logo" src="/images/logo-hashicorp-103x32@2x.png">
<img alt="Dropbox logo" src="/images/logo-dropbox-122x32@2x.png">
<img alt="Reddit logo" src="/images/logo-reddit-80x32@2x.png">
</div>
<div purpose="fade-left"></div>
<div purpose="fade-right"></div>
</div>
<div purpose="hero-logos" class="mx-auto">
<logo-carousel></logo-carousel>
</div>
<%/* Homepage content */%>
<div purpose="homepage-content" class="container">

View file

@ -16,6 +16,9 @@
<p>Purchase a Fleet Premium license</p>
</a>
</div>
<div purpose="logo-container">
<logo-carousel></logo-carousel>
</div>
</div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>