Merge branch 'main' into 15919-vulnerabilities-page

This commit is contained in:
Jacob Shandling 2024-02-21 10:42:21 -08:00
commit 1cf7362968
121 changed files with 2809 additions and 1250 deletions

View File

@ -22,7 +22,7 @@ It is [planned and ready](https://fleetdm.com/handbook/company/development-group
## Changes
### Product
- [ ] UI changes: TODO <!-- Insert the link to the relevant Figma file describing all relevant changes. Remove this checkbox if there are no changes to the user interface. -->
- [ ] UI changes: TODO <!-- Insert the link to the relevant Figma cover page. Remove this checkbox if there are no changes to the user interface. -->
- [ ] CLI usage changes: TODO <!-- Specify what changes to the CLI usage are required. Remove this checkbox if there are no changes to the CLI. -->
- [ ] REST API changes: TODO <!-- Specify what changes to the API are required. Remove this checkbox if there are no changes necessary. The product manager may move this item to the engineering list below if they decide that engineering will design the API changes. -->
- [ ] Permissions changes: TODO <!-- Specify what changes to the permissions are required. Remove this checkbox if there are no changes necessary. -->

55
.github/workflows/fleetd-tuf.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Update documentation of current versions of TUF fleetd components
on:
workflow_dispatch: # Manual
schedule:
- cron: '0 3 * * *' # Nightly 3AM UTC
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
cancel-in-progress: true
defaults:
run:
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
shell: bash
permissions:
contents: read
jobs:
update-fleetd-tuf:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: ${{ vars.GO_VERSION }}
- name: Checkout Code
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with:
fetch-depth: 0
- name: Update orbit/TUF.md
run: |
make fleetd-tuf
- name: PR changes
uses: peter-evans/create-pull-request@f22a7da129c901513876a2380e2dae9f8e145330 # v3.12.1
with:
base: main
branch: update-versions-of-fleetd-components-tuf
delete-branch: true
title: Update versions of fleetd components in Fleet's TUF [automated]
commit-message: |
Update versions of fleetd components in Fleet's TUF [automated]
Generated automatically with tools/tuf/status.
body: Automated change from [GitHub action](https://github.com/fleetdm/fleet/actions/workflows/fleetd-tuf.yml).

View File

@ -65,7 +65,7 @@ go.mod @fleetdm/go
/docs @rachaelshaw
/docs/Using-Fleet/REST-API.md @rachaelshaw # « REST API reference documentation
/docs/Contributing/API-for-contributors.md @rachaelshaw # « Advanced / contributors-only API reference documentation
/schema @rachaelshaw # « Data tables (osquery/fleetd schema) documentation
/schema @eashaw # « Data tables (osquery/fleetd schema) documentation
##############################################################################################
# 🫧 Pricing and features
@ -84,7 +84,8 @@ go.mod @fleetdm/go
#
# (see website/config/custom.js for DRIs of other paths not listed here)
##############################################################################################
/handbook @mikermcneil # See https://github.com/fleetdm/fleet/pull/13195
/handbook/company @mikermcneil
/handbook/README.md @mikermcneil
/handbook/business-operations @sampfluger88
/handbook/digital-experience @sampfluger88
/handbook/customer-success @sampfluger88

View File

@ -322,6 +322,14 @@ changelog-orbit:
sh -c "cat new-CHANGELOG.md orbit/CHANGELOG.md > tmp-CHANGELOG.md && rm new-CHANGELOG.md && mv tmp-CHANGELOG.md orbit/CHANGELOG.md"
sh -c "git rm orbit/changes/*"
# Updates the documentation for the currently released versions of fleetd components in Fleet's TUF.
fleetd-tuf:
sh -c 'echo "<!-- DO NOT EDIT. This document is automatically generated by running \`make fleetd-tuf\`. -->\n# tuf.fleetctl.com\n\nFollowing are the currently deployed versions of fleetd components on the \`stable\` and \`edge\` channel.\n" > orbit/TUF.md'
sh -c 'echo "## \`stable\`\n" >> orbit/TUF.md'
sh -c 'go run tools/tuf/status/tuf-status.go channel-version -channel stable -format markdown >> orbit/TUF.md'
sh -c 'echo "\n## \`edge\`\n" >> orbit/TUF.md'
sh -c 'go run tools/tuf/status/tuf-status.go channel-version -channel edge -format markdown >> orbit/TUF.md'
###
# Development DB commands
###

View File

@ -0,0 +1,47 @@
# Osquery 5.11.0 | VSCode, Apple silicon, and more.
![osquery 5.11.0](../website/assets/images/articles/osquery-5.11.0-cover-1600x900@2x.png)
Osquery 5.11 introduces enhancements that include the `vscode_extensions` table for inventorying VSCode extensions, additional Apple Silicon support columns in the `secureboot` table, Windows shortcut metadata parsing in the `file` table, and caching mechanisms for macOS keychain tables to prevent corruption. Openness is a key Fleet value. We welcome contributions to Fleet and find ways to contribute to other open-source projects. When you support Fleet, you are also contributing to projects like osquery. Lets take a look at the changes in this latest release.
Please note that osquery 5.11 has already been pushed to Fleets stable and edge auto-update channels.
## Highlights
* VSCode extensions table
* Apple silicon support added to `secureboot` table
* Shortcut metadata parsing on Windows
* Preventing keychain corruption with smart caching
### VSCode extensions table
Osquery introduces a new table named `vscode_extensions`, which expands the tool's capabilities in inventory management. This addition allows for the enumeration of extensions installed in Visual Studio Code (VSCode), providing valuable insights into the development environments across managed devices. With this table, IT and security teams can efficiently gather detailed information about VSCode extensions, aiding in compliance checks, security assessments, and the overall management of software assets.
### Apple silicon support added to `secureboot` table
The `secureboot` table in osquery has been updated to include new columns that provide deeper insights into the security configurations of Apple Silicon devices. The added columns are "description," "allow_kernel_extensions," and "allow_mdm_operations," which reflect the settings available in the Startup Security Utility of macOS. This enhancement enables a more detailed analysis of secure boot settings, facilitating better security posture assessments for Apple Silicon devices. This contribution was made by Zach Wasserman, CTO of Fleet.
### Shortcut metadata parsing on Windows
The `file` table in osquery has been enhanced to include parsing for shortcut metadata on Windows systems. This update allows for extracting and analyzing information from Windows shortcut files (`.lnk` files), such as target path, arguments, and other relevant shortcut details. This feature provides a more comprehensive understanding of the files present on Windows hosts, aiding in forensic investigations and system audits by offering insights into shortcut configurations and their associated actions. This addition further extends osquery's utility in providing detailed system information and contributes to its role as a valuable tool for IT and security professionals.
### Preventing keychain corruption with smart caching
Caching and throttling mechanisms have been introduced for the `certificates`, `keychain_acls`, and `keychain_items` tables on macOS to mitigate the risk of keychain corruption, a known issue from unstable macOS APIs. The new cache system evaluates if a keychain file has been altered by comparing its SHA256 hash to previous accesses. Should the file remain unchanged or accessed within a preset interval, osquery will reuse the cached results, reducing unnecessary file reads.
This caching operates individually across each table, allowing concurrent yet controlled access to keychain files, enhancing osquery's efficiency and stability on macOS platforms. This significant enhancement was contributed by Fleetie, Victor Lyuboslavsky.
<meta name="category" value="releases">
<meta name="authorFullName" value="JD Strong">
<meta name="authorGitHubUsername" value="spokanemac">
<meta name="publishedOn" value="2024-02-16">
<meta name="articleTitle" value="osquery 5.11.0 | VSCode, Apple silicon, and more">
<meta name="articleImageUrl" value="../website/assets/images/articles/osquery-5.11.0-cover-1600x900@2x.png">

View File

@ -11,7 +11,7 @@ If you would like to manage hosts that can travel outside your VPN or intranet,
## Using Fleet Desktop on remote devices
If you are using Fleet Desktop and want it to work on remote devices, the bare minimum API to expose is `/api/latest/fleet/device/*/desktop`. This minimal endpoint will only provide the number of failing policies.
If you are using Fleet Desktop and want it to work on remote devices, the bare minimum API to expose is `/api/*/fleet/device/*/desktop`. This minimal endpoint will only provide the number of failing policies.
For full Fleet Desktop and scripts functionality, `/api/fleet/orbit/*` and`/api/fleet/device/ping` must also be exposed.
@ -20,23 +20,59 @@ For full Fleet Desktop and scripts functionality, `/api/fleet/orbit/*` and`/api/
If you would like to use the fleetctl CLI from outside of your network, the following endpoints will also need to be exposed for `fleetctl`:
- `/api/setup`
- `/api/v1/setup`
- `/api/latest/fleet/*`
- `/api/v1/fleet/*`
- `/api/*/setup`
- `/api/*/fleet/*`
## Using Fleet's MDM features
If you would like to use Fleet's MDM features, the following endpoints need to be exposed:
### macOS
- `/mdm/apple/scep` to allow hosts to obtain a SCEP certificate.
- `/mdm/apple/mdm` to allow hosts to reach the server using the MDM protocol.
- `/api/mdm/apple/enroll` to allow DEP-enrolled devices to get an enrollment profile.
- `/api/*/fleet/device/*/mdm/apple/manual_enrollment_profile` to allow manually enrolled devices to
download an enrollment profile.
If you would like to use Fleet's macOS MDM features, the following endpoints need to be exposed:
- `/mdm/apple/scep`: Allows hosts to obtain a SCEP certificate.
- `/mdm/apple/mdm`: Allows hosts to reach the server using the MDM protocol.
- `/api/mdm/apple/enroll`: If you use automatic enrollment, allows hosts to get an enrollment profile.
- `/api/*/fleet/device/*`: Provides end users access to their **My device** page.
- This page is where they download their manual enrollment profile, rotate their disk encryption key, and use other features. For more information on these API endpoints see the documentation [here](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/API-for-contributors.md#device-authenticated-routes).
- `/api/*/fleet/mdm/sso` and `/api/*/fleet/mdm/sso/callback`: If you use automatic enrollment and you require [end user authentication](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula) during out-of-the-box macOS setup, allows end users to authenticate with your IdP.
- `/api/*/fleet/mdm/setup/eula/*`: If you use automatic enrollment and you require that the end user agrees to an [End User License Agreement (EULA)](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula) during out-of-the-box macOS setup, allows end user to see the EULA.
- `/api/*/fleet/mdm/bootstrap`: If you use automatic enrollment and you install a [bootstrap package](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#bootstrap-package) during out-of-the-box macOS setup, installs the bootstrap package.
> The `/mdm/apple/scep` and `/mdm/apple/mdm` endpoints are outside of the `/api` path because they
> are not RESTful and are not intended for use by API clients or browsers.
### Windows
If you would like to use Fleet's Windows MDM features, the following endpoints need to be exposed:
- `/api/mdm/microsoft/management`: Allows host to get MDM commands and profiles once the host.
- See the [Mobile Device Management Protocol specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mdm/33769a92-ac31-47ef-ae7b-dc8501f7104f).
- `/api/mdm/microsoft/discovery`: Allows hosts to get information from the MDM server.
- See the [section 3.1 on the MS-MDE2 specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/2681fd76-1997-4557-8963-cf656ab8d887) for more details.
- `/api/mdm/microsoft/policy`: Delivers the enrollment policies required to issue identity certificates to hosts.
- See the [section 3.3 on the MS-MDE2 specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/08ec4475-32c2-457d-8c27-5a176660a210) for more details.
- `/api/mdm/microsoft/enroll`: Delivers WS-Trust X.509v3 Token Enrollment (MS-WSTEP) functionality.
- See the [section 3.4 on the MS-MDE2 specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wstep/4766a85d-0d18-4fa1-a51f-e5cb98b752ea) for more details.
- `/api/mdm/microsoft/tos`: Presents end users with the Terms of Service agreement during out-of-the-box Windows setup. Required for automatic enrollment.
- `/api/mdm/microsoft/auth`: If you use automatic enrollment, authenticates end users during out-of-the-box Windows setup.
- See the [section 3.2 on the MS-MDE2 specification](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/27ed8c2c-0140-41ce-b2fa-c3d1a793ab4a) for more details.
## Advanced
The `/api/*/fleet/*` endpoints accessed by the fleetd agent can use mTLS with the certificate provided via the `--fleet-tls-client-certificate` flag in the `fleetctl package` command.
The `/mdm/apple/mdm` and `/api/mdm/apple/enroll` endpoints can use mTLS with the [SCEP certificate issued by the Fleet server](https://fleetdm.com/docs/configuration/fleet-server-configuration#mdm-apple-scep-cert-bytes).
These endpoints don't use mTLS:
- `/mdm/apple/scep`
- `/api/mdm/microsoft/discovery`
- `/api/mdm/microsoft/auth`
- `/api/mdm/microsoft/policy`
- `/api/mdm/microsoft/enroll`
- `/api/mdm/microsoft/management`
- `/api/mdm/microsoft/tos`
For macOS and Windows, the MDM client on the host will send the client certificate in a header. The Fleet server always does additional verification of this certificate.
<meta name="category" value="guides">
<meta name="authorGitHubUsername" value="mike-j-thomas">

View File

@ -0,0 +1 @@
- Fix device page showing invalid date for last restarted

View File

@ -0,0 +1,2 @@
- Added validation to reject requests to enqueue scripts for hosts that do not have fleetd installed
(i.e. plain osquery hosts).

View File

@ -0,0 +1 @@
- Fix title case to sentence case and a few other headers

View File

@ -0,0 +1,2 @@
- Fix a bug where updating the search field for the Software titles page caused an unwanted loss of
focus from the search field on rerender.

View File

@ -0,0 +1 @@
- Fix windows vulnerabilities without exploit/severity from crashing the page when rendered

View File

@ -0,0 +1,2 @@
- Fix a style bug where the controls on the software title and versions table would wrap and bump into
each other.

View File

@ -0,0 +1 @@
- Fix a bug where checkboxes within a hidden modal would not be hidden with the rest of the modal content.

View File

@ -0,0 +1 @@
- Fix a bug where long OS names caused the table to render outside its bounds with smaller viewports

View File

@ -0,0 +1,2 @@
* Fix alignment bugs on the Software > OS > details and Software > Versions > details empty table
states.

View File

@ -0,0 +1 @@
- Fix a bug where the "Done" button on the add hosts modal for plain osquery could be covered.

View File

@ -0,0 +1 @@
- Implemented permission checks for endpoints and UI routes related to software and software titles, restricting visibility to team-specific hosts.

1
changes/lock-perms-docs Normal file
View File

@ -0,0 +1 @@
- Updates the permissions docs to include permissions for lock/unlock/wipe actions on a host.

View File

@ -620,7 +620,7 @@ func TestGetSoftwareTitles(t *testing.T) {
var gotTeamID *uint
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
gotTeamID = opt.TeamID
return []fleet.SoftwareTitle{
{

View File

@ -0,0 +1,167 @@
package main
import (
"context"
"fmt"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
appleMdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-git/go-git/v5"
"github.com/micromdm/nanodep/tokenpki"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"os"
"path"
"path/filepath"
"testing"
)
func TestEnterpriseIntegrationsGitops(t *testing.T) {
testingSuite := new(enterpriseIntegrationGitopsTestSuite)
testingSuite.suite = &testingSuite.Suite
suite.Run(t, testingSuite)
}
type enterpriseIntegrationGitopsTestSuite struct {
suite.Suite
withServer
fleetCfg config.FleetConfig
}
func (s *enterpriseIntegrationGitopsTestSuite) SetupSuite() {
s.withDS.SetupSuite("integrationGitopsTestSuite")
appConf, err := s.ds.AppConfig(context.Background())
require.NoError(s.T(), err)
appConf.MDM.EnabledAndConfigured = true
appConf.MDM.WindowsEnabledAndConfigured = true
appConf.MDM.AppleBMEnabledAndConfigured = true
err = s.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(s.T(), err)
testCert, testKey, err := appleMdm.NewSCEPCACertKey()
require.NoError(s.T(), err)
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
config.SetTestMDMConfig(s.T(), &fleetCfg, testCertPEM, testKeyPEM, testBMToken, "../../server/service/testdata")
fleetCfg.Osquery.EnrollCooldown = 0
mdmStorage, err := s.ds.NewMDMAppleMDMStorage(testCertPEM, testKeyPEM)
require.NoError(s.T(), err)
depStorage, err := s.ds.NewMDMAppleDEPStorage(*testBMToken)
require.NoError(s.T(), err)
scepStorage, err := s.ds.NewSCEPDepot(testCertPEM, testKeyPEM)
require.NoError(s.T(), err)
redisPool := redistest.SetupRedis(s.T(), "zz", false, false, false)
serverConfig := service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
},
FleetConfig: &fleetCfg,
MDMStorage: mdmStorage,
DEPStorage: depStorage,
SCEPStorage: scepStorage,
Pool: redisPool,
APNSTopic: "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589",
}
users, server := service.RunServerForTestsWithDS(s.T(), s.ds, &serverConfig)
s.T().Setenv("FLEET_SERVER_ADDRESS", server.URL) // fleetctl always uses this env var in tests
s.server = server
s.users = users
s.fleetCfg = fleetCfg
appConf, err = s.ds.AppConfig(context.Background())
require.NoError(s.T(), err)
appConf.ServerSettings.ServerURL = server.URL
err = s.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(s.T(), err)
}
func (s *enterpriseIntegrationGitopsTestSuite) TearDownSuite() {
appConf, err := s.ds.AppConfig(context.Background())
require.NoError(s.T(), err)
appConf.MDM.EnabledAndConfigured = false
err = s.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(s.T(), err)
}
// TestFleetGitops runs `fleetctl gitops` command on configs in https://github.com/fleetdm/fleet-gitops repo.
// Changes to that repo may cause this test to fail.
func (s *enterpriseIntegrationGitopsTestSuite) TestFleetGitops() {
t := s.T()
const fleetGitopsRepo = "https://github.com/fleetdm/fleet-gitops"
// Create GitOps user
user := fleet.User{
Name: "GitOps User",
Email: "fleetctl-gitops@example.com",
GlobalRole: ptr.String(fleet.RoleGitOps),
}
require.NoError(t, user.SetPassword(test.GoodPassword, 10, 10))
_, err := s.ds.NewUser(context.Background(), &user)
require.NoError(t, err)
// Create a temporary fleetctl config file
fleetctlConfig, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
token := s.getTestToken(user.Email, test.GoodPassword)
configStr := fmt.Sprintf(
`
contexts:
default:
address: %s
tls-skip-verify: true
token: %s
`, s.server.URL, token,
)
_, err = fleetctlConfig.WriteString(configStr)
require.NoError(t, err)
// Clone git repo
repoDir := t.TempDir()
_, err = git.PlainClone(
repoDir, false, &git.CloneOptions{
ReferenceName: "main",
SingleBranch: true,
Depth: 1,
URL: fleetGitopsRepo,
Progress: os.Stdout,
},
)
require.NoError(t, err)
// Set the required environment variables
t.Setenv("FLEET_SSO_METADATA", "sso_metadata")
t.Setenv("FLEET_GLOBAL_ENROLL_SECRET", "global_enroll_secret")
t.Setenv("FLEET_WORKSTATIONS_ENROLL_SECRET", "workstations_enroll_secret")
t.Setenv("FLEET_WORKSTATIONS_CANARY_ENROLL_SECRET", "workstations_canary_enroll_secret")
globalFile := path.Join(repoDir, "default.yml")
teamsDir := path.Join(repoDir, "teams")
teamFiles, err := os.ReadDir(teamsDir)
require.NoError(t, err)
// Dry run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "--dry-run"})
for _, file := range teamFiles {
if filepath.Ext(file.Name()) == ".yml" {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", path.Join(teamsDir, file.Name()), "--dry-run"})
}
}
// Real run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile})
for _, file := range teamFiles {
if filepath.Ext(file.Name()) == ".yml" {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", path.Join(teamsDir, file.Name())})
}
}
}

View File

@ -1,33 +1,22 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
appleMdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-git/go-git/v5"
nanodepClient "github.com/micromdm/nanodep/client"
"github.com/micromdm/nanodep/tokenpki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"io"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"testing"
"time"
)
func TestIntegrationsGitops(t *testing.T) {
@ -72,7 +61,7 @@ func (s *integrationGitopsTestSuite) SetupSuite() {
serverConfig := service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
Tier: fleet.TierFree,
},
FleetConfig: &fleetCfg,
MDMStorage: mdmStorage,
@ -108,20 +97,11 @@ func (s *integrationGitopsTestSuite) TestFleetGitops() {
t := s.T()
const fleetGitopsRepo = "https://github.com/fleetdm/fleet-gitops"
// Create GitOps user
user := fleet.User{
Name: "GitOps User",
Email: "fleetctl-gitops@example.com",
GlobalRole: ptr.String(fleet.RoleGitOps),
}
require.NoError(t, user.SetPassword(test.GoodPassword, 10, 10))
_, err := s.ds.NewUser(context.Background(), &user)
require.NoError(t, err)
// Create a temporary fleetctl config file
fleetctlConfig, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
token := s.getTestToken(user.Email, test.GoodPassword)
// GitOps user is a premium feature, so we simply use an admin user.
token := s.getTestToken("admin1@example.com", test.GoodPassword)
configStr := fmt.Sprintf(
`
contexts:
@ -150,96 +130,13 @@ contexts:
// Set the required environment variables
t.Setenv("FLEET_SSO_METADATA", "sso_metadata")
t.Setenv("FLEET_GLOBAL_ENROLL_SECRET", "global_enroll_secret")
t.Setenv("FLEET_WORKSTATIONS_ENROLL_SECRET", "workstations_enroll_secret")
t.Setenv("FLEET_WORKSTATIONS_CANARY_ENROLL_SECRET", "workstations_canary_enroll_secret")
globalFile := path.Join(repoDir, "default.yml")
teamsDir := path.Join(repoDir, "teams")
teamFiles, err := os.ReadDir(teamsDir)
require.NoError(t, err)
// Dry run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "--dry-run"})
for _, file := range teamFiles {
if filepath.Ext(file.Name()) == ".yml" {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", path.Join(teamsDir, file.Name()), "--dry-run"})
}
}
// Real run
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile})
for _, file := range teamFiles {
if filepath.Ext(file.Name()) == ".yml" {
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", path.Join(teamsDir, file.Name())})
}
}
}
type withDS struct {
suite *suite.Suite
ds *mysql.Datastore
}
func (ts *withDS) SetupSuite(dbName string) {
t := ts.suite.T()
ts.ds = mysql.CreateNamedMySQLDS(t, dbName)
test.AddAllHostsLabel(t, ts.ds)
// Set up the required fields on AppConfig
appConf, err := ts.ds.AppConfig(context.Background())
require.NoError(t, err)
appConf.OrgInfo.OrgName = "FleetTest"
appConf.ServerSettings.ServerURL = "https://example.org"
err = ts.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(t, err)
}
func (ts *withDS) TearDownSuite() {
_ = ts.ds.Close()
}
type withServer struct {
withDS
server *httptest.Server
users map[string]fleet.User
}
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (ts *withServer) getTestToken(email string, password string) string {
params := loginRequest{
Email: email,
Password: password,
}
j, err := json.Marshal(&params)
require.NoError(ts.suite.T(), err)
requestBody := io.NopCloser(bytes.NewBuffer(j))
resp, err := http.Post(ts.server.URL+"/api/latest/fleet/login", "application/json", requestBody)
require.NoError(ts.suite.T(), err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(ts.suite.T(), http.StatusOK, resp.StatusCode)
jsn := struct {
User *fleet.User `json:"user"`
Token string `json:"token"`
Err []map[string]string `json:"errors,omitempty"`
}{}
err = json.NewDecoder(resp.Body).Decode(&jsn)
require.NoError(ts.suite.T(), err)
require.Len(ts.suite.T(), jsn.Err, 0)
return jsn.Token
}
var testBMToken = &nanodepClient.OAuth1Tokens{
ConsumerKey: "test_consumer",
ConsumerSecret: "test_secret",
AccessToken: "test_access_token",
AccessSecret: "test_access_secret",
AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
}

View File

@ -215,13 +215,13 @@ Fleet records the last 10,000 characters to prevent downtime.
if ident != "host1" || c.expectNotFound {
return nil, &notFoundError{}
}
return &fleet.Host{ID: 42, SeenTime: time.Now()}, nil
return &fleet.Host{ID: 42, SeenTime: time.Now(), OrbitNodeKey: ptr.String("abc")}, nil
}
ds.HostFunc = func(ctx context.Context, hid uint) (*fleet.Host, error) {
if hid != 42 || c.expectNotFound {
return nil, &notFoundError{}
}
h := fleet.Host{ID: hid, SeenTime: time.Now()}
h := fleet.Host{ID: hid, SeenTime: time.Now(), OrbitNodeKey: ptr.String("abc")}
if c.expectOffline {
h.SeenTime = time.Now().Add(-time.Hour)
}

View File

@ -3,7 +3,13 @@ package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/test"
nanodepClient "github.com/micromdm/nanodep/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"io"
"net/http"
"net/http/httptest"
@ -20,6 +26,75 @@ import (
"github.com/urfave/cli/v2"
)
type withDS struct {
suite *suite.Suite
ds *mysql.Datastore
}
func (ts *withDS) SetupSuite(dbName string) {
t := ts.suite.T()
ts.ds = mysql.CreateNamedMySQLDS(t, dbName)
test.AddAllHostsLabel(t, ts.ds)
// Set up the required fields on AppConfig
appConf, err := ts.ds.AppConfig(context.Background())
require.NoError(t, err)
appConf.OrgInfo.OrgName = "FleetTest"
appConf.ServerSettings.ServerURL = "https://example.org"
err = ts.ds.SaveAppConfig(context.Background(), appConf)
require.NoError(t, err)
}
func (ts *withDS) TearDownSuite() {
_ = ts.ds.Close()
}
type withServer struct {
withDS
server *httptest.Server
users map[string]fleet.User
}
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (ts *withServer) getTestToken(email string, password string) string {
params := loginRequest{
Email: email,
Password: password,
}
j, err := json.Marshal(&params)
require.NoError(ts.suite.T(), err)
requestBody := io.NopCloser(bytes.NewBuffer(j))
resp, err := http.Post(ts.server.URL+"/api/latest/fleet/login", "application/json", requestBody)
require.NoError(ts.suite.T(), err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(ts.suite.T(), http.StatusOK, resp.StatusCode)
jsn := struct {
User *fleet.User `json:"user"`
Token string `json:"token"`
Err []map[string]string `json:"errors,omitempty"`
}{}
err = json.NewDecoder(resp.Body).Decode(&jsn)
require.NoError(ts.suite.T(), err)
require.Len(ts.suite.T(), jsn.Err, 0)
return jsn.Token
}
var testBMToken = &nanodepClient.OAuth1Tokens{
ConsumerKey: "test_consumer",
ConsumerSecret: "test_secret",
AccessToken: "test_access_token",
AccessSecret: "test_access_secret",
AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
}
// runServerWithMockedDS runs the fleet server with several mocked DS methods.
//
// NOTE: Assumes the current session is always from the admin user (see ds.SessionByKeyFunc below).

View File

@ -14,6 +14,7 @@ import (
"io"
"log"
"math/rand"
"net"
"net/http"
_ "net/http/pprof"
"os"
@ -30,7 +31,6 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
"github.com/valyala/fasthttp"
)
var (
@ -334,7 +334,6 @@ type agent struct {
liveQueryNoResultsProb float64
strings map[string]string
serverAddress string
fastClient fasthttp.Client
stats *Stats
nodeKeyManager *nodeKeyManager
nodeKey string
@ -420,10 +419,6 @@ func newAgent(
if rand.Float64() <= orbitProb {
deviceAuthToken = ptr.String(uuid.NewString())
}
// #nosec (osquery-perf is only used for testing)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
serialNumber := mdmtest.RandSerialNumber()
if rand.Float64() <= emptySerialProb {
serialNumber = ""
@ -451,12 +446,9 @@ func newAgent(
munkiIssueCount: munkiIssueCount,
liveQueryFailProb: liveQueryFailProb,
liveQueryNoResultsProb: liveQueryNoResultsProb,
fastClient: fasthttp.Client{
TLSConfig: tlsConfig,
},
templates: templates,
deviceAuthToken: deviceAuthToken,
os: strings.TrimRight(templates.Name(), ".tmpl"),
templates: templates,
deviceAuthToken: deviceAuthToken,
os: strings.TrimRight(templates.Name(), ".tmpl"),
EnrollSecret: enrollSecret,
ConfigInterval: configInterval,
@ -840,18 +832,19 @@ func (a *agent) execScripts(execIDs []string, orbitClient *service.OrbitClient)
}
}
func (a *agent) waitingDo(req *fasthttp.Request, res *fasthttp.Response) {
err := a.fastClient.Do(req, res)
for err != nil || res.StatusCode() != http.StatusOK {
func (a *agent) waitingDo(request *http.Request) *http.Response {
response, err := http.DefaultClient.Do(request)
for err != nil || response.StatusCode != http.StatusOK {
if err != nil {
log.Printf("failed to run request: %s", err)
} else { // res.StatusCode() != http.StatusOK
log.Printf("request failed: %d", res.StatusCode())
log.Printf("request failed: %d", response.StatusCode)
}
a.stats.IncrementErrors(1)
<-time.Tick(time.Duration(rand.Intn(120)+1) * time.Second)
err = a.fastClient.Do(req, res)
response, err = http.DefaultClient.Do(request)
}
return response
}
// TODO: add support to `alreadyEnrolled` akin to the `enroll` function. for
@ -863,32 +856,23 @@ func (a *agent) orbitEnroll() error {
HardwareUUID: a.UUID,
HardwareSerial: a.SerialNumber,
}
jsonBytes, err := json.Marshal(params)
if err != nil {
log.Println("orbit json marshall:", err)
return err
}
req := fasthttp.AcquireRequest()
req.SetBody(jsonBytes)
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.SetRequestURI(a.serverAddress + "/api/fleet/orbit/enroll")
resp := fasthttp.AcquireResponse()
a.waitingDo(req, resp)
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
if resp.StatusCode() != http.StatusOK {
log.Println("orbit enroll status:", resp.StatusCode())
return fmt.Errorf("status code: %d", resp.StatusCode())
request, err := http.NewRequest("POST", a.serverAddress+"/api/fleet/orbit/enroll", bytes.NewReader(jsonBytes))
if err != nil {
return err
}
request.Header.Add("Content-type", "application/json")
response := a.waitingDo(request)
defer response.Body.Close()
var parsedResp service.EnrollOrbitResponse
if err := json.Unmarshal(resp.Body(), &parsedResp); err != nil {
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
log.Println("orbit json parse:", err)
return err
}
@ -915,26 +899,22 @@ func (a *agent) enroll(i int, onlyAlreadyEnrolled bool) error {
return err
}
req := fasthttp.AcquireRequest()
req.SetBody(body.Bytes())
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
req.SetRequestURI(a.serverAddress + "/api/osquery/enroll")
res := fasthttp.AcquireResponse()
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/enroll", &body)
if err != nil {
return err
}
request.Header.Add("Content-type", "application/json")
a.waitingDo(req, res)
response := a.waitingDo(request)
defer response.Body.Close()
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
if res.StatusCode() != http.StatusOK {
log.Println("enroll status:", res.StatusCode())
return fmt.Errorf("status code: %d", res.StatusCode())
if response.StatusCode != http.StatusOK {
log.Println("enroll status:", response.StatusCode)
return fmt.Errorf("status code: %d", response.StatusCode)
}
var parsedResp enrollResponse
if err := json.Unmarshal(res.Body(), &parsedResp); err != nil {
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
log.Println("json parse:", err)
return err
}
@ -948,28 +928,21 @@ func (a *agent) enroll(i int, onlyAlreadyEnrolled bool) error {
}
func (a *agent) config() error {
body := bytes.NewBufferString(`{"node_key": "` + a.nodeKey + `"}`)
req := fasthttp.AcquireRequest()
req.SetBody(body.Bytes())
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
req.SetRequestURI(a.serverAddress + "/api/osquery/config")
res := fasthttp.AcquireResponse()
err := a.fastClient.Do(req, res)
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/config", bytes.NewReader([]byte(`{"node_key": "`+a.nodeKey+`"}`)))
if err != nil {
return err
}
request.Header.Add("Content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("config request failed to run: %w", err)
}
defer response.Body.Close()
a.stats.IncrementConfigRequests()
statusCode := res.StatusCode()
statusCode := response.StatusCode
if statusCode != http.StatusOK {
a.stats.IncrementConfigErrors()
return fmt.Errorf("config request failed: %d", statusCode)
@ -980,7 +953,7 @@ func (a *agent) config() error {
Queries map[string]interface{} `json:"queries"`
} `json:"packs"`
}{}
if err := json.Unmarshal(res.Body(), &parsedResp); err != nil {
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
return fmt.Errorf("json parse at config: %w", err)
}
@ -1139,33 +1112,28 @@ func (a *agent) softwareMacOS() []map[string]string {
}
func (a *agent) DistributedRead() (*distributedReadResponse, error) {
req := fasthttp.AcquireRequest()
req.SetBody([]byte(`{"node_key": "` + a.nodeKey + `"}`))
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.Add("User-Agent", "osquery/4.6.0")
req.SetRequestURI(a.serverAddress + "/api/osquery/distributed/read")
res := fasthttp.AcquireResponse()
err := a.fastClient.Do(req, res)
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/distributed/read", bytes.NewReader([]byte(`{"node_key": "`+a.nodeKey+`"}`)))
if err != nil {
return nil, err
}
request.Header.Add("Content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, fmt.Errorf("distributed/read request failed to run: %w", err)
}
defer response.Body.Close()
a.stats.IncrementDistributedReads()
statusCode := res.StatusCode()
statusCode := response.StatusCode
if statusCode != http.StatusOK {
a.stats.IncrementDistributedReadErrors()
return nil, fmt.Errorf("distributed/read request failed: %d", statusCode)
}
var parsedResp distributedReadResponse
if err := json.Unmarshal(res.Body(), &parsedResp); err != nil {
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
log.Printf("json parse: %s", err)
return nil, err
}
@ -1596,26 +1564,21 @@ func (a *agent) DistributedWrite(queries map[string]string) error {
panic(err)
}
req := fasthttp.AcquireRequest()
req.SetBody(body)
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.Add("User-Agent", "osquery/5.0.1")
req.SetRequestURI(a.serverAddress + "/api/osquery/distributed/write")
res := fasthttp.AcquireResponse()
err = a.fastClient.Do(req, res)
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/distributed/write", bytes.NewReader(body))
if err != nil {
return err
}
request.Header.Add("Content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("distributed/write request failed to run: %w", err)
}
defer response.Body.Close()
a.stats.IncrementDistributedWrites()
statusCode := res.StatusCode()
statusCode := response.StatusCode
if statusCode != http.StatusOK {
a.stats.IncrementDistributedWriteErrors()
return fmt.Errorf("distributed/write request failed: %d", statusCode)
@ -1645,6 +1608,13 @@ func scheduledQueryResults(packName, queryName string, numResults int) json.RawM
}
func (a *agent) submitLogs(results []json.RawMessage) error {
// Connection check to prevent unnecessary JSON marshaling when the server is down.
conn, err := net.Dial("tcp", strings.TrimPrefix(a.serverAddress, "https://"))
if err != nil {
return err
}
conn.Close()
jsonResults, err := json.Marshal(results)
if err != nil {
panic(err)
@ -1654,36 +1624,31 @@ func (a *agent) submitLogs(results []json.RawMessage) error {
LogType string `json:"log_type"`
Data json.RawMessage `json:"data"`
}
r := submitLogsRequest{
slr := submitLogsRequest{
NodeKey: a.nodeKey,
LogType: "result",
Data: jsonResults,
}
body, err := json.Marshal(r)
body, err := json.Marshal(slr)
if err != nil {
panic(err)
}
req := fasthttp.AcquireRequest()
req.SetBody(body)
req.Header.SetMethod("POST")
req.Header.SetContentType("application/json")
req.Header.Add("User-Agent", "osquery/5.0.1")
req.SetRequestURI(a.serverAddress + "/api/osquery/log")
res := fasthttp.AcquireResponse()
err = a.fastClient.Do(req, res)
fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(res)
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/log", bytes.NewReader(body))
if err != nil {
return err
}
request.Header.Add("Content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("log request failed to run: %w", err)
}
defer response.Body.Close()
a.stats.IncrementResultLogRequests()
statusCode := res.StatusCode()
statusCode := response.StatusCode
if statusCode != http.StatusOK {
a.stats.IncrementResultLogErrors()
return fmt.Errorf("log request failed: %d", statusCode)
@ -1724,6 +1689,14 @@ func main() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// #nosec (osquery-perf is only used for testing)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = tlsConfig
http.DefaultClient.Transport = tr
validTemplateNames := map[string]bool{
"macos_13.6.2.tmpl": true,
"macos_14.1.2.tmpl": true,

View File

@ -240,13 +240,13 @@ spec:
deadline: "2022-01-04"
macos_settings:
custom_settings:
- path/to/profile1.mobileconfig
- path/to/profile2.mobileconfig
- path: path/to/profile1.mobileconfig
- path: path/to/profile2.mobileconfig
enable_disk_encryption: true
windows_settings:
custom_settings:
- path/to/profile3.xml
- path/to/profile4.xml
- path: path/to/profile3.xml
- path: path/to/profile4.xml
scripts:
- path/to/script1.sh
- path/to/script2.sh
@ -456,13 +456,13 @@ spec:
deadline: ""
macos_settings:
custom_settings:
- path/to/profile1.mobileconfig
- path/to/profile2.mobileconfig
- path: path/to/profile1.mobileconfig
- path: path/to/profile2.mobileconfig
enable_disk_encryption: true
windows_settings:
custom_settings:
- path/to/profile3.xml
- path/to/profile4.xml
- path: path/to/profile3.xml
- path: path/to/profile4.xml
```
### Settings
@ -1131,12 +1131,10 @@ Enables or disables Windows MDM support.
**Applies only to Fleet Premium**.
The following options allow configuring the behavior of Nudge for macOS hosts that belong to no team and are enrolled into Fleet's MDM.
The following options allow configuring OS updates for macOS hosts.
##### mdm.macos_updates.minimum_version
Hosts that belong to no team and are enrolled into Fleet's MDM will be nudged until their macOS is at or above this version.
Requires `mdm.macos_updates.deadline` to be set.
- Default value: ""
@ -1151,8 +1149,6 @@ Requires `mdm.macos_updates.deadline` to be set.
A deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8).
Hosts that belong to no team and are enrolled into Fleet's MDM won't be able to dismiss the Nudge window once this deadline is past.
Requires `mdm.macos_updates.minimum_version` to be set.
- Default value: ""
@ -1163,6 +1159,36 @@ Requires `mdm.macos_updates.minimum_version` to be set.
deadline: "2022-01-01"
```
##### mdm.windows_updates
**Applies only to Fleet Premium**.
The following options allow configuring OS updates for Windows hosts.
##### mdm.windows_updates.deadline
A deadline in days.
- Default value: ""
- Config file format:
```yaml
mdm:
windows_updates:
deadline_days: "5"
```
##### mdm.windows_updates.grace_period
A grace period in days.
- Default value: ""
- Config file format:
```yaml
mdm:
windows_updates:
grace_period_days: "2"
```
##### mdm.macos_settings
The following settings are macOS-specific settings for Fleet's MDM solution.
@ -1181,8 +1207,8 @@ If you're using Fleet Premium, these profiles apply to all hosts assigned to no
mdm:
macos_settings:
custom_settings:
- path/to/profile1.mobileconfig
- path/to/profile2.mobileconfig
- path: path/to/profile1.mobileconfig
- path: path/to/profile2.mobileconfig
```
##### mdm.macos_settings.enable_disk_encryption
@ -1221,8 +1247,8 @@ If you're using Fleet Premium, these profiles apply to all hosts assigned to no
mdm:
windows_settings:
custom_settings:
- path/to/profile1.xml
- path/to/profile2.xml
- path: path/to/profile1.xml
- path: path/to/profile2.xml
```
#### Scripts

View File

@ -138,5 +138,4 @@ If this works and the browser is not working then it might be a rendering issue
You should also try running the live query on different browsers.
<meta name="pageOrderInSection" value="1800">
<meta name="description" value="An overview of live queries in Fleet and steps for troubleshooting.">
<meta name="navSection" value="The basics">

View File

@ -1,18 +1,14 @@
# Anatomy
This page details the core concepts you need to know to use Fleet.
## Fleet UI
Fleet UI is the GUI (graphical user interface) used to control Fleet. [Docs](https://fleetdm.com/docs/using-fleet/fleet-ui).
## Fleetctl
Fleetctl (pronouced “fleet control”) is a CLI (command line interface) tool for managing Fleet from the command line. [Docs](https://fleetdm.com/docs/using-fleet/fleetctl-cli).
## Fleetd
Fleetd is a bundle of agents provided by Fleet to gather information about your devices. Fleetd includes [osquery](https://www.osquery.io/), Orbit, and Fleet Desktop.
Fleetd is a bundle of agents provided by Fleet to gather information about your devices. Fleetd includes [osquery](https://www.osquery.io/), [Orbit](https://github.com/fleetdm/fleet/blob/main/orbit/README.md), Fleet Desktop, and the [Fleetd Chrome Extension](https://github.com/fleetdm/fleet/blob/main/ee/fleetd-chrome/README.md).
## Osquery
Osquery is an open-source tool for gathering information about the state of any device that the osquery agent has been installed on. [Learn more](https://www.osquery.io/).
@ -21,7 +17,10 @@ Osquery is an open-source tool for gathering information about the state of any
Orbit is an osquery version and configuration manager, built by Fleet. [Docs](https://fleetdm.com/docs/using-fleet/orbit).
## Fleet Desktop
Fleet Desktop is a menu bar icon that gives end users visibility into the security and status of their machine. [Docs](https://fleetdm.com/docs/using-fleet/fleet-desktop).
Fleet Desktop is a menu bar icon that gives end users visibility into the security and status of their machine. [Docs](https://fleetdm.com/docs/using-fleet/fleet-desktop).
## Fleetd Chrome Extension
The Fleetd Chrome Extension enrolls ChromeOS devices in Fleet. [Docs](https://github.com/fleetdm/fleet/blob/main/ee/fleetd-chrome/README.md).
## Host
A host is a computer, server, or other endpoint. Fleet gathers information from an osquery agent installed on each of your hosts. [Docs](https://fleetdm.com/docs/using-fleet/adding-hosts).

View File

@ -1820,12 +1820,15 @@ None.
- [Get mobile device management (MDM) summary](#get-mobile-device-management-mdm-summary)
- [Get host's mobile device management (MDM) and Munki information](#get-hosts-mobile-device-management-mdm-and-munki-information)
- [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-macadmin-mobile-device-management-mdm-and-munki-information)
- [Get host OS versions](#get-host-os-versions)
- [List host OS versions](#list-host-os-versions)
- [Get host OS version](#get-host-os-version)
- [Get host's scripts](#get-hosts-scripts)
- [Get hosts report in CSV](#get-hosts-report-in-csv)
- [Get host's disk encryption key](#get-hosts-disk-encryption-key)
- [Get host's past activity](#get-hosts-past-activity)
- [Get host's upcoming activity](#get-hosts-upcoming-activity)
- [Live query one host (ad-hoc)](#live-query-one-host-ad-hoc)
- [Live query host by identifier (ad-hoc)](#live-query-host-by-identifier-ad-hoc)
### On the different timestamps in the host data structure
@ -1872,7 +1875,7 @@ the `software` table.
| policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. |
| software_version_id | integer | query | The ID of the software version to filter hosts by. |
| software_title_id | integer | query | The ID of the software title to filter hosts by. |
| os_id | integer | query | The ID of the operating system to filter hosts by. |
| os_version_id | integer | query | The ID of the operating system version to filter hosts by. |
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
| os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` |
| device_mapping | boolean | query | Indicates whether `device_mapping` should be included for each host. See ["Get host's Google Chrome profiles](#get-hosts-google-chrome-profiles) for more information about this feature. |
@ -2091,7 +2094,7 @@ Response payload with the `munki_issue_id` filter provided:
| policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. |
| software_version_id | integer | query | The ID of the software version to filter hosts by. |
| software_title_id | integer | query | The ID of the software title to filter hosts by. |
| os_id | integer | query | The ID of the operating system to filter hosts by. |
| os_version_id | integer | query | The ID of the operating system version to filter hosts by. |
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
| os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` |
| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `after`, `status`, `query` and `team_id`. |
@ -3437,7 +3440,7 @@ A `team_id` of `0` returns the statistics for hosts that are not part of any tea
}
```
### Get host OS versions
### List host OS versions
Retrieves the aggregated host OS versions information.
@ -3451,6 +3454,12 @@ Retrieves the aggregated host OS versions information.
| platform | string | query | Filters the hosts to the specified platform |
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
| os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` |
| team_id | integer | query | _Available in Fleet Premium_. Filters to only include OS versions for the specified team. |
| page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. |
| order_key | string | query | What to order results by. Allowed fields are: `hosts_count`. Default is `hosts_count` (descending). |
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
##### Default response
@ -3458,60 +3467,111 @@ Retrieves the aggregated host OS versions information.
```json
{
"counts_updated_at": "2022-03-22T21:38:31Z",
"count": 1
"counts_updated_at": "2023-12-06T22:17:30Z",
"os_versions": [
{
"hosts_count": 1,
"name": "CentOS 6.10.0",
"name_only": "CentOS",
"version": "6.10.0",
"platform": "rhel",
"os_id": 1
},
{
"hosts_count": 1,
"name": "CentOS Linux 7.9.2009",
"name_only": "CentOS",
"version": "7.9.2009",
"platform": "rhel",
"os_id": 2
},
{
"hosts_count": 1,
"name": "CentOS Linux 8.3.2011",
"name_only": "CentOS",
"version": "8.2.2011",
"platform": "rhel",
"os_id": 3
},
{
"hosts_count": 1,
"name": "Debian GNU/Linux 10.0.0",
"name_only": "Debian GNU/Linux",
"version": "10.0.0",
"platform": "debian",
"os_id": 4
},
{
"hosts_count": 1,
"name": "Debian GNU/Linux 9.0.0",
"name_only": "Debian GNU/Linux",
"version": "9.0.0",
"platform": "debian",
"os_id": 5
},
{
"hosts_count": 1,
"name": "Ubuntu 16.4.0 LTS",
"name_only": "Ubuntu",
"version": "16.4.0 LTS",
"platform": "ubuntu",
"os_id": 6
"os_version_id": 123,
"hosts_count": 21,
"name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
"name_only": "Microsoft Windows 11 Pro 23H2",
"version": "10.0.22621.1234",
"platform": "windows",
"generated_cpes": [],
"vulnerabilities": [
{
"cve": "CVE-2022-30190",
"details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
"cvss_score": 7.8,// Available in Fleet Premium
"epss_probability": 0.9729,// Available in Fleet Premium
"cisa_known_exploit": false,// Available in Fleet Premium
"cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
"cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
"resolved_in_version": ""// Available in Fleet Premium
}
]
}
]
],
"meta": {
"has_next_results": false,
"has_previous_results": false
}
}
```
OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
```json
{
"hosts_count": 1,
"name": "CentOS Linux 7.9.2009",
"name_only": "CentOS",
"version": "7.9.2009",
"platform": "rhel",
"generated_cpes": [],
"vulnerabilities": []
}
```
### Get host OS version
Retrieves information about the specified OS version.
`GET /api/v1/fleet/os_versions/:id`
#### Parameters
| Name | Type | In | Description |
| ---- | ---- | -- | ----------- |
| id | integer | path | **Required.** The OS version's ID. |
##### Default response
`Status: 200`
```json
{
"counts_updated_at": "2023-12-06T22:17:30Z",
"os_version": {
"id": 123,
"hosts_count": 21,
"name": "Microsoft Windows 11 Pro 23H2 10.0.22621.1234",
"name_only": "Microsoft Windows 11 Pro 23H2",
"version": "10.0.22621.1234",
"platform": "windows",
"generated_cpes": [],
"vulnerabilities": [
{
"cve": "CVE-2022-30190",
"details_link": "https://nvd.nist.gov/vuln/detail/CVE-2022-30190",
"cvss_score": 7.8,// Available in Fleet Premium
"epss_probability": 0.9729,// Available in Fleet Premium
"cisa_known_exploit": false,// Available in Fleet Premium
"cve_published": "2022-06-01T00:15:00Z",// Available in Fleet Premium
"cve_description": "Microsoft Windows Support Diagnostic Tool (MSDT) Remote Code Execution Vulnerability.",// Available in Fleet Premium
"resolved_in_version": ""// Available in Fleet Premium
}
]
}
}
```
OS vulnerability data is currently available for Windows and macOS. For other platforms, `vulnerabilities` will be an empty array:
```json
{
"id": 321,
"hosts_count": 1,
"name": "CentOS Linux 7.9.2009",
"name_only": "CentOS",
"version": "7.9.2009",
"platform": "rhel",
"generated_cpes": [],
"vulnerabilities": []
}
```
### Get host's scripts
`GET /api/v1/fleet/hosts/:id/scripts`
@ -3533,7 +3593,6 @@ Retrieves the aggregated host OS versions information.
`Status: 200`
```json
{
"scripts": [
{
"script_id": 3,
@ -3593,7 +3652,7 @@ requested by a web browser.
| policy_response | string | query | **Requires `policy_id`**. Valid options are 'passing' or 'failing'. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** |
| software_version_id | integer | query | The ID of the software version to filter hosts by. |
| software_title_id | integer | query | The ID of the software title to filter hosts by. |
| os_id | integer | query | The ID of the operating system to filter hosts by. |
| os_version_id | integer | query | The ID of the operating system version to filter hosts by. |
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
| os_version | string | query | The version of the operating system to filter hosts by. `os_name` must also be specified with `os_version` |
| mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). |
@ -3826,6 +3885,106 @@ Retrieves a list of the configuration profiles assigned to a host.
}
```
### Live query one host (ad-hoc)
Runs an ad-hoc live query against the specified host and responds with the results.
The live query will stop if the targeted host is offline, or if the query times out. Timeouts happen if the host hasn't responded after the configured `FLEET_LIVE_QUERY_REST_PERIOD` (default 25 seconds) or if the `distributed_interval` agent option (default 10 seconds) is higher than the `FLEET_LIVE_QUERY_REST_PERIOD`.
`POST /api/v1/fleet/hosts/:id/query`
#### Parameters
| Name | Type | In | Description |
|-----------|-------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | integer | path | **Required**. The target host ID. |
| query | string | body | **Required**. The query SQL. |
#### Example
`POST /api/v1/fleet/hosts/123/query`
##### Request body
```json
{
"query": "SELECT model, vendor FROM usb_devices;"
}
```
##### Default response
`Status: 200`
```json
{
"host_id": 123,
"query": "SELECT model, vendor FROM usb_devices;",
"status": "online", // "online" or "offline"
"error": null,
"rows": [
{
"model": "USB2.0 Hub",
"vendor": "VIA Labs, Inc."
}
]
}
```
Note that if the host is online and the query times out, this endpoint will return an error and `rows` will be `null`. If the host is offline, no error will be returned, and `rows` will be`null`.
### Live query host by identifier (ad-hoc)
Runs an ad-hoc live query against a host identified using `uuid` and responds with the results.
The live query will stop if the targeted host is offline, or if the query times out. Timeouts happen if the host hasn't responded after the configured `FLEET_LIVE_QUERY_REST_PERIOD` (default 25 seconds) or if the `distributed_interval` agent option (default 10 seconds) is higher than the `FLEET_LIVE_QUERY_REST_PERIOD`.
`POST /api/v1/fleet/hosts/identifier/:identifier/query`
#### Parameters
| Name | Type | In | Description |
|-----------|-------|------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| identifier | integer or string | path | **Required**. The host's `hardware_serial`, `uuid`, `osquery_host_id`, `hostname`, or `node_key`. |
| query | string | body | **Required**. The query SQL. |
#### Example
`POST /api/v1/fleet/hosts/identifier/392547dc-0000-0000-a87a-d701ff75bc65/query`
##### Request body
```json
{
"query": "SELECT model, vendor FROM usb_devices;"
}
```
##### Default response
`Status: 200`
```json
{
"host_id": 123,
"query": "SELECT model, vendor FROM usb_devices;",
"status": "online", // "online" or "offline"
"error": null,
"rows": [
{
"model": "USB2.0 Hub",
"vendor": "VIA Labs, Inc."
}
]
}
```
Note that if the host is online and the query times out, this endpoint will return an error and `rows` will be `null`. If the host is offline, no error will be returned, and `rows` will be `null`.
---
@ -6537,9 +6696,7 @@ Deletes the queries specified by ID. Returns the count of queries successfully d
Runs a live query against the specified hosts and responds with the results.
If some targeted hosts haven't responded, the live query will stop after 25 seconds (or whatever time period is configured), and all collected results are returned.
The timeout period is configurable via environment variable on the Fleet server (e.g. `FLEET_LIVE_QUERY_REST_PERIOD=90s`). If setting a higher value than the default, be sure not to exceed your load balancer timeout.
The live query will stop if the request times out. Timeouts happen if targeted hosts haven't responded after the configured `FLEET_LIVE_QUERY_REST_PERIOD` (default 25 seconds) or if the `distributed_interval` agent option (default 10 seconds) is higher than the `FLEET_LIVE_QUERY_REST_PERIOD`.
`POST /api/v1/fleet/queries/:id/run`

View File

@ -0,0 +1,50 @@
# OS updates
_Available in Fleet Premium_
In Fleet you can enforce OS updates on your macOS and Windows hosts remotely.
## Enforce OS updates
You can enforce OS updates using the Fleet UI, Fleet API, or [Fleet's GitOps workflow](https://github.com/fleetdm/fleet-gitops).
Fleet UI:
1. Head to the **Controls** > **OS updates** tab.
2. To enforce OS updates for macOS, select **macOS** and set a **Minimum version** and **Deadline**.
3. For Windows, select **Windows** and set a **Deadline** and **Grace period**.
Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#modify-team).
## End user experience
### macOS
End users are encouraged to update macOS (via [Nudge](https://github.com/macadmins/nudge)).
![Nudge window](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/nudge-window.png)
| | > 1 day before deadline | < 1 day before deadline | Past deadline |
| ------------------------------------ | ----------------------- | ----------------------- | --------------------- |
| Nudge window frequency | Once a day at 8pm GMT | Once every 2 hours | Immediately on login |
| End user can defer | ✅ | ✅ | ❌ |
| Nudge window is dismissible | ✅ | ✅ | ❌ |
### Windows
End users are encouraged to update Windows via the native Windows dialog.
| | Before deadline | Past deadline |
| ----------------------------------------- | ----------------| ------------- |
| End user can defer automatic restart | ✅ | ❌ |
If an end user was on vacation when the deadline passed, the end user is given a grace period (configured) before the host automatically restarts.
Fleet enforces OS updates for quality and feature updates. Read more about the types of Windows OS updates in the Microsoft documentation [here](https://learn.microsoft.com/en-us/windows/deployment/update/get-started-updates-channels-tools#types-of-updates).
<meta name="pageOrderInSection" value="1503">
<meta name="title" value="OS updates">
<meta name="description" value="Learn how to manage OS updates on macOS and Windows devices.">
<meta name="navSection" value="Device management">

View File

@ -1,160 +0,0 @@
# macOS updates
## End user macOS update reminders via Nudge
_Available in Fleet Premium_
End users can be reminded and encouraged to update macOS (via [Nudge](https://github.com/macadmins/nudge)).
![Nudge window](https://raw.githubusercontent.com/fleetdm/fleet/main/docs/images/nudge-window.png)
A Fleet admin can set a minimum version and deadline for Fleet-enrolled hosts. If an end user's machine is below the minimum version, the Nudge window above will periodically appear to encourage them to upgrade. The end user has the option to defer the update, but as the deadline approaches, the Nudge window appears more frequently.
When the end user machine is below the minimum version, Nudge applies the following behavior:
| | > 1 day before deadline | < 1 day before deadline | past deadline |
| ------------------------------------ | ----------------------- | ----------------------- | --------------------- |
| Nudge window frequency | Once a day at 8pm GMT | Once every 2 hours | Immediately on login |
| End user can defer | ✅ | ✅ | ❌ |
| Nudge window is dismissable | ✅ | ✅ | ❌ |
### How to set up
To set the macOS updates settings in the UI, visit the **Controls** section and then select the **macOS updates** tab.
To set the macOS updates settings via CLI, use the configurations listed [here](https://fleetdm.com/docs/using-fleet/configuration-files#mdm-macos-updates).
### End user experience
After the user clicks "update" in the Nudge window, they will be taken to the standard Apple software update screen:
![Apple software update screen on macOS 12](https://user-images.githubusercontent.com/5359586/228936740-2e8acf2e-6523-4710-9b3f-8243398bd98e.png)
Here, the user would follow Apple's standard two-step process for macOS updates:
1. Download the macOS update. This occurs in the background and does not interrupt the end user's work.
2. Initiate the update which does prevent the end user from using the host for a time.
On Intel Macs, Fleet triggers step 1 (downloading the macOS update) programmatically when a new version is available. This way, when the user arrives on the software update screen, they only need to initiate step 2.
> On Macs with Apple Silicon (e.g. M1), downloading the macOS update may require end user action. Apple doesn't support downloading the update programmatically on Macs with Apple silicon.
Step 2 (installing the update) always requires end user action.
### Known issues
#### Apple Rapid Security Responses (RSRs)
Currently, end user macOS update reminders via Nudge don't support RSR versions (ex. "13.4.1 (a)").
You can use custom MDM commands in Fleet to trigger built-in macOS update reminders for RSRs. Learn how [here](#end-user-macos-update-via-built-in-macos-notifications).
#### Mac is up to date
Sometimes after the end user clicks "update" on the Nudge window, the end user's Mac will say that macOS is up to date when it isn't. This known issue can create a frustrating experience for the end user. Ask the end user to follow the steps below to troubleshoot:
1. From the Apple menu in the top left corner of your screen, select **System Settings** or **System Preferences**.
2. In the search bar, type "Software Update." Select **Software Update**.
3. Type "Command (⌘)-R" to check for updates. If you see an available update, select **Restart Now** to update.
4. If you still don't see an available update, from the Apple menu in the top left corner of your screen, select **Restart...** to restart your Mac.
5. After your Mac restarts, from the Apple menu in the top left corner of your screen, select **System Settings** or **System Preferences**.
6. In the search bar, type "Software Update." Select **Software Update** and select **Restart Now** to update.
## End user macOS update via built-in macOS notifications
Built-in macOS update reminders are available in Fleet Free and Fleet Premium.
To trigger these reminders, we will do the following steps:
1. Force a macOS update scan
2. List available macOS updates
3. Trigger macOS update reminder
### Step 1: force a macOS update scan
Use the request payload below when running a custom MDM command with Fleet. Documentation on how to run a custom command is [here](https://fleetdm.com/docs/using-fleet/mdm-commands#custom-commands).
Request payload:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>ForceUpdateScan</key>
<true/>
<key>RequestType</key>
<string>ScheduleOSUpdateScan</string>
</dict>
</dict>
</plist>
```
### Step 2: list available macOS updates
1. Run another custom MDM command using the request payload below.
Request payload:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>RequestType</key>
<string>AvailableOSUpdates</string>
</dict>
</dict>
</plist>
```
2. Copy the `ProductKey` from the command's results. Documentation on how to view a command's results is [here](https://fleetdm.com/docs/using-fleet/mdm-commands#step-4-view-the-commands-results).
Example product key: `MSU_UPDATE_22F770820d_patch_13.4.1_rsr`
### Step 3: trigger macOS update reminder
Run another custom MDM command using the request payload below. Replace the product key with your product key.
> This payload will trigger the "Install ASAP" behavior which displays a macOS notification with a 60 seconds timer before the Mac automatically restarts. The end user can dismiss the timer. To trigger different behavior, update the `InstallAction`. Options are documented by Apple [here](https://developer.apple.com/documentation/devicemanagement/scheduleosupdatecommand/command/updatesitem).
Request payload:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>RequestType</key>
<string>ScheduleOSUpdate</string>
<key>Updates</key>
<array>
<dict>
<key>InstallAction</key>
<string>InstallASAP</string>
<key>ProductKey</key>
<string>MSU_UPDATE_22F770820d_patch_13.4.1_rsr</string>
</dict>
</array>
</dict>
</dict>
</plist>
```
<meta name="pageOrderInSection" value="1503">
<meta name="title" value="macOS updates">
<meta name="description" value="Learn how to manage macOS updates and set up end user reminders with Fleet MDM.">
<meta name="navSection" value="Device management">

View File

@ -92,6 +92,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
| View saved scripts\* | ✅ | ✅ | ✅ | ✅ | |
| Edit/upload saved scripts\* | | | ✅ | ✅ | ✅ |
| Run saved scripts on hosts\* | ✅ | ✅ | ✅ | ✅ | |
| Lock, unlock, and wipe hosts\* | | | ✅ | ✅ | |
\* Applies only to Fleet Premium
@ -158,6 +159,7 @@ Users that are members of multiple teams can be assigned different roles for eac
| Edit/upload saved scripts | | | ✅ | ✅ | |
| Run saved scripts on hosts | ✅ | ✅ | ✅ | ✅ | |
| View script details by host | ✅ | ✅ | ✅ | ✅ | |
| Lock, unlock, and wipe hosts | | | ✅ | ✅ | |
\* Applies only to [Fleet REST API](https://fleetdm.com/docs/using-fleet/rest-api)

View File

@ -2,9 +2,14 @@
.platform-wrapper {
padding: 0; // different to pad sticky subnav properly
.react-tabs {
&__tab-list {
margin: 0 0 1.25rem;
.component__tabs-wrapper {
// negate problematic sticky positioning
position: initial;
.react-tabs {
&__tab-list {
margin: 0 0 1.25rem;
}
}
}

View File

@ -11,6 +11,9 @@
&__hidden {
visibility: hidden;
.fleet-checkbox__tick:after {
visibility: hidden;
}
}
&__content {

View File

@ -85,7 +85,7 @@ const PackQueriesTable = ({
resultsTitle={"queries"}
emptyComponent={() =>
EmptyTable({
header: "No queries match your search criteria.",
header: "No queries match your search criteria",
info: "Try a different search.",
})
}

View File

@ -47,7 +47,7 @@ const EmptyOperatingSystems = (platform: SelectedPlatform): JSX.Element => (
className={`${baseClass}__os-empty-table`}
header={`No${
` ${PLATFORM_DISPLAY_NAMES[platform]}` || ""
} operating systems detected.`}
} operating systems detected`}
info="This report is updated every hour to protect the performance of your
devices."
/>

View File

@ -55,7 +55,7 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
disableCount
emptyComponent={() => (
<EmptyTable
header="No Disk Encryption Status"
header="No disk encryption status"
info="Expecting to status data? Try again in a few seconds as the system
catches up."
/>

View File

@ -23,7 +23,7 @@ const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
<p>As the deadline gets closer, Fleet provides stronger encouragement.</p>
<CustomLink
text="Learn more about macOS updates in Fleet"
url="https://fleetdm.com/docs/using-fleet/mdm-macos-updates"
url="https://fleetdm.com/learn-more-about/os-updates"
newTab
/>
</>
@ -31,15 +31,14 @@ const NudgeDescription = ({ platform }: INudgeDescriptionProps) => {
<>
<h3>End user experience on Windows</h3>
<p>
When a new Windows update is published, the update will be downloaded
and installed automatically before 8am and after 5pm (end users local
time). Before the deadline passes, users will be able to defer restarts.
After the deadline passes restart will be forced regardless of active
hours.
When a Windows host becomes aware of a new update, end users are able to
defer restarts. Automatic restarts happen before 8am and after 5pm (end
users local time). After the deadline, restarts are forced regardless
of active hours.
</p>
<CustomLink
text="Learn more about Windows updates in Fleet"
url="Links to: https://fleetdm.com/docs/using-fleet/mdm-windows-updates"
url="https://fleetdm.com/learn-more-about/os-updates"
newTab
/>
</>

View File

@ -8,7 +8,7 @@ const OSVersionsEmptyState = () => {
return (
<EmptyTable
className={`${baseClass}__empty-table`}
header="No OS versions detected."
header="No OS versions detected"
info={
<span>
This report is updated every hour to protect

View File

@ -58,7 +58,7 @@ const BootstrapPackageTable = ({
disableCount
emptyComponent={() => (
<EmptyTable
header="No Bootstrap Package Status"
header="No bootstrap package status"
info="Expecting to status data? Try again in a few seconds as the system
catches up."
/>

View File

@ -25,12 +25,31 @@
.table-container {
&__data-table-block {
.data-table-block {
.data-table__table {
tbody {
.link-cell {
display: flex;
align-items: center;
gap: $pad-small;
.data-table {
&__wrapper {
overflow-x: auto;
}
&__table {
tbody {
.name_only__cell {
max-width: $col-md;
// Tooltip does not get cut off
.children-wrapper {
overflow: initial;
}
// ellipsis for software name
.software-name {
overflow: hidden;
text-overflow: ellipsis;
}
}
.link-cell {
display: flex;
align-items: center;
gap: $pad-small;
}
}
}
}

View File

@ -74,6 +74,7 @@ const SoftwareTitleDetailsPage = ({
[{ scope: "softwareById", softwareId, teamId: teamIdForApi }],
({ queryKey }) => softwareAPI.getSoftwareTitle(queryKey[0]),
{
refetchOnWindowFocus: false,
select: (data) => data.software_title,
onError: (error) => {
if (!ignoreAxiosError(error, [403, 404])) {

View File

@ -41,7 +41,7 @@
flex-direction: column-reverse; // Search bar on top
margin-bottom: $pad-medium;
@media (min-width: $break-md) {
@media (min-width: $table-controls-break) {
flex-direction: row;
}
}
@ -64,7 +64,7 @@
width: 100%;
}
@media (min-width: $break-md) {
@media (min-width: $table-controls-break) {
width: auto;
.input-icon-field__input {
@ -120,7 +120,6 @@
// ellipsis for software name
.software-name {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
}
}

View File

@ -119,10 +119,6 @@ const SoftwareTitles = ({
}
);
if (isTitlesFetching) {
return <Spinner />;
}
if (isTitlesError || isVersionsError) {
return <TableDataError className={`${baseClass}__table-error`} />;
}

View File

@ -77,6 +77,7 @@ const SoftwareVersionDetailsPage = ({
[{ scope: "softwareVersion", versionId, teamId: teamIdForApi }],
({ queryKey }) => softwareAPI.getSoftwareVersion(queryKey[0]),
{
refetchOnWindowFocus: false,
select: (data) => data.software,
onError: (error) => {
if (!ignoreAxiosError(error, [403, 404])) {

View File

@ -103,16 +103,17 @@ const SoftwareVulnerabilitiesTable = ({
columnConfigs={tableHeaders}
data={data}
defaultSortHeader={isPremiumTier ? "updated_at" : "cve"} // TODO: Change premium to created_at when added to API
defaultSortDirection={"desc"}
defaultSortDirection="desc"
emptyComponent={() => <NoVulnsDetected itemName={itemName} />}
isAllPagesSelected={false}
isLoading={isLoading}
isClientSidePagination
pageSize={20}
resultsTitle={"vulnerabilities"}
resultsTitle="vulnerabilities"
showMarkAllPages={false}
disableMultiRowSelect
onSelectSingleRow={handleRowSelect}
disableTableHeader={data.length === 0}
/>
</div>
);

View File

@ -10,6 +10,10 @@
}
}
.empty-table__container {
margin: 1.5rem auto;
}
// used to position header text with premium icon correctly
.column-header {
display: flex;

View File

@ -66,7 +66,7 @@ const EmptyMembersTable = ({
if (searchString !== "") {
return (
<EmptyTable
header="We couldnt find any users."
header="We couldn't find any users"
info="Expecting to see users? Try again in a few seconds as the system catches up."
/>
);

View File

@ -30,7 +30,7 @@ import EditUserModal from "../EditUserModal";
const EmptyUsersTable = () => (
<EmptyTable
header="No users match the current criteria."
header="No users match the current criteria"
info="Expecting to see users? Try again in a few seconds as the system catches up."
/>
);

View File

@ -1402,7 +1402,7 @@ const ManageHostsPage = ({
const emptyState = () => {
const emptyHosts: IEmptyTableProps = {
graphicName: "empty-hosts",
header: "Hosts will show up here once theyre added to Fleet.",
header: "Hosts will show up here once theyre added to Fleet",
info:
"Expecting to see hosts? Try again in a few seconds as the system catches up.",
};

View File

@ -132,9 +132,7 @@
}
// table header content responsive styles
// NOTE: 1150px is a custom breakpoint to deal with responsiveness of the
// table controls. 990px doesnt work for us in this case.
@media (max-width: 1150px) {
@media (max-width: $table-controls-break) {
&__header {
flex-direction: column;
}
@ -238,7 +236,7 @@
&__status_dropdown {
width: 175px;
@media (min-width: 1150px) {
@media (min-width: $table-controls-break) {
width: 190px;
}

View File

@ -69,7 +69,7 @@
text-overflow: ellipsis;
white-space: nowrap;
@media (max-width: 1150px) {
@media (max-width: $table-controls-break) {
width: auto;
}
}
@ -149,7 +149,7 @@
}
}
@media (min-width: 1150px) {
@media (min-width: $table-controls-break) {
.label-filter-select {
min-width: 220px;

View File

@ -25,7 +25,11 @@ import InfoBanner from "components/InfoBanner";
import Icon from "components/Icon/Icon";
import { normalizeEmptyValues } from "utilities/helpers";
import PATHS from "router/paths";
import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants";
import {
DOCUMENT_TITLE_SUFFIX,
HOST_ABOUT_DATA,
HOST_SUMMARY_DATA,
} from "utilities/constants";
import HostSummaryCard from "../cards/HostSummary";
import AboutCard from "../cards/About";
@ -205,39 +209,9 @@ const DeviceUserPage = ({
}
);
const titleData = normalizeEmptyValues(
pick(host, [
"id",
"status",
"issues",
"memory",
"cpu_type",
"os_version",
"osquery_version",
"enroll_secret_name",
"detail_updated_at",
"percent_disk_space_available",
"gigs_disk_space_available",
"team_name",
"platform",
"mdm",
])
);
const summaryData = normalizeEmptyValues(pick(host, HOST_SUMMARY_DATA));
const aboutData = normalizeEmptyValues(
pick(host, [
"seen_time",
"uptime",
"last_enrolled_at",
"hardware_model",
"hardware_serial",
"primary_ip",
"public_ip",
"geolocation",
"batteries",
"detail_updated_at",
])
);
const aboutData = normalizeEmptyValues(pick(host, HOST_ABOUT_DATA));
const toggleInfoModal = useCallback(() => {
setShowInfoModal(!showInfoModal);
@ -392,8 +366,7 @@ const DeviceUserPage = ({
</InfoBanner>
)}
<HostSummaryCard
titleData={titleData}
diskEncryptionEnabled={host?.disk_encryption_enabled}
summaryData={summaryData}
bootstrapPackageData={bootstrapPackageData}
isPremiumTier={isPremiumTier}
toggleOSSettingsModal={toggleOSSettingsModal}

View File

@ -112,10 +112,8 @@ const canLockHost = ({
isFleetMdm,
isGlobalAdmin,
isGlobalMaintainer,
isGlobalObserver,
isTeamAdmin,
isTeamMaintainer,
isTeamObserver,
hostMdmDeviceStatus,
}: IHostActionConfigOptions) => {
// macOS hosts can be locked if they are enrolled in MDM and the MDM is enabled
@ -131,12 +129,7 @@ const canLockHost = ({
(hostPlatform === "windows" ||
isLinuxLike(hostPlatform) ||
canLockDarwin) &&
(isGlobalAdmin ||
isGlobalMaintainer ||
isGlobalObserver ||
isTeamAdmin ||
isTeamMaintainer ||
isTeamObserver)
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
);
};
@ -180,10 +173,8 @@ const canUnlock = ({
isPremiumTier,
isGlobalAdmin,
isGlobalMaintainer,
isGlobalObserver,
isTeamAdmin,
isTeamMaintainer,
isTeamObserver,
isFleetMdm,
isEnrolledInMdm,
isMdmEnabledAndConfigured,
@ -206,12 +197,7 @@ const canUnlock = ({
return (
isPremiumTier &&
isValidState &&
(isGlobalAdmin ||
isGlobalMaintainer ||
isGlobalObserver ||
isTeamAdmin ||
isTeamMaintainer ||
isTeamObserver) &&
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) &&
(canLockDarwin || hostPlatform === "windows" || isLinuxLike(hostPlatform))
);
};

View File

@ -46,7 +46,12 @@ import {
TAGGED_TEMPLATES,
} from "utilities/helpers";
import permissions from "utilities/permissions";
import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants";
import {
DOCUMENT_TITLE_SUFFIX,
HOST_SUMMARY_DATA,
HOST_ABOUT_DATA,
HOST_OSQUERY_DATA,
} from "utilities/constants";
import Spinner from "components/Spinner";
import TabsWrapper from "components/TabsWrapper";
@ -457,48 +462,11 @@ const HostDetailsPage = ({
setPathname(location.pathname + location.search);
}, [location]);
const titleData = normalizeEmptyValues(
pick(host, [
"id",
"status",
"issues",
"memory",
"cpu_type",
"platform",
"os_version",
"osquery_version",
"enroll_secret_name",
"detail_updated_at",
"percent_disk_space_available",
"gigs_disk_space_available",
"team_name",
"display_name",
])
);
const summaryData = normalizeEmptyValues(pick(host, HOST_SUMMARY_DATA));
const aboutData = normalizeEmptyValues(
pick(host, [
"seen_time",
"uptime",
"last_enrolled_at",
"hardware_model",
"hardware_serial",
"primary_ip",
"public_ip",
"geolocation",
"batteries",
"detail_updated_at",
"last_restarted_at",
])
);
const aboutData = normalizeEmptyValues(pick(host, HOST_ABOUT_DATA));
const osqueryData = normalizeEmptyValues(
pick(host, [
"config_tls_refresh",
"logger_tls_period",
"distributed_interval",
])
);
const osqueryData = normalizeEmptyValues(pick(host, HOST_OSQUERY_DATA));
const togglePolicyDetailsModal = useCallback(
(policy: IHostPolicy) => {
@ -795,8 +763,7 @@ const HostDetailsPage = ({
/>
</div>
<HostSummaryCard
titleData={titleData}
diskEncryptionEnabled={host?.disk_encryption_enabled}
summaryData={summaryData}
bootstrapPackageData={bootstrapPackageData}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}

View File

@ -63,6 +63,15 @@ const generateFormattedTooltip = (detail: string) => {
const keyValuePairs = detail.split(/, */);
const formattedElements: JSX.Element[] = [];
// Special case to handle bitlocker error message. It does not follow the
// expected string format so we will just render the error message as is.
if (
detail.includes("BitLocker") ||
detail.includes("preparing volume for encryption")
) {
return detail;
}
keyValuePairs.forEach((pair, i) => {
const [key, value] = pair.split(/: */);
if (key && value) {

View File

@ -42,7 +42,7 @@ const PastActivityFeed = ({
if (activitiesList === null || activitiesList.length === 0) {
return (
<EmptyFeed
title="No Activity"
title="No activity"
message="When a script runs on a host, it shows up here."
className={`${baseClass}__empty-feed`}
/>

View File

@ -102,9 +102,8 @@ interface IBootstrapPackageData {
}
interface IHostSummaryProps {
titleData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers
summaryData: any; // TODO: create interfaces for this and use consistently across host pages and related helpers
bootstrapPackageData?: IBootstrapPackageData;
diskEncryptionEnabled?: boolean;
isPremiumTier?: boolean;
isSandboxMode?: boolean;
toggleOSSettingsModal?: () => void;
@ -164,9 +163,8 @@ const getHostDiskEncryptionTooltipMessage = (
};
const HostSummary = ({
titleData,
summaryData,
bootstrapPackageData,
diskEncryptionEnabled,
isPremiumTier,
isSandboxMode = false,
toggleOSSettingsModal,
@ -180,10 +178,14 @@ const HostSummary = ({
osSettings,
hostMdmDeviceStatus,
}: IHostSummaryProps): JSX.Element => {
const { status, platform } = titleData;
const {
status,
platform,
disk_encryption_enabled: diskEncryptionEnabled,
} = summaryData;
const renderRefetch = () => {
const isOnline = titleData.status === "online";
const isOnline = summaryData.status === "online";
let isDisabled = false;
let tooltip: React.ReactNode = <></>;
@ -237,11 +239,11 @@ const HostSummary = ({
data-html
>
<span className={`tooltip__tooltip-text`}>
Failing policies ({titleData.issues.failing_policies_count})
Failing policies ({summaryData.issues.failing_policies_count})
</span>
</ReactTooltip>
<span className={"info-flex__data__text"}>
{titleData.issues.total_issues_count}
{summaryData.issues.total_issues_count}
</span>
</span>
</div>
@ -251,8 +253,8 @@ const HostSummary = ({
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Team</span>
<span className={`info-flex__data`}>
{titleData.team_name ? (
`${titleData.team_name}`
{summaryData.team_name ? (
`${summaryData.team_name}`
) : (
<span className="info-flex__no-team">No team</span>
)}
@ -269,12 +271,24 @@ const HostSummary = ({
platform,
diskEncryptionEnabled
);
let statusText;
if (platform === "chrome") {
statusText = "Always on";
} else {
statusText = diskEncryptionEnabled ? "On" : "Off";
switch (true) {
case platform === "chrome":
statusText = "Always on";
break;
case diskEncryptionEnabled === true:
statusText = "On";
break;
case diskEncryptionEnabled === false:
statusText = "Off";
break;
default:
// something unexpected happened on the way to this component, display whatever we got or
// "Unknown" to draw attention to the issue.
statusText = diskEncryptionEnabled || "Unknown";
}
return (
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Disk encryption</span>
@ -316,7 +330,7 @@ const HostSummary = ({
/>
</div>
{(titleData.issues?.total_issues_count > 0 || isSandboxMode) &&
{(summaryData.issues?.total_issues_count > 0 || isSandboxMode) &&
isPremiumTier &&
renderIssues()}
@ -352,9 +366,11 @@ const HostSummary = ({
<span className="info-flex__header">Disk space</span>
<DiskSpaceGraph
baseClass="info-flex"
gigsDiskSpaceAvailable={titleData.gigs_disk_space_available}
percentDiskSpaceAvailable={titleData.percent_disk_space_available}
id={`disk-space-tooltip-${titleData.id}`}
gigsDiskSpaceAvailable={summaryData.gigs_disk_space_available}
percentDiskSpaceAvailable={
summaryData.percent_disk_space_available
}
id={`disk-space-tooltip-${summaryData.id}`}
platform={platform}
tooltipPosition="bottom"
/>
@ -366,28 +382,28 @@ const HostSummary = ({
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Memory</span>
<span className="info-flex__data">
{wrapFleetHelper(humanHostMemory, titleData.memory)}
{wrapFleetHelper(humanHostMemory, summaryData.memory)}
</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Processor type</span>
<span className="info-flex__data">{titleData.cpu_type}</span>
<span className="info-flex__data">{summaryData.cpu_type}</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Operating system</span>
<span className="info-flex__data">{titleData.os_version}</span>
<span className="info-flex__data">{summaryData.os_version}</span>
</div>
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Osquery</span>
<span className="info-flex__data">{titleData.osquery_version}</span>
<span className="info-flex__data">{summaryData.osquery_version}</span>
</div>
</div>
);
};
const lastFetched = titleData.detail_updated_at ? (
const lastFetched = summaryData.detail_updated_at ? (
<HumanTimeDiffWithFleetLaunchCutoff
timeString={titleData.detail_updated_at}
timeString={summaryData.detail_updated_at}
/>
) : (
": unavailable"
@ -430,7 +446,7 @@ const HostSummary = ({
<h1 className="display-name">
{deviceUser
? "My device"
: titleData.display_name || DEFAULT_EMPTY_CELL_VALUE}
: summaryData.display_name || DEFAULT_EMPTY_CELL_VALUE}
</h1>
{renderDeviceStatusTag()}

View File

@ -30,7 +30,6 @@ const Policies = ({
<EmptyTable
header={
<>
{" "}
No policies are checked{" "}
{deviceUser ? `on your device` : `for this host`}
</>

View File

@ -74,7 +74,7 @@ const PacksTable = ({
};
if (searchString) {
delete emptyPacks.graphicName;
emptyPacks.header = "No packs match the current search criteria.";
emptyPacks.header = "No packs match the current search criteria";
emptyPacks.info =
"Expecting to see packs? Try again in a few seconds as the system catches up.";
delete emptyPacks.primaryButton;

View File

@ -222,7 +222,7 @@ const QueriesTable = ({
};
if (searchQuery) {
delete emptyQueries.graphicName;
emptyQueries.header = "No queries match the current search criteria.";
emptyQueries.header = "No queries match the current search criteria";
emptyQueries.info =
"Expecting to see queries? Try again in a few seconds as the system catches up.";
} else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {

View File

@ -5,3 +5,4 @@ $break-md: 990px;
$break-sm: 880px;
$break-xs: 768px;
$tooltip-break-md: 1000px; // Prevents horizontal scrolling off viewport
$table-controls-break: 1150px;

View File

@ -322,3 +322,41 @@ export const EMPTY_AGENT_OPTIONS = {
export const DEFAULT_EMPTY_CELL_VALUE = "---";
export const DOCUMENT_TITLE_SUFFIX = "Fleet";
export const HOST_SUMMARY_DATA = [
"id",
"status",
"issues",
"memory",
"cpu_type",
"platform",
"os_version",
"osquery_version",
"enroll_secret_name",
"detail_updated_at",
"percent_disk_space_available",
"gigs_disk_space_available",
"team_name",
"disk_encryption_enabled",
"display_name", // Not rendered on my device page
];
export const HOST_ABOUT_DATA = [
"seen_time",
"uptime",
"last_enrolled_at",
"hardware_model",
"hardware_serial",
"primary_ip",
"public_ip",
"geolocation",
"batteries",
"detail_updated_at",
"last_restarted_at",
];
export const HOST_OSQUERY_DATA = [
"config_tls_refresh",
"logger_tls_period",
"distributed_interval",
];

View File

@ -225,7 +225,11 @@ export const formatConfigDataForServer = (config: any): any => {
};
};
export const formatFloatAsPercentage = (float: number): string => {
export const formatFloatAsPercentage = (float?: number): string => {
if (float === undefined) {
return DEFAULT_EMPTY_CELL_VALUE;
}
const formatter = Intl.NumberFormat("en-US", {
maximumSignificantDigits: 2,
style: "percent",
@ -798,7 +802,11 @@ export const normalizeEmptyValues = (
return reduce(
hostData,
(result, value, key) => {
if ((Number.isFinite(value) && value !== 0) || !isEmpty(value)) {
if (
(Number.isFinite(value) && value !== 0) ||
!isEmpty(value) ||
typeof value === "boolean"
) {
Object.assign(result, { [key]: value });
} else {
Object.assign(result, { [key]: DEFAULT_EMPTY_CELL_VALUE });

5
go.mod
View File

@ -34,6 +34,7 @@ require (
github.com/fatih/color v1.15.0
github.com/getsentry/sentry-go v0.18.0
github.com/ghodss/yaml v1.0.0
github.com/go-git/go-git/v5 v5.11.0
github.com/go-ini/ini v1.67.0
github.com/go-kit/kit v0.12.0
github.com/go-kit/log v0.2.1
@ -98,7 +99,6 @@ require (
github.com/tj/assert v0.0.3
github.com/ulikunitz/xz v0.5.10
github.com/urfave/cli/v2 v2.23.5
github.com/valyala/fasthttp v1.40.0
github.com/ziutek/mymysql v1.5.4
go.elastic.co/apm/module/apmgorilla/v2 v2.3.0
go.elastic.co/apm/module/apmsql/v2 v2.4.3
@ -164,7 +164,6 @@ require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/akavel/rsrc v0.10.2 // indirect
github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/antchfx/xpath v1.2.2 // indirect
github.com/apache/thrift v0.18.1 // indirect
github.com/apex/log v1.9.0 // indirect
@ -210,7 +209,6 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@ -292,7 +290,6 @@ require (
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vartanbeno/go-reddit/v2 v2.0.0 // indirect
github.com/xanzy/go-gitlab v0.50.3 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect

10
go.sum
View File

@ -210,8 +210,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ=
github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU=
@ -835,7 +833,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab h1:KVR7cs+oPyy85i+8t1ZaNSy1bymCy5FuWyt51pdrXu4=
@ -1198,11 +1195,6 @@ github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vartanbeno/go-reddit/v2 v2.0.0 h1:fxYMqx5lhbmJ3yYRN1nnQC/gecRB3xpUS2BbG7GLpsk=
github.com/vartanbeno/go-reddit/v2 v2.0.0/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
@ -1325,7 +1317,6 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
@ -1582,7 +1573,6 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -229,6 +229,16 @@ Within 60 days of the end of the year, follow these steps:
- Provide SVB with our board-approved annual operating budgets and projections (on a quarterly granularity) for the new year.
- Deliver this as early as possible in case they have questions.
### Process a tool upgrade request from a team member
- A Fleetie may request an upgraded license seat for Fleet tools by submitting an issue through ZenHub.
- BizOps will upgrade or add the license seat as needed and let the requesting team member know they did it.
### Downgrade an unused license seat
- On the first Wednesday of every quarter, the CEO, head of BizOps and apprentice to the CEO will meet for 30 minutes to audit license seats in Figma, Slack, GitHub, Salesforce and other tools.
- During this meeting, as many seats will be downgraded as possible. When doubt exists, downgrade.
- Afterward, post in #random letting folks know that the quarterly tool reconciliation and seat clearing is complete, and that any members who lost access to anything they still need can submit a ZenHub issue to BizOps to have their access restored.
- The goal is to build deep, integrated knowledge of tool usage across Fleet and cut costs whenever possible. It will also force conversations on redundancies and decisions that aren't helping the business that otherwise might not be looked at a second time.
### Update weekly KPIs
- Create the weekly update issue from the template in ZenHub every Friday and update the [KPIs for BizOps](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit#gid=0) by 5pm US central time.
- Check the KPI sheet at 5pm US central time to ensure all departments have updated their KPIs on time. If any departments are delinquent, notify the department head and let the [Apprentice to the CEO](https://fleetdm.com/handbook/ceo#team) know so they can put it on the agenda for their next one-on-one with the CEO.
@ -296,29 +306,21 @@ Check the "📃 Planned articles" column in [#g-demand board](https://app.zenhub
## Rituals
The following table lists this department's rituals, frequency, and Directly Responsible Individual (DRI).
<rituals :rituals="rituals['handbook/business-operations/business-operations.rituals.yml']"></rituals>
<!--
Note: These are out of date, but retained for future reference. TODO: Deal with them and delete them
| Weekly update reminder | Weekly | Early Friday mornings (US time), a Slack bot posts in the `#g-e` channel reminding directly responsible individuals for KPIs to add their metrics for the current week in ["KPIs"](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit) before the end of the day. | N/A |
| Access revalidation | Quarterly | Review critical access groups to make sure they contain only relevant people. | Mike McNeil |
| 550C update | Annually | File California 550C. | Mike McNeil |
| TPA verifications | Quarterly | Every quarter before tax filing due dates, Mike McNeil audits state accounts to ensure TPA is set up or renewed. | Mike McNeil |
| Brex reconciliation | Monthly | Make sure all company-issued credit card transactions include memos. | Nathanael Holliday |
| Hours update | Weekly | Screenshots of contractor hours as shown in Gusto are sent via Slack to each contractor's manager with no further action necessary if everything appears normal. | Mike McNeil |
| QBO check | Quarterly | The first month after the previous quarter has closed, make sure that QBO is accurate compared to Fleet's records. | Nathanael Holliday |
| YubiKey adoption | Monthly | Track YubiKey adoption in Google workspace and follow up with those that aren't using it. | Mike McNeil |
| Security policy update | Annually | Update security policies and have them approved by the CEO. | Nathanael Holliday |
| Security notifications check | Daily | Check Slack, Google, Vanta, and Fleet dogfood for security-related notifications. | Nathanael Holliday |
| Changeset for onboarding issue template | Quarterly | pull up the changeset in the onboarding issue template and send out a link to the diff to all team members by posting in Slack's `#general` channel. | Mike McNeil |
| Recruiting progress checkup | Weekly | Mike McNeil looks in the [Fleeties spreadsheet](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) and reports on each open position. | Mike McNeil |
| Investor and advisor updates | PRN | Mike McNeil tracks the last contact with investors and coordinates outreach with CEO. | Mike McNeil |
| MDM device enrollment | Quarterly | Provide export of MDM enrolled devices to the ops team. | Luke Heath |
-->
#### Stubs

View File

@ -18,6 +18,13 @@
autoIssue:
labels: [ "#g-business-operations" ]
repo: "confidential"
#-
# task: "Review ongoing articles"
# startedOn: "2023-10-02"
# frequency: "Daily"
# description: "Check 📃 Planned articles and complete steps in each issue"
# moreInfoUrl: "https://fleetdm.com/handbook/demand#review-ongoing-articles"
# dri: "spokanemac"
-
task: "Check ongoing events"
startedOn: "2024-02-09"
@ -115,6 +122,13 @@
autoIssue:
labels: [ "#g-business-operations" ]
repo: "confidential"
-
task: "Downgrade unused license seats"
startedOn: "2024-03-31"
frequency: "Quarterly"
description: "Downgrade unused or questionable license seats on the first Wednesday of every quarter"
moreInfoUrl: "https://fleetdm.com/handbook/business-operations#downgrade-an-unused-license-seat"
dri: "joStableford"
-
task: "Investor reporting"
startedOn: "2024-03-31"
@ -150,10 +164,4 @@
description: "Provide information to tax team with Deloitte and assist with filing and paying state and federal returns"
moreInfoUrl:
dri: "hollidayn"
#-
# task: "Review ongoing articles"
# startedOn: "2023-10-02"
# frequency: "Daily"
# description: "Check 📃 Planned articles and complete steps in each issue"
# moreInfoUrl: "https://fleetdm.com/handbook/demand#review-ongoing-articles"
# dri: "spokanemac"

View File

@ -141,7 +141,7 @@ To provide clarity about decision-making, [responsibility](https://fleetdm.com/h
- 🫧 [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.
- 🦢 [Product Design](https://fleetdm.com/handbook/product-design): The Product Design department is directly responsible for defining and prioritizing the changes made to the core product.
- 🌐 [Digital Experience](https://fleetdm.com/handbook/digital-experience): The Digital Experience department is directly responsible for the framework, content design, and technology behind Fleet's remote work culture, including fleetdm.com, the handbook, issue templates, UI style guides, internal tooling, Zapier flows, Docusign templates, key spreadsheets, and project management processes.
- 🌐 [Digital Experience](https://fleetdm.com/handbook/digital-experience): The Digital Experience department is directly responsible for the framework, content design, and technology behind Fleet's remote work culture and overall brand experience, including fleetdm.com, the handbook, issue templates, UI style guides, consistent brandfronts, internal tooling, Zapier flows, Docusign templates, key spreadsheets, and project management processes.
## 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

View File

@ -12,17 +12,39 @@ You can read about the company's positioning and product strategy in ["🎐 Why
### Competition
We track competitors' capabilities and adjacent (or commonly integrated) products in Google doc [Competition](https://docs.google.com/document/d/1Bqdui6oQthdv5XtD5l7EZVB-duNRcqVRg7NVA4lCXeI/edit) (private Google doc).
### Directly responsible individuals (DRIs)
## Directly responsible individuals (DRIs)
| Responsibility | DRI |
| -------------- | --- |
| Intentionality of Fleet's interfaces | noahtalerman |
| Best practices for using Fleet | noahtalerman |
| What goes in a release | lukeheath |
| Engineering output and architecture | lukeheath |
| Intentionality of Fleet's interfaces | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/noahtalerman))_ |
| Best practices for using Fleet | [Noah Talerman](https://www.linkedin.com/in/noah-talerman/) _([@noahtalerman](https://github.com/noahtalerman))_ |
| What goes in a release | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_ |
| Engineering output and architecture | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_ |
| Structure and intentionallity of the [Docs](https://fleetdm.com/docs/get-started/why-fleet)| [Mike Thomas](https://www.linkedin.com/in/mike-thomas-52277938) _([@mike-j-thomas](https://github.com/mike-j-thomas))_ |
| Design and content of the [Docs](https://fleetdm.com/docs/get-started/why-fleet) | [Rachael Shaw](https://www.linkedin.com/in/rachaelcshaw/) _([@rachaelshaw](https://github.com/rachaelshaw))_ |
### Docs
This page details processes related to maintaining and updating the ([Fleet docs](https://fleetdm.com/docs)).
When someone asks a question in a public channel, it's safe to assume they aren't the only person looking for an answer.
To make our docs as helpful as possible, the Community team gathers these questions and uses them to make a weekly documentation update.
Fleet's goal is to answer every question with a link to the docs and/or result in a documentation update.
## Fleetdm.com
Any change to fleetdm.com follows the same process as [making changes](https://fleetdm.com/handbook/company/product-groups#making-changes) to the core product. To propose a change to Fleet's website [create a website request](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=website-request.md&title=Request%3A+__________________________) on the #g-digital-experience kanban board.
Before committing anything to code, we create wireframes (referred to as ["drafting"](https://fleetdm.com/handbook/company/product-groups#making-changes)) to illustrate all changes that affect the layout and structure of the user interface, design, or APIs of fleetdm.com. See [Why do we use a wireframe first approach](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach) for more information.
The [Digital Experience team](https://fleetdm.com/handbook/digital-experience#team) holds regular design review sessions to evaluate, revise, and approve wireframes before moving into production. Design review sessions are hosted by the [Head of Design](https://calendar.google.com/calendar/u/0?cid=bXRob21hc0BmbGVldGRtLmNvbQ) and typically take place daily, late afternoon (CST). Anyone is welcome to join.
## Marketing programs
Fleet's community programs are rooted in several areas; created to nurture communication between all current and future Fleet users through events, community support, [social media](#social-media), conversation-starting, [ads](#ads), video, and articles.
### Social media
Fleet's largest asset is our user community, the people actually using Fleet. Public conversations on social media create valuable opportunities for contributors to answer technical questions and collect feedback.
@ -175,6 +197,7 @@ We use these prefixes to organize the Fleet Slack:
* ***2023-***: for temporary channels _(Note: specify the relevant year in four digits, like "YYYY-`)_
### Slack communications and best practices
- We use [channels over DMs](https://fleetdm.com/handbook/company/why-this-way#why-group-slack-channels).
- We use threads in Slack as much as possible. Threads help limit noise for other people following the channel and reduce notification overload.
- We configure our [working hours in Slack](https://slack.com/help/articles/360025054173-Set-up-Slack-for-work-hours-) to make sure everyone knows when they can get in touch with others.
- In consideration of our team, Fleet avoids using global tags in channels (i.e. @here, @channel, etc.) (What about polls? Good question, Fleeties are asked to post their poll in the channel and @mention the teammates they would like to hear from.)
@ -241,6 +264,19 @@ To process intake team members will:
- If the goal/user story is unclear, assign the issue to the requestor and at-mention them in an issue comment asking to clarify the intended action.
- If the task is to be backlogged (i.e. "Not yet"), place the issue in the "Not yet" column and at-mention the requestor in an issue comment. Explain why the task is unable to be prioritized and provide a tentative ETA on when the task will be completed.
### Estimation points
Estimation points represent the effort required to complete a task. After accessing wireframes, we typically play planning poker, a gamified estimation technique, to determine the necessary story point value.
We use the following story points to estimate website tasks:
| Story point | Time |
|:---|:--------------|
| 1 | 1 to 2 hours |
| 2 | 2 to 4 hours |
| 3 | 1 day |
| 5 | 1 to 2 days |
| 8 | Up to a week |
| 13 | 1 to 2 weeks |
### Making a pull request
Our handbook and docs pages are written in Markdown and are editable from our website (via GitHub). Follow the instructions below to propose an edit to the handbook or docs.
@ -1132,259 +1168,9 @@ If the mermaid syntax is incorrect, the diagram will be replaced with an image d
graph TD;
A--D
```
## Website
This page details processes related to maintaining and updating the Fleet website ([fleetdm.com](https://fleetdm.com)).
### Responsibilities
The [website group](https://fleetdm.com/handbook/company/product-groups#website-group) is responsible for production and maintenance of the Fleet website.
### Website Rituals
| Ritual | Frequency | Description | DRI |
|:-----------------------------|:-------------------------|:----------------------------------------------------|-------------------|
| Generate latest schema | once every 3 weeks | After each sprint, generate the latest tables json file to incorporate any new schema documentation. | Eric Shaw |
### Website roadmap
View planned changes to the website on the website group's [sprint board](https://app.zenhub.com/workspaces/g-website-6451748b4eb15200131d4bab/board?sprints=none).
### Requesting changes
See Marketing [intake](https://fleetdm.com/handbook/digital-experience#contact-us) for more information on how the website team prioritizes new requests. Bugs are always prioritized first.
### Wireframes
Before committing anything to code, we create wireframes (referred to as ["drafting"](https://fleetdm.com/handbook/company/product-groups#making-changes)) to illustrate all changes that affect the layout and structure of the user interface, design, or APIs of fleetdm.com.
See [Why do we use a wireframe first approach](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach) for more information.
### Design reviews
We hold regular design review sessions to evaluate, revise, and approve wireframes before moving into production.
Design review sessions are hosted by [Mike Thomas](https://calendar.google.com/calendar/u/0?cid=bXRob21hc0BmbGVldGRtLmNvbQ) and typically take place daily, late afternoon (CST). Anyone is welcome to join.
### Estimation sessions
We use estimation sessions to estimate the effort required to complete a prioritized task.
Through these sessions, we can:
- Confirm that wireframes are complete before moving to production.
- Consider all edge cases and requirements that may have been with during wireframing.
- Avoid having the engineer make choices for “unknowns” during production.
- More accurately plan and prioritize upcoming tasks.
#### Story points
Story points represent the effort required to complete a task. After accessing wireframes, we typically play planning poker, a gamified estimation technique, to determine the necessary story point value.
We use the following story points to estimate website tasks:
| Story point | Time |
|:---|:---|
| 1 | 1 to 2 hours |
| 2 | 2 to 4 hours |
| 3 | 1 day |
| 5 | 1 to 2 days |
| 8 | Up to a week |
| 13 | 1 to 2 weeks |
### Quality
Quality assurance (QA) checks must be completed before changes to the website can be merged. Read on to learn about the quality assurance process for the website.
> **Important:** A PR to the website should not be merged until the quality assurance process has been successfully completed.
#### Manual QA
The product manager of the website group is responsible for making sure that manual QA steps have been added to requests.
#### Writing QA steps
QA steps are step-by-step instructions used to confirm that changed to the website function as expected. They should be simple and clear enough for anybody to follow. Example steps are included in [the “Website request” issue template](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=%23g-digital-experience&template=website-request.md&title=Request%3A+__________________________).
#### Actioning QA steps
[View the website locally](#test-changes-to-the-website) and follow the QA steps in the request ticket to test changes.
QA steps should be actioned when a request has been moved into the “Review/QA” column of the website product board. PRs to the website should not be merged until QA has been completed.
A successful QA check can be indicated by leaving a comment in the conversation thread of the PR.
#### Additional QA
In addition to the steps above. All website changes must be thoroughly checked at all breakpoints and a [browser compatibility](#browser-compatibility) test should be carried out on [supported browsers](https://fleetdm.com/docs/using-fleet/supported-browsers) before website changes can go live.
### Testing changes
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:
- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet).
- [Node.js](https://nodejs.org/en/download/)
- (Optional) [Sails.js](https://sailsjs.com/) installed globally on your machine (`npm install sails -g`)
Once you have the above follow these steps:
1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo.
> Note: If this is your first time running this script, you will need to run `npm install` inside of the website/ folder to install the website's dependencies.
2. Run the `build-static-content` script to generate HTML pages from our Markdown and YAML content.
- **With Node**, you will need to use `node ./node_modules/sails/bin/sails run build-static-content` to execute the script.
- **With Sails.js installed globally** you can use `sails run build-static-content` to execute the script.
> You can use the `--skipGithubRequests` flag to skip requests made to GitHub if you get rate-limited by GitHubs API while running this script.
>
> e.g., `node ./node_modules/sails/bin/sails run build-static-content --skipGithubRequests`
>Note: When this script runs, the website's configuration file ([`website/.sailsrc`](https://github.com/fleetdm/fleet/blob/main/website/.sailsrc)) will automatically be updated with information the website uses to display content built from Markdown and YAML. Changes to this file should never be committed to the GitHub repo. If you want to exclude changes to this file in any PRs you make, you can run this terminal command in your local copy of the Fleet repo: `git update-index --assume-unchanged ./website/.sailsrc`.
3. Once the script is complete, start the website server. From the `website/` folder:
- **With Node.js:** start the server by running `node ./node_modules/sails/bin/sails lift`
- **With Sails.js installed globally:** start the server by running `sails lift`.
4. When the server has started, the Fleet website will be available at [http://localhost:2024](http://localhost:2024)
> **Note:** Some features, such as self-service license dispenser and account creation, are not available when running the website locally. If you need help testing features on a local copy, reach out to `@eashaw` in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) channel on Slack.
#### The "Deploy Fleet Website" GitHub action failed
If the action fails, please complete the following steps:
1. Head to the fleetdm-website app in the [Heroku dashboard](https://heroku.com) and select the "Activity" tab.
2. Select "Roll back to here" on the second to most recent deploy.
3. Head to the fleetdm/fleet GitHub repository and re-run the Deploy Fleet Website action.
### Browser compatibility
A browser compatibility check of [fleetdm.com](https://fleetdm.com/) should be carried out monthly to verify that the website looks and functions as expected across all [supported browsers](../../docs/Using-Fleet/Supported-browsers.md).
- We use [BrowserStack](https://www.browserstack.com/users/sign_in) (logins can be found in [1Password](https://start.1password.com/open/i?a=N3F7LHAKQ5G3JPFPX234EC4ZDQ&v=3ycqkai6naxhqsylmsos6vairu&i=nwnxrrbpcwkuzaazh3rywzoh6e&h=fleetdevicemanagement.1password.com)) for our cross-browser checks.
- Check for issues against the latest version of Google Chrome (macOS). We use this as our baseline for quality assurance.
- Document any issues in GitHub as a [bug report](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md&title=), and assign them for fixing.
- If in doubt about anything regarding design or layout, please reach out to the Design team.
### Error handling
#### Responding to a 5xx error on fleetdm.com
Production systems can fail for various reasons, and it can be frustrating to users when they do, and customer experience is significant to Fleet. In the event of system failure, Fleet will:
* investigate the problem to determine the root cause.
* identify affected users.
* escalate if necessary.
* understand and remediate the problem.
* notify impacted users of any steps they need to take (if any). If a customer paid with a credit card and had a bad experience, default to refunding their money.
* Conduct an incident post-mortem to determine any additional steps we need (including monitoring) to take to prevent this class of problems from happening in the future.
#### Incident post-mortems
When conducting an incident post-mortem, answer the following three questions:
1. Impact: What impact did this error have? How many humans experienced this error, if any, and who were they?
2. Root Cause: Why did this error happen?
3. Side effects: did this error have any side effects? e.g., did it corrupt any data? Did code that was supposed to run afterward and “finish something up” not run, and did it leave anything in the database or other systems in a broken state requiring repair? This typically involves checking the line in the source code that threw the error.
### Vulnerability monitoring
Every week, we run `npm audit --only=prod` to check for vulnerabilities on the production dependencies of fleetdm.com. Once we have a solution to configure GitHub's Dependabot to ignore devDependencies, this manual process can be replaced with Dependabot.
### Landing pages and website experiments
Experimental pages are short-lived, temporary landing pages intended for a small audience. All experiments and landing pages need to go through the standard [drafting process](https://fleetdm.com/handbook/company/product-groups#making-changes) before they are created.
Website experiments and landing pages live behind `/imagine` url. Which is hidden from the sitemap and intended to be linked to from ads and marketing campaigns. Design experiments (flyers, swag, etc.) should be limited to small audiences (less than 500 people) to avoid damaging the brand or confusing our customers. In general, experiments that are of a design nature should be targeted at prospects and random users, never targeted at our customers.
Some examples of experiments that would live behind the `/imagine` url:
- A flyer for a meetup "Free shirt to the person who can solve this riddle!"
- A landing page for a movie screening presented by Fleet
- A landing page for a private event
- A landing page for an ad campaign that is running for 4 weeks.
- An A/B test on product positioning
- A giveaway page for a conference
- Table-top signage for a conference booth or meetup
The Fleet website has a built-in landing page generator that can be used to quickly create a new page that lives under the /imagine/ url.
To generate a new page, you'll need:
- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet).
- [Node.js](https://nodejs.org/en/download/)
- (Optional) [Sails.js](https://sailsjs.com/) installed globally on your machine (`npm install sails -g`)
1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo.
> Note: If this is your first time running the website locally, you will need to run `npm install` inside of the website/ folder to install the website's dependencies.
2. Call the `landing-page` generator by running `node ./node_modules/sails/bin/sails generate landing-page [page-name]`, replacing `[page-name]` with the kebab-cased name (words seperated by dashes `-`) of your page.
3. After the files have been generated, you'll need to manually update the website's routes. To do this, copy and paste the generated route for the new page to the "Imagine" section of `website/config/routes.js`.
4. Next you need to update the stylesheets so that the page can inherit the correct styles. To do this, copy and paste the generated import statement to the "Imagine" section of `website/assets/styles/importer.less`.
5. Start the website by running `node ./node_modules/sails/bin/sails lift` (or `sails lift` if you have Sails installed globally). The new landing page will be availible at `http://localhost:1337/imagine/{page-name}`.
6. Replace the lorum ipsum and placeholder images on the generated page with the page's real content, and add a meta description and title by changing the `pageTitleForMeta` and `pageDescriptionForMeta in the page's `locals` in `website/config/routes.js`.
### How to export images for the website
In Figma:
1. Select the layers you want to export.
2. Confirm export settings and naming convention:
* Item name - color variant - (CSS)size - @2x.fileformat (e.g., `os-macos-black-16x16@2x.png`)
* Note that the dimensions in the filename are in CSS pixels. In this example, if you opened it in preview, the image would actually have dimensions of 32x32px but in the filename, and in HTML/CSS, we'll size it as if it were 16x16. This is so that we support retina displays by default.
* File extension might be .jpg or .png.
* Avoid using SVGs or icon fonts.
3. Click the __Export__ button.
### Website services
#### Cloudflare
We use Cloudflare to manage the DNS records of fleetdm.com and our other domains. Cloudflare offers a free, user-friendly, and cloud-agnostic interface that allows authorized team members to manage all our domains easily.
If you need access to Fleet's Cloudflare account, please ask the [DRI](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) Zach Wasserman in Slack for an invitation.
To make DNS changes in Cloudflare:
1. Log into your Cloudflare account and select the "Fleet" account.
2. Select the domain you want to change and go to the DNS panel on that domain's dashboard.
3. To add a record, click the "Add record" button, select the record's type, fill in the required values, and click "Save". If you're making changes to an existing record, you only need to click on the record, update the record's values, and save your changes.
#### Heroku
TODO: Document.
#### Algolia
TODO: Document.
### Docs
This page details processes related to maintaining and updating the ([Fleet docs](https://fleetdm.com/docs)).
When someone asks a question in a public channel, it's safe to assume they aren't the only person looking for an answer.
To make our docs as helpful as possible, the Community team gathers these questions and uses them to make a weekly documentation update.
Fleet's goal is to answer every question with a link to the docs and/or result in a documentation update.
#### Documentation DRIs
TODO: Document.
#### Tracking
When responding to a question or issue in the [#fleet channel of the osquery Slack workspace](https://fleetdm.com/slack), push the thread to Zapier using the `TODO: Update docs` Zap. This will add information about the thread to the [Slack Questions Spreadsheet](https://docs.google.com/spreadsheets/d/15AgmjlnV4oRW5m94N5q7DjeBBix8MANV9XLWRktMDGE/edit#gid=336721544). In the `Notes` field, you can include any information that you think will be helpful when making weekly doc updates. That may be something like
- proposed change to the documentation.
- documentation link that was sent as a response.
- link to associated thread in [#help-oncall](https://fleetdm.slack.com/archives/C024DGVCABZ).
- **Note:** When submitting any pull request that changes Markdown files in the docs, request an editor review from Kathy Satterlee, who will escalate to the [on-call engineer](https://fleetdm.com/handbook/engineering#oncall-rotation) as needed.
## Commonly used terms
This glossary provides definitions to commonly used terms within our space.
| Term | Meaning |
@ -1454,6 +1240,49 @@ This glossary provides definitions to commonly used terms within our space.
#### Stubs
The following stubs are included only to make links backward compatible.
##### Website
Please see 📖[handbook/company/communications#fleetdm-com](https://fleetdm.com/handbook/company/communications#fleetdm-com).
##### Responsibilities
Please see 📖[handbook/digital-experience#responsibilities](https://fleetdm.com/handbook/digital-experience#responsibilities).
##### Website Rituals
Please see 📖[handbook/digital-experience#rituals](https://fleetdm.com/handbook/digital-experience#rituals).
##### Website roadmap
Please see 📖[handbook/digital-experience#contact-us](https://fleetdm.com/handbook/digital-experience#contact-us).
##### Requesting changes
Please see 📖[handbook/digital-experience#contact-us](https://fleetdm.com/handbook/digital-experience#contact-us).
Please see 📖[handbook/digital-experience#qa-a-change-to-fleetdm-com](https://fleetdm.com/handbook/digital-experience#qa-a-change-to-fleetdm-com) for below.
##### Quality
##### Manual QA
##### Writing QA steps
##### Actioning QA steps
##### Additional QA
##### Error handling
Please see 📖[handbook/digital-experience#qa-a-change-to-fleetdm-com](https://fleetdm.com/handbook/digital-experience#qa-a-change-to-fleetdm-com) for above.
##### Responding to a 5xx error on fleetdm.com
Please see 📖[handbook/digital-experience#respond-to-a-5xx-error-on-fleetdm-com](https://fleetdm.com/handbook/digital-experience#respond-to-a-5xx-error-on-fleetdm-com).
##### The "Deploy Fleet Website" GitHub action failed
Please see 📖[handbook/digital-experience#re-run-the-deploy-fleet-website-action](https://fleetdm.com/handbook/digital-experience#re-run-the-deploy-fleet-website-action).
##### Vulnerability monitoring
Please see 📖[handbook/digital-experience#check-production-dependencies-of-fleetdm.com](https://fleetdm.com/handbook/digital-experience#check-production-dependencies-of-fleetdm.com).
##### Landing pages and website experiments
Please see 📖[handbook/digital-experience#generate-a-new-landing-page](https://fleetdm.com/handbook/digital-experience#generate-a-new-landing-page).
##### How to export images for the website
Please see 📖[handbook/digital-experience#export-an-image-for-fleetdm-com](https://fleetdm.com/handbook/digital-experience#export-an-image-for-fleetdm-com).
##### Incident post-mortems
Please see 📖[handbook/digital-experience#export-an-image-for-fleetdm-com](https://fleetdm.com/handbook/digital-experience#export-an-image-for-fleetdm-com).
Please see 📖[handbook/company/communications#purchase-company-issued-equipment](https://fleetdm.com/handbook/company/communications#equipment) for below.
##### Equipment retention and replacement
##### Returning equipment
@ -1463,10 +1292,25 @@ Please see 📖[handbook/company/communications#purchase-company-issued-equipmen
Please see 📖[handbook/company/communications#purchase-company-issued-equipment](https://fleetdm.com/handbook/company/communications#equipment) for above.
##### Purchasing a company-issued device
Please see 📖[handbook/business-operations/#secure-company-issued-equipment-for-a-team-member](https://fleetdm.com/handbook/business-operations/#secure-company-issued-equipment-for-a-team-member).
Please see 📖[handbook/business-operations#secure-company-issued-equipment-for-a-team-member](https://fleetdm.com/handbook/business-operations#secure-company-issued-equipment-for-a-team-member).
##### Company travel
Please see 📖[handbook/company/communications#travel](https://fleetdm.com/handbook/company/communications#travel).
##### Estimation sessions
Please see 📖[handbook/company/product-groups#user-story-discovery](https://fleetdm.com/handbook/company/product-groups#user-story-discovery).
##### Website services
Please see 📖[handbook/digital-experience](https://fleetdm.com/handbook/digital-experience).
##### Testing changes
Please see 📖[handbook/digital-experience#test-fleetdm-com-locally](https://fleetdm.com/handbook/digital-experience#test-fleetdm-com-locally).
##### Cloudflare
Please see 📖[handbook/digital-experience#edit-a-dns-record](https://fleetdm.com/handbook/digital-experience#edit-a-dns-record).
##### Incident post-mortems
Please see 📖[handbook/engineering#perform-an-incident-postmortem](https://fleetdm.com/handbook/product-groups#perform-an-incident-postmortem).
<meta name="maintainedBy" value="mikermcneil">
<meta name="title" value="🛰️ Communications">

View File

@ -4,21 +4,46 @@ This page covers the things managers and other leaders at Fleet need to know abo
## CEO flaws
[Openness](https://fleetdm.com/handbook/company#values) is important, and so I want to live that by sharing the flaws I know I have. Im fully responsible for improving the things below, listing them is no excuse. They are listed here for two reasons. The first one is so that people know it is _not just them_, but actually _my fault_. The second one is so I can improve, I hope that listing them here lets people know I appreciate when you speak up to me about them.
[Openness](https://fleetdm.com/handbook/company#values) is important, and so I want to live that by sharing the flaws I know I have. Im fully responsible for improving the things below, listing them is no excuse.
- I often need to talk to think. If you get lost, you can interrupt me or send a Zoom chat: "Hold up, could you go over that again?"
- I can speak quickly. It is ok to say “Mike, hold on a second.” (Noah is good at this.)
- I can be quick to criticize before I appreciate, especially if something looks almost done. I appreciate it when fleeties ask “What do you think of my work?” It reminds me I'm speaking in front of the sculptor, not just alone with the statue.
- I can hurry to decisions when I think something is time sensitive or nearly ready to ship. It is ok to say “Im looking for early feedback.” It is ok to say “Im not yet 70% sure Im 100% done with this."
- I can be inconsistent about how certain I sound about the same topics at different times. Even when I am probably still certain. That's because I try to question blind certainty, even my own. The problem is, I don't always remember why I first became certain about every topic. It is okay to stop and share what you observe: "You seem less sure about this. What's up?" If I am waffling, it can be helpful to say "Did we write down a decision on that? I'll make a GitHub issue." (Luke is good at this.)
- I get grumpy when I am tired and I worry that I'll forget to follow up about things. You can say: "Would it help if I made a confidential issue about this for you and Sam to go over tonight?"
- I sometimes will keep talking longer than I otherwise would (ESPECIALLY OVER ZOOM AND ESPECIALLY WHILE SCREENSHARING) when it is harder to see faces and pick up on subtle cues. This is exacerbated by me being afraid Ill forget to come back to the topic, and feeling like I have to get to address it immediately or it will be lost. When you think I am riffing 🎸 /monologuing 🗣️ on a tangential topic that isnt in the agenda, its ok to interrupt by sharing your screen and show me the sprawl visually in the notes youve been taking, select the text of the tangent and say: “Im taking notes, and I noticed that weve veered onto a tangent. Do you want me to tag you in a Google Doc comment to follow up later on this?"
These flaws are listed here publicly for two reasons. The first is so that people know it is _not just them_, but actually _my fault_. The second is so I can improve and be held accountable.
1. I have a bad habit of not wanting to impose.
- But this can result in me [over-specifying solutions for problems](https://docs.google.com/document/d/1PUkMIBIStDe87drezqKURYz_ZAmMzQPg_PilmWORmsM/edit), instead of presenting the whole problem and putting someone in charge of it.
- You can say: "Mike, what was the original problem? If I'm not the right person to run with solving it, I understand, but could you let me know some time candidly why I'm not?"
2. I can get nervous and rush things.
- I can speak quickly. It is ok to say “Mike, hold on a second.”
- I can be quick to criticize before I appreciate, especially if something looks almost done.
- I appreciate it when fleeties ask “What do you think of my work?” It reminds me I'm speaking in front of the sculptor, not just alone with the statue.
- I can hurry to decisions when I think something is time-sensitive or nearly ready to ship. It is ok to say “Im looking for early feedback" vs. “Im not yet 70% sure Im 100% done with this."
3. I often need to talk to think.
- It is ok to set a boundary and let me marinade. You might say: "Hold up. What is our goal?"
- I get grumpy when I am tired and I worry that I'll forget to follow up about things. So I try to say them all.
- I can talk too much. On video calls, and especially during a screenshare session, I sometimes will keep talking longer than I otherwise would. If you get lost, or overwhelmed, you can interrupt me or send a chat: "Hold up, I'm feeling out of phase from this conversation."
<!-- Cutting back on some content just to make this more readable. Prioritizing the ones that matter the most. -mike feb 2024.
- I can be inconsistent about how certain I sound about the same topics at different times. Even when I am probably still certain. That's because I try to question blind certainty, even my own. The problem is, I don't always remember why I first became certain about every topic. It is okay to stop and share what you observe: "You seem less sure about this. What's up?" If I am waffling, it can be helpful to say "Did we write down a decision on that? I'll make a GitHub issue." ([Luke](https://fleetdm.com/handbook/engineering#team) is good at this.)
- I sometimes will keep talking longer than I otherwise would (ESPECIALLY OVER ZOOM AND ESPECIALLY WHILE SCREENSHARING) when it is harder to see faces and pick up on subtle cues. This is exacerbated by me being afraid Ill forget to come back to the topic, and feeling like I have to get to address it immediately or it will be lost. When you think I am riffing 🎸 /monologuing 🗣️ on a tangential topic that isnt on the agenda, it's always ok to interrupt and tell me to my face, or send me a direct message.
-->
> If you notice one of these flaws, and especially if you deliver feedback about it and don't feel heard, or you feel hurt, or you feel like I didn't "get it", please send me a link to this section of the handbook, or just interrupt me and give me [feedback in the moment](https://fleetdm.com/handbook/company/communications#performance-feedback). I will be extremely grateful, and value your bravery in pursuit of what's in the best interest of the company. (And if I don't, keep trying. I'll come crawling back. Promise.)
## Contact the CEO
**Still want to contact the CEO?** You can send `@mikermcneil` a DM in Fleet Slack, at-mention our CEO in the [#help-leadership channel](https://fleetdm.slack.com/archives/C02HWSTJ17Z), or [schedule time with the CEO](https://fleetdm.com/handbook/company/communications#schedule-time-with-the-ceo).
## CEO responsibilities
Ultimately, the CEO is responsible for the success or failure of the company. The CEO is the [directly responsible individual (DRI)](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) for pricing, tiers, the business model, signatures on all documents, product marketing (brandfronts, pitchfronts, featurefronts, ICPs, personas, and targeting).
> **Note:** When the CEO is out of office, CEO responsibilities are either paused, delegated, or coordinated through the Apprentice to the CEO so they can be handled promptly. (It depends on the responsibility and the situation.)
> **Note:** When the CEO is out of office, CEO responsibilities are either paused, delegated, or coordinated through the [Apprentice to the CEO](https://fleetdm.com/handbook/digital-experience#team) so they can be handled promptly. (It depends on the responsibility and the situation.)
## Outline of departmental page structure

View File

@ -533,7 +533,7 @@
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14550
- description: Target profiles to specific hosts using SQL.
moreInfoUrl: https://github.com/fleetdm/fleet/issues/14715
- description: Automatically re-deploy configuration profiles on macOS they're not installed.
- description: Automatically re-deploy configuration profiles when they're not installed.
productCategories: [Device management]
pricingTableCategories: [Device management]
- industryName: Self service
@ -571,8 +571,8 @@
productCategories: [Device management]
pricingTableCategories: [Device management]
waysToUse:
- description: Ship a macOS workstation to the end users home and have them automatically enroll to Fleet during out-of-the-box setup.
- description: Ship a Windows workstation to the end users home and have them automatically enroll to Fleet during out-of-the-box setup.
- 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.
- description: Require end users to authenticate with your identity provider (IdP) and agree to an end user license agreement (EULA) before they can use their new workstation
- industryName: Enforce OS updates
@ -583,7 +583,7 @@
pricingTableCategories: [Device management]
waysToUse:
- description: Enforce macOS updates via Nudge.
- description: Automatically update Windows after the end user reaches a deadline. Coming soon (2023-12-30) #Customer-preston
- description: Automatically update Windows after the end user reaches a deadline.
- industryName: Encrypt macOS hard disks with FileVault
documentationUrl: https://fleetdm.com/docs/using-fleet/mdm-disk-encryption
tier: Premium
@ -732,11 +732,6 @@
tier: Premium
waysToUse:
- description: Automatically set admin access to Fleet based on your IDP
- industryName: Vanta integration
productCategories: [Endpoint operations,Device management]
pricingTableCategories: [Integrations]
usualDepartment: IT
tier: Premium
- industryName: Trigger a workflow based on a failing policy
productCategories: [Endpoint operations,Device management]
pricingTableCategories: [Integrations]
@ -848,3 +843,9 @@
usualDepartment: Security
productCategories: [Endpoint operations]
pricingTableCategories: [Endpoint operations]
- industryName: Asset discovery* #Coming soon (TBD)
tier: Premium
usualDepartment: Security
productCategories: [Vulnerability management]
pricingTableCategories: [Vulnerability management]
comingSoonOn: 2024-06-30

View File

@ -67,9 +67,9 @@ To deliver on this mission, we need a clear, repeatable process for turning an i
> Learn more about Fleet's philosophy and process for making interface changes to the product, and [why we use a wireframe-first approach](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach).
To make a change to Fleet:
- First, [get it prioritized](https://fleetdm.com/handbook/product).
- Then, it will be [drafted](https://fleetdm.com/handbook/company/development-groups#drafting) (planned).
- Next, it will be [implemented](https://fleetdm.com/handbook/company/development-groups#implementing) and [released](https://fleetdm.com/handbook/engineering#release-process).
- First, [get it prioritized](https://fleetdm.com/handbook/product-design).
- Then, it will be [drafted](https://fleetdm.com/handbook/company/product-groups#drafting) (planned).
- Next, it will be [implemented](https://fleetdm.com/handbook/company/product-groups#implementing) and [released](https://fleetdm.com/handbook/engineering#release-process).
### Planned and unplanned changes
Most changes to Fleet are planned changes. They are [prioritized](https://fleetdm.com/handbook/product), defined, designed, revised, estimated, and scheduled into a release sprint _prior to starting implementation_. The process of going from a prioritized goal to an estimated, scheduled, committed user story with a target release is called "drafting", or "the drafting phase".
@ -143,6 +143,7 @@ User stories are small and independently valuable.
#### Engineering-initiated stories
<!-- TODO: Move steps to "Create an Engineering-initiated story" to handbook/engineering#responsibilities -->
Engineering-initiated stories are types of user stories created by engineers to make technical changes to Fleet. Technical changes should improve the user experience or contributor experience. For example, optimizing SQL that improves the response time of an API endpoint improves user experience by reducing latency. A script that generates common boilerplate, or automated tests to cover important business logic, improves the quality of life for contributors, making them happier and more productive, resulting in faster delivery of features to our customers.
It is important to frame engineering-initiated user stories the same way we frame all user stories. Stay focused on how this technical change will drive value for our users.
@ -405,7 +406,7 @@ Once the bug is properly labeled, assign it to the [relevant engineering manager
#### In product drafting (as needed)
If a bug requires input from product the `:product` label is added, the `:release` label is removed, and the PM is assigned to the issue. It will stay in this state until product closes the bug, or removes the `:product` label and assigns to an EM.
#### In engineering
#### In engineering
A bug is in engineering after it has been reproduced and assigned to an EM. If a bug meets the criteria for a [critical bug](https://fleetdm.com/handbook/engineering#critical-bugs), the `~critical bug` label is added, and the EM follows the [critical bug notification process](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Releasing-Fleet.md#critical-bug-notification-process).
During daily standup, the EM will filter the board to only `:incoming` bugs and review with the team. The EM will remove the `:incoming` label, prioritize the bug in the "Ready" coulmn, unassign themselves, and assign an engineer or leave it unassigned for the first available engineer.
@ -481,6 +482,7 @@ How to escalate:
2. Create a new thread in the [#help-engineering channel](https://fleetdm.slack.com/archives/C019WG4GH0A), tagging `@zwass` and provide the information turned up in your research. Please include possibly relevant links (even if you didn't find what you were looking for there). Zach will work with you to craft an appropriate answer or find another team member who can help.
### Changing of the guard
The on-call developer changes each week on Wednesday.
@ -579,7 +581,7 @@ OPTIONS
## Meetings
<!--
<!-- TODO: Find out what to do with this stuff. Delete?
### Eng Together
This meeting is to disseminate engineering-wide announcements, promote cohesion across groups within the engineering team, and connect with engineers (and the "engineering-curious") in other departments. Held monthly for one hour.
@ -641,14 +643,19 @@ Design consultations are scheduled as needed with the relevant participants, typ
- Discuss implementation details
### Design reviews
Design reviews are [conducted daily by the CEO](https://fleetdm.com/handbook/digital-experience#calendar-audit).
Design reviews are conducted daily between the [Head of Product Design](https://fleetdm.com/handbook/product-design#team) and contributors proposing changes to Fleet's interfaces, such as the graphical user interface (GUI) or REST API. This fast cadence shortens the feedback loop, makes progress visible, and encourages early feedback. This helps Fleet stay intentional about how the product is designed and minimize common issues like UI inconsistencies or accidental breaking changes to the API.
The product designer prepares proposed changes in the form of wireframes for this meeting, and presents them quickly. Here are some tips for making this meeting effective:
Product designers or other contributors come prepared to this meeting with their proposed changes in a GitHub issue. Usually these are in the form of Figma wireframes, a pull request to the API docs showing changes, or a demo of a prototype. The Head of Product Design and other participants review the changes quickly and give feedback, and then the contributor applies revisions and attends again the next day or as soon as possible for another go-round. The Head of Product Design is responsible for looping in the right engineers, community members, and other subject-matter experts to iterate on and refine upcoming product changes in the best interest of the business.
Here are some tips for making this meeting effective:
- Bring 1 key engineer who has been helping out with the user story, when possible and helpful.
- Say the user story out loud to remind participants of what it is.
- At the beginning of describing your change, indicate whether you are 70% sure you are 100% done, or are looking for early feedback.
- Avoid explaining or showing multiple ways it could work. Show the one way you think it should work and let your work speak for itself.
- For follow-ups, repeat the user story, but show only what has changed or been added since the last review.
- Zoom in.
- Read Fleet's [best practices for meetings](https://fleetdm.com/handbook/company/communications#meetings).
> To allow for asynchronous participation, instead of attending, contributors can alternatively choose to add an agenda item to the "Product design review" meeting with a GitHub link. Then, the Head of Product Design will review during the meeting and provide feedback. Every "Product design review" is recorded and automatically transcribed to a Google Doc so that it is searchable by every Fleet team member.
### Weekly bug review
QA has weekly check-in with product to go over the inbox items. QA is responsible for proposing “not a bug”, closing due to lack of response (with a nice message), or raising other relevant questions. All requires product agreement

View File

@ -20,6 +20,146 @@ This page details processes specific to working [with](#contact-us) and [within]
## Responsibilities
The Digital Experience department is directly responsible for the framework, content design, and technology behind Fleet's remote work culture, including fleetdm.com, the handbook, issue templates, UI style guides, internal tooling, Zapier flows, Docusign templates, key spreadsheets, and project management processes.
### QA a change to fleetdm.com
Each PR to the website is manually checked for quality and tested before before going live on fleetdm.com. To test any change to fleetdm.com
1. Write clear step-by-step instructions to confirm that the change to the fleetdm.com functions as expected and doesn't break any possible automation. These steps should be simple and clear enough for anybody to follow.
2. [View the website locally](#test-changes-to-the-website) and follow the QA steps in the request ticket to test changes.
3. Check the change in relation to all breakpoints and [browser compatibility](https://fleetdm.com/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.
### 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:
- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet).
- [Node.js](https://nodejs.org/en/download/)
- (Optional) [Sails.js](https://sailsjs.com/) installed globally on your machine (`npm install sails -g`)
Once you have the above follow these steps:
1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo.
> Note: If this is your first time running this script, you will need to run `npm install` inside of the website/ folder to install the website's dependencies.
2. Run the `build-static-content` script to generate HTML pages from our Markdown and YAML content.
- **With Node**, you will need to use `node ./node_modules/sails/bin/sails run build-static-content` to execute the script.
- **With Sails.js installed globally** you can use `sails run build-static-content` to execute the script.
> You can use the `--skipGithubRequests` flag to skip requests made to GitHub if you get rate-limited by GitHubs API while running this script.
>
> e.g., `node ./node_modules/sails/bin/sails run build-static-content --skipGithubRequests`
> When this script runs, the website's configuration file ([`website/.sailsrc`](https://github.com/fleetdm/fleet/blob/main/website/.sailsrc)) will automatically be updated with information the website uses to display content built from Markdown and YAML. Changes to this file should never be committed to the GitHub repo. If you want to exclude changes to this file in any PRs you make, you can run this terminal command in your local copy of the Fleet repo: `git update-index --assume-unchanged ./website/.sailsrc`.
3. Once the script is complete, start the website server. From the `website/` folder:
- **With Node.js:** start the server by running `node ./node_modules/sails/bin/sails lift`
- **With Sails.js installed globally:** start the server by running `sails lift`.
4. When the server has started, the Fleet website will be available at [http://localhost:2024](http://localhost:2024)
> **Note:** Some features, such as self-service license dispenser and account creation, are not available when running the website locally. If you need help testing features on a local copy, reach out to `@eashaw` in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) channel on Slack.
### Edit a DNS record
We use Cloudflare to manage the DNS records of fleetdm.com and our other domains. To make DNS changes in Cloudflare:
1. Log into your Cloudflare account and select the "Fleet" account.
2. Select the domain you want to change and go to the DNS panel on that domain's dashboard.
3. To add a record, click the "Add record" button, select the record's type, fill in the required values, and click "Save". If you're making changes to an existing record, you only need to click on the record, update the record's values, and save your changes.
> If you need access to Fleet's Cloudflare account, please ask the [DRI](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) Zach Wasserman in Slack for an invitation.
### Check production dependencies of fleetdm.com
Every week, we run `npm audit --only=prod` to check for vulnerabilities on the production dependencies of fleetdm.com. Once we have a solution to configure GitHub's Dependabot to ignore devDependencies, this manual process can be replaced with Dependabot.
### Respond to a 5xx error on fleetdm.com
Production systems can fail for various reasons, and it can be frustrating to users when they do, and customer experience is significant to Fleet. In the event of system failure, Fleet will:
- investigate the problem to determine the root cause.
- identify affected users.
- escalate if necessary.
- understand and remediate the problem.
- notify impacted users of any steps they need to take (if any). If a customer paid with a credit card and had a bad experience, default to refunding their money.
- Conduct an incident post-mortem to determine any additional steps we need (including monitoring) to take to prevent this class of problems from happening in the future.
### Check browser compatibility for fleetdm.com
A browser compatibility check of [fleetdm.com](https://fleetdm.com/) should be carried out monthly to verify that the website looks and functions as expected across all [supported browsers](https://fleetdm.com/docs/using-fleet/supported-browsers).
- We use [BrowserStack](https://www.browserstack.com/users/sign_in) (logins can be found in [1Password](https://start.1password.com/open/i?a=N3F7LHAKQ5G3JPFPX234EC4ZDQ&v=3ycqkai6naxhqsylmsos6vairu&i=nwnxrrbpcwkuzaazh3rywzoh6e&h=fleetdevicemanagement.1password.com)) for our cross-browser checks.
- Check for issues against the latest version of Google Chrome (macOS). We use this as our baseline for quality assurance.
- Document any issues in GitHub as a [bug](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md&title=), and assign them for fixing.
- If in doubt about anything regarding design or layout, please reach out to the [Head of Design](https://fleetdm.com/hanbook/digital-experience#team).
### Export an image for fleetdm.com
In Figma:
1. Select the layers you want to export.
2. Confirm export settings and naming convention:
- Item name - color variant - (CSS)size - @2x.fileformat (e.g., `os-macos-black-16x16@2x.png`)
- Note that the dimensions in the filename are in CSS pixels. In this example, if you opened it in preview, the image would actually have dimensions of 32x32px but in the filename, and in HTML/CSS, we'll size it as if it were 16x16. This is so that we support retina displays by default.
- File extension might be .jpg or .png.
- Avoid using SVGs or icon fonts.
3. Click the __Export__ button.
### Generate a new landing page
Experimental pages are short-lived, temporary landing pages intended for a small audience. All experiments and landing pages need to go through the standard [drafting process](https://fleetdm.com/handbook/company/product-groups#making-changes) before they are created.
Website experiments and landing pages live behind `/imagine` url. Which is hidden from the sitemap and intended to be linked to from ads and marketing campaigns. Design experiments (flyers, swag, etc.) should be limited to small audiences (less than 500 people) to avoid damaging the brand or confusing our customers. In general, experiments that are of a design nature should be targeted at prospects and random users, never targeted at our customers.
Some examples of experiments that would live behind the `/imagine` url:
- A flyer for a meetup "Free shirt to the person who can solve this riddle!"
- A landing page for a movie screening presented by Fleet
- A landing page for a private event
- A landing page for an ad campaign that is running for 4 weeks.
- An A/B test on product positioning
- A giveaway page for a conference
- Table-top signage for a conference booth or meetup
The Fleet website has a built-in landing page generator that can be used to quickly create a new page that lives under the /imagine/ url.
To generate a new page, you'll need:
- A local copy of the [Fleet repo](https://github.com/fleetdm/fleet).
- [Node.js](https://nodejs.org/en/download/)
- (Optional) [Sails.js](https://sailsjs.com/) installed globally on your machine (`npm install sails -g`)
1. Open your terminal program, and navigate to the `website/` folder of your local copy of the Fleet repo.
> Note: If this is your first time running the website locally, you will need to run `npm install` inside of the website/ folder to install the website's dependencies.
2. Call the `landing-page` generator by running `node ./node_modules/sails/bin/sails generate landing-page [page-name]`, replacing `[page-name]` with the kebab-cased name (words seperated by dashes `-`) of your page.
3. After the files have been generated, you'll need to manually update the website's routes. To do this, copy and paste the generated route for the new page to the "Imagine" section of `website/config/routes.js`.
4. Next you need to update the stylesheets so that the page can inherit the correct styles. To do this, copy and paste the generated import statement to the "Imagine" section of `website/assets/styles/importer.less`.
5. Start the website by running `node ./node_modules/sails/bin/sails lift` (or `sails lift` if you have Sails installed globally). The new landing page will be availible at `http://localhost:1337/imagine/{page-name}`.
6. Replace the lorum ipsum and placeholder images on the generated page with the page's real content, and add a meta description and title by changing the `pageTitleForMeta` and `pageDescriptionForMeta in the page's `locals` in `website/config/routes.js`.
### Restart Algolia manually
At least once every hour, an Algolia crawler reindexes the Fleet website's content. If an error occurs while the website is being indexed, Algolia will block our crawler and respond to requests with this message: `"This action cannot be executed on a blocked crawler"`.
When this happens, you'll need to manually start the crawler in the [Algolia crawler dashboard](https://crawler.algolia.com/admin/) to unblock it.
You can do this by logging into the crawler dashboard using the login saved in 1password and clicking the "Restart crawling" button on our crawler's "overview" page](https://crawler.algolia.com/admin/crawlers/497dd4fd-f8dd-4ffb-85c9-2a56b7fafe98/overview).
No further action is needed if the crawler successfully reindexes the Fleet website. If another error occurs while the crawler is running, take a screenshot of the error and add it to the GitHub issue created for the alert and @mention `eashaw` for help.
### Re-run the "Deploy Fleet Website" action
If the action fails, please complete the following steps:
1. Head to the fleetdm-website app in the [Heroku dashboard](https://heroku.com) and select the "Activity" tab.
2. Select "Roll back to here" on the second to most recent deploy.
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:
@ -28,13 +168,14 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid
| 🐋 AE | ✅ | ✅ | ✅ | ✅ | ✅
| 🐋 CSM | ✅ | ✅ | ❌ | ✅ | ✅
| 🐋 SC | ✅ | ✅ | ❌ | ❌ | ✅
| ⚗️ PM | ❌ | ❌ | ❌ | ✅ | ✅
| ⚗️ PD | ❌ | ❌ | ❌ | ✅ | ✅
| ⚗️ 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).
@ -60,6 +201,7 @@ From time to time, you will need to schedule an interview between a candidate an
- Add candidate's [LinkedIn url](https://www.linkedin.com/search/results/all/?keywords=people) on the first bullet for Mike.
3. Set the Google Calendar description of the calendar event to: `Agenda: URL_FOR_NEW_COPY_OF_FINAL_INTERVIEW_DOC`
### Program the CEO to do something
1. If necessary or if unsure, immediately direct message the CEO on Slack to clarify priority level, timing, and level of effort. (For example, whether to schedule 30m or 60m to complete in full, or 30m planning as an iterative step.)
2. If there is not room on the calendar to schedule this soon enough with both Mike and Sam as needed (erring on the side of sooner), then either immediately direct message the CEO with a backup plan, or if it can obviously wait, then discuss at the next roundup.
@ -76,7 +218,8 @@ Agenda:
> Keep calendar event titles short so they are readable at a glance. Please include any other info via link, so that information is not duplicated or lost in the calendar.
### Prepare for CEO office minutes meeting
### Prepare for CEO office minutes
Before the start of the meeting, the Apprentice to the CEO will prepare the "CEO office minutes" meeting [agenda](https://docs.google.com/document/d/12cd0N8KvHkfJxYlo7ggdisrvqw4MCErDoIzLjmBIdj4/edit) such that the following is true:
1. All agenda items are prefixed with a date of when the item will be covered and name of the person requesting to discuss the issue.
2. All team members with an agenda item have added themselves **and their manager** to the correct calendar event. If the team member or manager hasn't been added to the calendar event before the meeting begins, the agenda item is de-prioritized in favor of others with representatives in attendance.
@ -84,6 +227,7 @@ Before the start of the meeting, the Apprentice to the CEO will prepare the "CEO
> If the manager is unable to attend the scheduled time of the meeting, the Apprentice will work with the team member to schedule an adhoc meeting between them, their manager, and the CEO.
### Process the CEO's calendar
Time management for the CEO is essential. The Apprentice processes the CEO's calendar multiple times per day.
@ -106,6 +250,7 @@ Time management for the CEO is essential. The Apprentice processes the CEO's ca
- Google Drive
- Be sure to do this from the CEO's browser so as to not lock him out of any meeting docs.
### Process the CEO's inbox
- The Apprentice to the CEO is [responsible](https://fleetdm.com/handbook/company/why-this-way#why-direct-responsibility) for [processing all email traffic](https://docs.google.com/document/d/1gH3IRRgptrqSYzBFy-77g98JROTL8wqrazJIMkp-Gb4/edit#heading=h.i7mkhr6m123r) prior to CEO review.
The Apprentice will reduce the scope of Mike's inbox to only include necessary and actionable communication.
@ -113,11 +258,13 @@ The Apprentice will reduce the scope of Mike's inbox to only include necessary a
- Escalate actionable sales communication and update Mike directly.
- Ensure all calendar invites have the necessary documents included.
### Document performance feedback
Every Friday at 5PM a [Business Operations team member](https://fleetdm.com/handbook/business-operations#team) will look for missing data in the [KPIs spreadsheet](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit#gid=0).
1. If KPIs are not reported on time, the BizOps Engineer will notify the Apprentice to the CEO and the DRI.
2. The Apprentice will update the "performance management" section of the appropriate individual's 1:1 doc so that the CEO can address during the next 1:1 meeting with the DRI.
### Send the weekly update
We like to be open about milestones and announcements.
- Every Friday, e-group members [report their KPIs for the week](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit) by 5:00pm U.S. Central Time Zone.
@ -144,6 +291,7 @@ We like to be open about milestones and announcements.
- 📬 **Send it!**
### Troubleshoot signature automation
We use Zapier to automate how completed DocuSign envelopes are formatted and stored. This process ensures we store signed documents in the correct folder and that filenames are formatted consistently.
When the final signature is added to an envelope in DocuSign, it is marked as completed and sent to Zapier, where it goes through these steps:
@ -173,6 +321,7 @@ The Apprentice schedules all travel arrangements for the CEO including flights,
- Use the Brex card.
- Frequent flyer details of all (previously flown) airlines are in 1Password as well as important travel documents.
### Process incoming equipment
Upon receiving any device, the Apprentice will process the incoming equipment by:
1. Search for the SN of the physical device in the ["Company equipment" spreadsheet](https://docs.google.com/spreadsheets/d/1hFlymLlRWIaWeVh14IRz03yE-ytBLfUaqVz0VVmmoGI/edit#gid=0) to confirm the correct equipment was received.
@ -185,6 +334,7 @@ Upon receiving any device, the Apprentice will process the incoming equipment by
8. Using the "Recovery assistant" tab (In the top left corner), select "Delete this Mac".
9. Follow the prompts to activate the device and reinstall the appropriate version of macOS.
### Ship approved equipment
Once the Business Operations department approves inventory to be shipped from Fleet IT, the Apprentice will ship the equipment by:
1. Compare the equipment request issue with the ["Company equipment" spreadsheet](https://docs.google.com/spreadsheets/d/1hFlymLlRWIaWeVh14IRz03yE-ytBLfUaqVz0VVmmoGI/edit#gid=0) and verify physical inventory.
@ -211,6 +361,7 @@ Once the Business Operations department approves inventory to be shipped from Fl
The day before the All hands, Mike will prepare slides that reflect the CEO vision and focus.
#### Share recording of all hands meeting
The Apprentice will post a link to the All hands Gong recording and slide deck in Slack.
@ -236,15 +387,6 @@ You can also grab a copy of the [original slides](https://fleetdm.com/handbook/c
📬 **Send it!**
<!--
### Connect with recently active community members
Read the three 3 most recent questions asked in [osquery](https://osquery.slack.com/archives/C01DXJL16D8) and [MacAdmins](https://macadmins.slack.com/archives/C19MR7EM9) Slack. Find each contributor on [LinkedIn](https://www.linkedin.com/search/results/all/?sid=54z).
Send connect request (blank).
Goal: No one else is currently LinkedIn connecting with community Slack participants. This puts a face to the project and welcomes them to Fleet.
--->
### Process and backup Sid agenda
Every two weeks, our CEO Mike has a meeting with Sid Sijbrandij. The CEO uses dedicated (blocked, recurring) time to prepare for this meeting earlier in the week.
@ -258,6 +400,7 @@ Then process the backup Sid agenda by:
**Being sure to preserve agenda format**, process the 💻 Sid : Mike(Fleet) master doc by:
- (Unless otherwise prefixed) Delete all agenda items, **being sure to leave 3 empty bullets in every section**.
### Process and backup E-group agenda
Immediately after every e-group the Apprentice makes a copy of the E-group agenda doc and renames it "YYYY-MM-DD backup of E-group agenda". Then saves it to the [(¶¶) E-group archive](https://drive.google.com/drive/u/0/folders/1IsSGMgbt4pDcP8gSnLj8Z8NGY7_6UTt6).
@ -275,6 +418,7 @@ Then process the backup E-group agenda by:
If it's the day of an All hands:
- Remove any spotlights that aren't a permanent staple (e.g. Mike: Every time: Pick a value, present on it.).
### Check LinkedIn for unread messages
Once a day the Apprentice will confirm check LinkedIn for unread messages.
- Log into the CEO's [LinkedIn](https://www.linkedin.com/search/results/all/?sid=s2%3A).
@ -283,6 +427,7 @@ Once a day the Apprentice will confirm check LinkedIn for unread messages.
- Click "Unread".
Bring all unreads to the CEO.
### Unroll a Slack thread
From time to time the CEO will ask the Apprentice to the CEO to unroll a Slack thread into a well-named whiteboard google doc for safekeeping and future searching.
1. Start with a new doc.
@ -292,6 +437,7 @@ From time to time the CEO will ask the Apprentice to the CEO to unroll a Slack t
- To copy images right-click+copy and then paste in the doc (some resizing may be necessary to fit the page).
### Delete an accidental meeting recording
It's not enough to just "delete" a recording of a meeting in Gong. Instead, use these steps:

View File

@ -1,5 +1,36 @@
# https://github.com/fleetdm/fleet/pull/13084
-
task: "Check browser compatibility for fleetdm.com"
startedOn: "2024-03-01"
frequency: "Monthly"
description: "Run `npm audit --only=prod` to check for vulnerabilities on the production dependencies of fleetdm.com."
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#check-production-dependencies-of-fleetdm-com"
dri: "eashaw"
autoIssue: # Enables automation of GitHub issues
labels: [ "#g-digital-experience" ] # label to be applied to issue
repo: fleet
-
task: "Check production dependencies of fleetdm.com"
startedOn: "2023-11-10"
frequency: "Weekly"
description: "Check for vulnerabilities on the production dependencies of fleetdm.com."
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#check-production-dependencies-of-fleetdm-com"
dri: "eashaw"
autoIssue: # Enables automation of GitHub issues
labels: [ "#g-digital-experience" ] # label to be applied to issue
repo: fleet
-
task: "Generate latest schema"
startedOn: "2024-02-19"
frequency: "Triweekly"
description: "After each sprint, generate the latest tables json file to incorporate any new schema documentation."
moreInfoUrl: # TODO tie to a responsibility
dri: "eashaw"
autoIssue: # Enables automation of GitHub issues
labels: [ "#g-digital-experience" ] # label to be applied to issue
repo: fleet
-
task: "Check osquery Slack invitation"
startedOn: "2023-11-10"

View File

@ -5,14 +5,14 @@ This handbook page details processes specific to working [with](#team) and [with
| Role                            | Contributor(s) |
|:--------------------------------|:-----------------------------------------------------------------------------------------------------------|
| Head of Product Engineering | [Luke Heath](https://www.linkedin.com/in/lukeheath/) _([@lukeheath](https://github.com/lukeheath))_
| Engineering Manager | [George Karr](https://www.linkedin.com/in/george-karr-4977b441/) _([@georgekarrv](https://github.com/georgekarrv))_, [Sharon Katz](https://www.linkedin.com/in/sharon-katz-45b1b3a/) _([@sharon-fdm](https://github.com/sharon-fdm))_
| Product Quality Specialist | [Reed Haynes](https://www.linkedin.com/in/reed-haynes-633a69a3/) _([@xpkoala](https://github.com/xpkoala))_, [Sabrina Coy](https://www.linkedin.com/in/bricoy/) _([@sabrinabuckets](https://github.com/sabrinabuckets))_
| Developer | _See ["Current product groups"](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
| Engineering Manager | _See ["Current product groups"](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
| Product Quality Specialist | [Reed Haynes](https://www.linkedin.com/in/reed-haynes-633a69a3/) _([@xpkoala](https://github.com/xpkoala))_, [Sabrina Coy](https://www.linkedin.com/in/bricoy/) _([@sabrinabuckets](https://github.com/sabrinabuckets))_
| Developer | _See ["Current product groups"](https://fleetdm.com/handbook/company/product-groups#current-product-groups)_
## Contact us
- Any community member can [**file a 🦟 "Bug report"**](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&projects=&template=bug-report.md&title=) (If urgent, mention a [team member](#team) in the [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) Slack channel).
- Any Fleet team member can view the [Endpoint ops](https://app.zenhub.com/workspaces/-g-endpoint-ops-current-sprint-63bd7e0bf75dba002a2343ac/board), [MDM](https://app.zenhub.com/workspaces/-g-mdm-current-sprint-63bc507f6558550011840298/board), or [Website](https://app.zenhub.com/workspaces/-g-website--product-marketing--brand-6451748b4eb15200131d4bab/board) kanban boards including the status on all reported bugs.
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
- To **make a request** of this department, [create an issue](https://fleetdm.com/handbook/company/product-groups#current-product-groups) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in the [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) Slack channel.
- Any Fleet team member can [view the kanban boards](https://fleetdm.com/handbook/company/product-groups#current-product-groups) for this department, including pending tasks and the status of new requests.
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
## Responsibilities
The 🚀 Engineering department at Fleet is directly responsible for writing and maintaining the [code](https://github.com/fleetdm/fleet) for Fleet's core product and infrastructure.
@ -204,6 +204,7 @@ The on-call developer is responsible for:
- Successfully [transferring the on-call persona to the next developer](https://fleetdm.com/handbook/company/product-groups#changing-of-the-guard).
### Notify community members about a critical bug
<!-- TODO: Move back to product groups, it touches multiple departments -->
We inform customers and the community about critical bugs immediately so they dont trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the #help-product-design channel with the filed bug.
If the bug finder is not a Fleetie (e.g., a member of the community), then whoever sees the critical bug should raise the alarm. (We would expect this to be Customer success in the community Slack or QA in the bug inbox, though it could be anyone.) Note that the bug finder here is NOT necessarily the **first** person who sees the bug. If you come across a bug you think is critical, but it has not been escalated, raise the alarm!
@ -284,13 +285,14 @@ Steps to renew the certificate:
10. Verify by logging into a normal apple account (not billing@...) and Generate a new Push Certificate following our [setup MDM](https://fleetdm.com/docs/using-fleet/mdm-macos-setup#step-2-generate-an-apns-certificate) steps and verify the Expiration date is 1 year from today.
11. Adjust calendar event to be between 2-4 weeks before the next expiration.
### Preform an incident postmortem
### Perform an incident postmortem
<!-- TODO: move philosophy to product groups and link to this responsibility from there-->
At Fleet, we take customer incidents very seriously. After working with customers to resolve issues, we will conduct an internal postmortem to determine any process, documentation, or coding changes to prevent similar incidents from happening in the future. Why? We strive to make Fleet the best osquery management platform globally, and we sincerely believe that starts with sharing lessons learned with the community to become stronger together.
At Fleet, we do postmortem meetings for every service or feature outage and every critical bug, whether it's a customer's environment or on fleetdm.com.
- **Postmortem documentation**
Before running the postmortem meeting, copy this [Postmortem Template](https://docs.google.com/document/d/1Ajp2LfIclWfr4Bm77lnUggkYNQyfjePiWSnBv1b1nwM/edit?usp=sharing) document and populate it with some initial data to enable a productive conversation.
Before running the postmortem meeting, copy this [postmortem template](https://docs.google.com/document/d/1Ajp2LfIclWfr4Bm77lnUggkYNQyfjePiWSnBv1b1nwM/edit?usp=sharing) document and populate it with some initial data to enable a productive conversation.
- **Postmortem meeting**
Invite all stakeholders, typically the team involved and QA representatives.
@ -331,9 +333,6 @@ Please see [handbook/engineering#notify-community-members-about-a-critical-bug](
##### Finding bugs
Please see [handbook/engineering#run-fleet-locally-for-qa-purposes](https://fleetdm.com/handbook/engineering#run-fleet-localy-for-qa-purposes)
##### Engineering-initiated stories
Please see [handbook/company/engineering#create-an-engineering-initiated-user](https://fleetdm.com/handbook/company/product-groups#endpoint-ops-group).
##### Scrum at Fleet
Please see [handbook/company/product-groups#engineering-initiated-stories](https://fleetdm.com/handbook/company/product-groups#scrum-at-fleet)

View File

@ -1,78 +1,11 @@
# Scaling Fleet
Nowadays, Fleet, as a Go server, scales horizontally very well. Its not very CPU or memory intensive. In terms of load in infrastructure, from highest to lowest are: MySQL, Redis, and Fleet.
In general, we should burn a bit of CPU or memory on the Fleet side if it allows us to reduce the load on MySQL or Redis.
In many, caching helps, but given that we are not doing load balancing based on host id (i.e., make sure that the same host ends up in the same Fleet server). This goes only so far. Caching host-specific data is not done because round-robin LB means all Fleet instances end up circling the total list of hosts.
### How to prevent most of this
The best way weve got so far to prevent any scaling issues is to load test things. **Every new feature must have its corresponding osquery-perf implementation as part of the PR, and it should be tested at a reasonable scale for the feature**.
Besides that, you should consider the answer(s) to the following question: how can I know that the feature Im working on is working and performing well enough? Add any logs, metrics, or anything that will help us debug and understand whats happening when things unavoidably go wrong or take longer than anticipated.
**HOWEVER** (and forgive this Captain Obvious comment): do NOT optimize before you KNOW you have to. Dont hesitate to take an extra day on your feature/bug work to load test things properly.
## What have we learned so far?
This is a document that evolves and will likely always be incomplete. If you feel like something is missing, either add it or bring it up in any way you consider.
## Connecting to Dogfood MySQL & Redis
### Prerequisites
1. Setup [VPN](https://github.com/fleetdm/confidential/blob/main/vpn/README.md)
2. Configure [SSO](https://github.com/fleetdm/confidential/tree/main/infrastructure/sso#how-to-use-sso)
### Connecting
#### MySQL
Get the database host:
```shell
DB_HOST=$(aws rds describe-db-clusters --filter Name=db-cluster-id,Values=fleet-dogfood --query "DBClusters[0].Endpoint" --output=text)
```
Get the database user:
```shell
DB_USER=$(aws rds describe-db-clusters --filter Name=db-cluster-id,Values=fleet-dogfood --query "DBClusters[0].MasterUsername" --output=text)
```
Get the database password:
```shell
DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id fleet-dogfood-database-password --query "SecretString" --output=text)
```
Connect:
```shell
mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}"
```
#### Redis
Get the Redis Host:
```shell
REDIS_HOST=$(aws elasticache describe-replication-groups --replication-group-id fleetdm-redis --query "ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address" --output=text)
```
Connect:
```shell
redis-cli -h "${REDIS_HOST}"
```
## Foreign keys and locking
Among the first things you learn in database data modeling is: that if one table references a row in another, that reference should be a foreign key. This provides a lot of assurances and makes coding basic things much simpler.
However, this database feature doesnt come without a cost. The one to focus on here is locking, and heres a great summary of the issue: https://www.percona.com/blog/2006/12/12/innodb-locking-and-foreign-keys/
The TLDR is: understand very well how a table will be used. If we do bulk inserts/updates, InnoDB might lock more than you anticipate and cause issues. This is not an argument to not do bulk inserts/updates, but to be very careful when you add a foreign key.
In particular, host_id is a foreign key weve been skipping in all the new additional host data tables, which is not something that comes for free, as with that, [we have to keep the data consistent by hand with cleanups](https://github.com/fleetdm/fleet/blob/71a237042a9c39a45bc8f9c76465e5ff6039eba9/server/datastore/mysql/hosts.go#L444).
### In this section
### What have we learned so far?
- [How Fleet scales](#how-fleet-scales)
- [How to prevent most of this](#how-to-prevent-most-of-this)
- [Foreign keys and locking](#foreign-keys-and-locking)
- [Insert on duplicate update](#insert-on-duplicate-update)
- [Host extra data and JOINs](#host-extra-data-and-joins)
- [What DB tables matter more when thinking about performance?](#what-db-tables-matter-more-when-thinking-about-performance)
@ -82,8 +15,33 @@ In particular, host_id is a foreign key weve been skipping in all the new add
- [Counts and aggregated data](#counts-and-aggregated-data)
- [Caching data such as app config](#caching-data-such-as-app-config)
- [Redis SCAN](#redis-scan)
- [Fleet docs](#fleet-docs)
- [Community support](#community-support)
- [Connecting to Dogfood MySQL & Redis](#connecting-to-dogfood-mysql--redis)
### How Fleet scales
Nowadays, Fleet, as a Go server, scales horizontally very well. Its not very CPU or memory intensive. In terms of load in infrastructure, from highest to lowest are: MySQL, Redis, and Fleet.
In general, we should burn a bit of CPU or memory on the Fleet side if it allows us to reduce the load on MySQL or Redis.
In many cases, caching helps, but given that we are not doing load balancing based on host id (i.e., make sure that the same host ends up in the same Fleet server). This goes only so far. Caching host-specific data is not done because round-robin LB means all Fleet instances end up circling the total list of hosts.
### How to prevent most of this
The best way weve got so far to prevent any scaling issues is to load test things. **Every new feature must have its corresponding osquery-perf implementation as part of the PR, and it should be tested at a reasonable scale for the feature**.
Besides that, you should consider the answer(s) to the following question: how can I know that the feature Im working on is working and performing well enough? Add any logs, metrics, or anything that will help us debug and understand whats happening when things unavoidably go wrong or take longer than anticipated.
**HOWEVER** (and forgive this Captain Obvious comment): do NOT optimize before you KNOW you have to. Dont hesitate to take an extra day on your feature/bug work to load test things properly.
### Foreign keys and locking
Among the first things you learn in database data modeling is: that if one table references a row in another, that reference should be a foreign key. This provides a lot of assurances and makes coding basic things much simpler.
However, this database feature doesnt come without a cost. The one to focus on here is locking, and heres a great summary of the issue: https://www.percona.com/blog/2006/12/12/innodb-locking-and-foreign-keys/
The TLDR is: understand very well how a table will be used. If we do bulk inserts/updates, InnoDB might lock more than you anticipate and cause issues. This is not an argument to not do bulk inserts/updates, but to be very careful when you add a foreign key.
In particular, host_id is a foreign key weve been skipping in all the new additional host data tables, which is not something that comes for free, as with that, [we have to keep the data consistent by hand with cleanups](https://github.com/fleetdm/fleet/blob/71a237042a9c39a45bc8f9c76465e5ff6039eba9/server/datastore/mysql/hosts.go#L444).
### Insert on duplicate update
@ -173,7 +131,55 @@ Another place to cache things would be Redis. The improvement here is that all i
### Redis SCAN
Redis has solved many scaling problems in general, but its not devoid of scaling problems of its own. In particular, we learned that the SCAN command scans the whole key space before it does the filtering. This can be very slow, depending on the state of the system. If Redis is slow, a lot suffers from it.
Redis has solved many scaling problems in general, but its not devoid of scaling problems of its
own. In particular, we learned that the SCAN command scans the whole key space before it does the
filtering. This can be very slow, depending on the state of the system. If Redis is slow, a lot
suffers from it.
### Connecting to Dogfood MySQL & Redis
When investigating performance issues, it can be helpful to connect directly to the MySQL and Redis
instances to run queries and inspect data. Below are instructions for connecting to the Dogfood
MySQL and Redis instances.
#### Prerequisites
1. Setup [VPN](https://github.com/fleetdm/confidential/blob/main/vpn/README.md)
2. Configure [SSO](https://github.com/fleetdm/confidential/tree/main/infrastructure/sso#how-to-use-sso)
#### MySQL
Get the database host:
```shell
DB_HOST=$(aws rds describe-db-clusters --filter Name=db-cluster-id,Values=fleet-dogfood --query "DBClusters[0].Endpoint" --output=text)
```
Get the database user:
```shell
DB_USER=$(aws rds describe-db-clusters --filter Name=db-cluster-id,Values=fleet-dogfood --query "DBClusters[0].MasterUsername" --output=text)
```
Get the database password:
```shell
DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id fleet-dogfood-database-password --query "SecretString" --output=text)
```
Connect:
```shell
mysql -h"${DB_HOST}" -u"${DB_USER}" -p"${DB_PASSWORD}"
```
#### Redis
Get the Redis Host:
```shell
REDIS_HOST=$(aws elasticache describe-replication-groups --replication-group-id fleetdm-redis --query "ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address" --output=text)
```
Connect:
```shell
redis-cli -h "${REDIS_HOST}"
```
<meta name="maintainedBy" value="lukeheath">
<meta name="title" value="Scaling Fleet">

View File

@ -13,7 +13,7 @@ This handbook page details processes specific to working [with](#contact-us) and
## Contact us
- To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?labels=%3Aproduct&title=Product%20design%20request%C2%BB______________________&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in [#help-product-design](https://fleetdm.slack.com/archives/C02A8BRABB5).
- Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request.
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/-g-website--product-marketing--brand-6451748b4eb15200131d4bab/board) for this department, including pending tasks and the status of new requests.
- Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none) for this department, including pending tasks and the status of new requests.
## Responsibilities
The Product Design department is responsible for reviewing and collecting feedback from users, would-be users, and future users, prioritizing changes, designing the changes, and delivering these changes to the engineering team. Product Design prioritizes and shapes all changes involving functionality or usage, including the UI, REST API, command line, and webhooks.
@ -67,6 +67,7 @@ When starting a new draft:
> As drafting occurs, inevitably, the requirements will change. The main description of the issue should be the single source of truth for the problem to be solved and the required outcome. The product manager is responsible for keeping the main description of the issue up-to-date. Comments and other items can and should be kept in the issue for historical record-keeping.
### Ensure story drafting is complete
<!--TODO update responsibility to reflect reality (e.g. line 75 == "Bugs board"?)-->
Once a story has gone through design and is considered "Settled", it moves to the "Settled" column on the drafting board and assign to the Engineering Manager (EM).
Before assigning an EM to [estimate](https://fleetdm.com/handbook/engineering#sprint-ceremonies) a user story, the product designer ensures the product section of the user story [checklist](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story&projects=&template=story.md&title=) is complete.
@ -110,10 +111,10 @@ After aligning with the Director of Product Development on the outcomes, The Hea
- **De-prioritized**: The Head of Product Design should close the issue and, as the DRI, ensure all follow-up actions are finalized.
### Write a user story
Product Managers [write user stories](https://fleetdm.com/handbook/company/development-groups#writing-a-good-user-story) in the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board). The drafting board is shared by every [product group](https://fleetdm.com/handbook/company/development-groups).
Product Managers [write user stories](https://fleetdm.com/handbook/company/product-groups#writing-a-good-user-story) in the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board). The drafting board is shared by every [product group](https://fleetdm.com/handbook/company/development-groups).
### Draft a user story
Product Designers [draft user stories](https://fleetdm.com/handbook/company/development-groups#drafting) that have been prioritized by PMs. If the estimated user stories for a product group exceed [that group's capacity](https://fleetdm.com/handbook/company/product-groups#current-product-groups), all new design work for that group is paused, and the designer will contribute in other ways (documentation & handbook work, Figma maintenance, QA, etc.) until the PM deprioritizes estimated stories to make room, or until the next sprint begins. (If the designer has existing work-in-progress, they will continue to review and iterate on those designs and see the stories through to estimation.)
Product Designers [draft user stories](https://fleetdm.com/handbook/company/product-groups#drafting) that have been prioritized by PMs. If the estimated user stories for a product group exceed [that group's capacity](https://fleetdm.com/handbook/company/product-groups#current-product-groups), all new design work for that group is paused, and the designer will contribute in other ways (documentation & handbook work, Figma maintenance, QA, etc.) until the PM deprioritizes estimated stories to make room, or until the next sprint begins. (If the designer has existing work-in-progress, they will continue to review and iterate on those designs and see the stories through to estimation.)
If an issue's title or user story summary (_"as a…I want to…so that"_) does not match the intended change being discussed, the designer will move the issue to the "Needs clarity" column of the drafting board and assign the group product manager. The group product manager will revisit ASAP and edit the issue title and user story summary, then reassign the designer and move the issue back to the "Prioritized" column.
@ -180,14 +181,6 @@ available in Google Drive.
Some of the data is forwarded to [Datadog](https://us5.datadoghq.com/dashboard/7pb-63g-xty/usage-statistics?from_ts=1682952132131&to_ts=1685630532131&live=true) and is available to Fleeties.
### Restart Algolia manually
At least once every hour, an Algolia crawler reindexes the Fleet website's content. If an error occurs while the website is being indexed, Algolia will block our crawler and respond to requests with this message: `"This action cannot be executed on a blocked crawler"`.
When this happens, you'll need to manually start the crawler in the [Algolia crawler dashboard](https://crawler.algolia.com/admin/) to unblock it.
You can do this by logging into the crawler dashboard using the login saved in 1password and clicking the "Restart crawling" button on our crawler's "overview" page](https://crawler.algolia.com/admin/crawlers/497dd4fd-f8dd-4ffb-85c9-2a56b7fafe98/overview).
No further action is needed if the crawler successfully reindexes the Fleet website. If another error occurs while the crawler is running, take a screenshot of the error and add it to the GitHub issue created for the alert and @mention `eashaw` for help.
## Rituals
<rituals :rituals="rituals['handbook/product-design/product-design.rituals.yml']"></rituals>
@ -322,5 +315,8 @@ Please see [handbook/product-groups#customer-feature-requests](https://fleetdm.c
##### After the feature is accepted
Please see [handbook/product-groups#after-the-feature-is-accepted](https://fleetdm.com/handbook/product-groups#after-the-feature-is-accepted)
##### Restart Algolia manually
Please see [handbook/digital-experience#restart-algolia-manually](https://fleetdm.com/handbook/digital-experience#restart-algolia-manually)
<meta name="maintainedBy" value="noahtalerman">
<meta name="title" value="🦢 Product design">

View File

@ -93,8 +93,8 @@ module "main" {
fleet_config = {
image = local.geolite2_image
family = local.customer
cpu = 256
mem = 512
cpu = 1024
mem = 4096
autoscaling = {
min_capacity = 2
max_capacity = 5
@ -122,7 +122,7 @@ module "main" {
module.ses.fleet_extra_environment_variables,
local.extra_environment_variables,
module.geolite2.extra_environment_variables,
module.vuln-processing.extra_environment_variables
# module.vuln-processing.extra_environment_variables
)
extra_secrets = merge(module.mdm.extra_secrets, local.sentry_secrets)
# extra_load_balancers = [{
@ -451,17 +451,17 @@ module "geolite2" {
license_key = var.geolite2_license
}
module "vuln-processing" {
source = "github.com/fleetdm/fleet//terraform/addons/external-vuln-scans?ref=tf-mod-addon-external-vuln-scans-v2.0.0"
ecs_cluster = module.main.byo-vpc.byo-db.byo-ecs.service.cluster
execution_iam_role_arn = module.main.byo-vpc.byo-db.byo-ecs.execution_iam_role_arn
subnets = module.main.byo-vpc.byo-db.byo-ecs.service.network_configuration[0].subnets
security_groups = module.main.byo-vpc.byo-db.byo-ecs.service.network_configuration[0].security_groups
fleet_config = module.main.byo-vpc.byo-db.byo-ecs.fleet_config
task_role_arn = module.main.byo-vpc.byo-db.byo-ecs.iam_role_arn
awslogs_config = {
group = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.name
region = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.region
prefix = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.prefix
}
}
# module "vuln-processing" {
# source = "github.com/fleetdm/fleet//terraform/addons/external-vuln-scans?ref=tf-mod-addon-external-vuln-scans-v2.0.0"
# ecs_cluster = module.main.byo-vpc.byo-db.byo-ecs.service.cluster
# execution_iam_role_arn = module.main.byo-vpc.byo-db.byo-ecs.execution_iam_role_arn
# subnets = module.main.byo-vpc.byo-db.byo-ecs.service.network_configuration[0].subnets
# security_groups = module.main.byo-vpc.byo-db.byo-ecs.service.network_configuration[0].security_groups
# fleet_config = module.main.byo-vpc.byo-db.byo-ecs.fleet_config
# task_role_arn = module.main.byo-vpc.byo-db.byo-ecs.iam_role_arn
# awslogs_config = {
# group = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.name
# region = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.region
# prefix = module.main.byo-vpc.byo-db.byo-ecs.fleet_config.awslogs.prefix
# }
# }

View File

@ -28,7 +28,7 @@ module "aurora_mysql" { #tfsec:ignore:aws-rds-enable-performance-insights-encryp
name = "${local.name}-mysql"
engine = "aurora-mysql"
engine_version = "8.0.mysql_aurora.3.02.2"
engine_version = "8.0.mysql_aurora.3.03.3"
instance_class = var.db_instance_type
instances = {
@ -64,7 +64,7 @@ module "aurora_mysql" { #tfsec:ignore:aws-rds-enable-performance-insights-encryp
}
]
snapshot_identifier = "arn:aws:rds:us-east-2:917007347864:cluster-snapshot:cleaned"
snapshot_identifier = "arn:aws:rds:us-east-2:917007347864:cluster-snapshot:cleaned-8-0"
monitoring_interval = 60
iam_role_name = "${local.name}-rds"

View File

@ -111,6 +111,23 @@ docker images | grep 'BRANCH_NAME' | awk '{print $3}'
terraform apply -var tag=BRANCH_NAME -var loadtest_containers=XXX -target=aws_ecs_service.fleet -target=aws_ecs_task_definition.backend -target=aws_ecs_task_definition.migration -target=aws_s3_bucket_acl.osquery-results -target=aws_s3_bucket_acl.osquery-status -target=docker_registry_image.fleet
```
### Deploying code changes to osquery-perf
Following are the steps to deploy new code changes to osquery-perf (known as `loadtest` image in ECS) on a running loadtest environment.
> osquery-perf simulator in ECS doesn't keep state so you cannot change existing hosts to use new osquery-perf code.
> The following is to add new hosts with new/modified osquery-perf code. (This happens if during a load test
> the developer realizes there's bug in osquery-perf or if it's not simulating osquery properly.)
> You must push your code changes to the `$BRANCH_NAME`.
1. Bring all `loadtest` containers to `0` by running terraform apply with `loadtest_containers=0`.
1. Delete all existing hosts (by selecting all on the UI).
1. Delete all your local `loadtest` images, the image tags are of the form: `loadtest-$BRANCH_NAME-$TAG` (these are the `loadtest` images pushed to ECR). (Use `docker image list` to get their `IMAGE ID` and then run `docker rmi -f $ID`.)
1. Delete local images of the form `REPOSITORY=<none>` and `TAG=<none>` that were built recently (these are the builder images). (Use `docker image list` to get their `IMAGE ID` and then run `docker rmi -f $ID`.)
1. Log in to Amazon ECR (Elastic Container Registry) and delete the corresponding `loadtest` image.
1. By executing the `terraform apply` with `-loadtest_containers=N` it will trigger a rebuild of the `loadtest` image.
### Troubleshooting
#### Using a release tag instead of a branch

View File

@ -7,13 +7,64 @@ Orbit is the recommended agent for Fleet. But Orbit can be used with or without
# Documentation
- [Releasing Orbit](docs/Releasing-Orbit.md)
## How to build from source
To build orbit we use [goreleaser](https://goreleaser.com/).
For reference, here are the build configuration files:
- [Goreleaser github workflow](../.github/workflows/goreleaser-orbit.yml)
- Goreleaser configuration file for each platform:
- [goreleaser-linux.yml](./goreleaser-linux.yml)
- [goreleaser-macos.yml](./goreleaser-macos.yml)
- [goreleaser-windows.yml](./goreleaser-windows.yml)
Following are the commands to build in case you can't use goreleaser.
> IMPORTANT: We recommend you build orbit natively and not cross compile to avoid any build or runtime errors.
### macOS
```sh
CGO_ENABLED=1 \
CODESIGN_IDENTITY=$CODESIGN_IDENTITY \
ORBIT_VERSION=$VERSION \
ORBIT_BINARY_PATH=./orbit-macos \
go run ./orbit/tools/build/build.go
```
### Windows
```sh
CGO_ENABLED=0 \
GOOS=windows \
GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$VERSION \
-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Commit=$COMMIT \
-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Date=$DATE" \
-o ./orbit.exe ./orbit/cmd/orbit
```
### Linux
```sorbit/README.mdh
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Version=$VERSION \
-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Commit=$COMMIT \
-X github.com/fleetdm/fleet/v4/orbit/pkg/build.Date=$DATE" \
-o ./orbit-linux ./orbit/cmd/orbit
```
## Bugs
To report a bug or request a feature, [click here](https://github.com/fleetdm/fleet/issues).
#### Orbit Development
## Orbit Development
##### Run Orbit From Source
### Run Orbit From Source
To execute orbit from source directly, run the following command:
@ -37,11 +88,12 @@ go run github.com/fleetdm/fleet/v4/orbit/cmd/orbit \
-- --flagfile=flagfile.txt --verbose
```
##### Generate Installer Packages from Orbit Source
### Generate Installer Packages from Orbit Source
The `fleetctl package` command generates installers by fetching the targets/executables from a [TUF](https://theupdateframework.io/) repository.
To generate an installer that contains an Orbit built from source you need to setup a local TUF repository.
The following document explains how you can generate a TUF repository, and installers that use it [tools/tuf/test](../tools/tuf/test/README.md).
## FAQs
### How does Orbit compare with Kolide Launcher?
@ -71,7 +123,7 @@ Orbit uses a configurable update server. We expect that many folks will just use
## Community
#### Chat
### Chat
Please join us in the #fleet channel on [osquery Slack](https://fleetdm.com/slack).

24
orbit/TUF.md Normal file
View File

@ -0,0 +1,24 @@
<!-- DO NOT EDIT. This document is automatically generated by running `make fleetd-tuf`. -->
# tuf.fleetctl.com
Following are the currently deployed versions of fleetd components on the `stable` and `edge` channel.
## `stable`
| Component\OS | macOS | Linux | Windows |
|--------------|--------------|--------|---------|
| orbit | 1.21.0 | 1.21.0 | 1.21.0 |
| desktop | 1.21.0 | 1.21.0 | 1.21.0 |
| osqueryd | 5.11.0 | 5.11.0 | 5.11.0 |
| nudge | 1.1.10.81462 | - | - |
| swiftDialog | 2.1.0 | - | - |
## `edge`
| Component\OS | macOS | Linux | Windows |
|--------------|--------|--------|---------|
| orbit | 1.21.0 | 1.21.0 | 1.21.0 |
| desktop | 1.21.0 | 1.21.0 | 1.21.0 |
| osqueryd | 5.11.0 | 5.11.0 | 5.11.0 |
| nudge | - | - | - |
| swiftDialog | - | - | - |

View File

@ -1,5 +1,9 @@
project_name: orbit
#################################################################################################
# If this is updated make sure to update the "How to build from source" section in the README.md.
#################################################################################################
builds:
- id: orbit
dir: ./orbit/cmd/orbit/

View File

@ -1,5 +1,9 @@
project_name: orbit
#################################################################################################
# If this is updated make sure to update the "How to build from source" section in the README.md.
#################################################################################################
builds:
- id: orbit-macos
dir: ./orbit/cmd/orbit/

View File

@ -1,5 +1,9 @@
project_name: orbit
#################################################################################################
# If this is updated make sure to update the "How to build from source" section in the README.md.
#################################################################################################
builds:
- id: orbit
dir: ./orbit/cmd/orbit/

View File

@ -1,4 +1,5 @@
name: account_policy_data
description: Additional macOS user account data from the AccountPolicy section of [OpenDirectory](https://en.wikipedia.org/wiki/Apple_Open_Directory).
examples: >-
Query the creation date of user accounts. You could also query the date of the
last failed login attempt or password change.

View File

@ -577,6 +577,7 @@ SELECT
h.updated_at,
h.detail_updated_at,
h.node_key,
h.orbit_node_key,
h.hostname,
h.uuid,
h.platform,

View File

@ -847,6 +847,74 @@ func (ds *Datastore) whereFilterHostsByTeams(filter fleet.TeamFilter, hostKey st
return fmt.Sprintf("%s.team_id IN (%s)", hostKey, strings.Join(idStrs, ","))
}
// whereFilterGlobalOrTeamIDByTeams is the same as whereFilterHostsByTeams, it
// returns the appropriate condition to use in the WHERE clause to render only
// the appropriate teams, but is to be used when the team_id column uses "0" to
// mean "all teams including no team". This is the case e.g. for
// software_title_host_counts.
//
// filter provides the filtering parameters that should be used.
// filterTableAlias is the name/alias of the table to use in generating the
// SQL.
func (ds *Datastore) whereFilterGlobalOrTeamIDByTeams(filter fleet.TeamFilter, filterTableAlias string) string {
if filter.User == nil {
// This is likely unintentional, however we would like to return no
// results rather than panicking or returning some other error. At least
// log.
level.Info(ds.logger).Log("err", "team filter missing user")
return "FALSE"
}
defaultAllowClause := fmt.Sprintf("%s.team_id = 0", filterTableAlias)
if filter.TeamID != nil {
defaultAllowClause = fmt.Sprintf("%s.team_id = %d", filterTableAlias, *filter.TeamID)
}
if filter.User.GlobalRole != nil {
switch *filter.User.GlobalRole {
case fleet.RoleAdmin, fleet.RoleMaintainer, fleet.RoleObserverPlus:
return defaultAllowClause
case fleet.RoleObserver:
if filter.IncludeObserver {
return defaultAllowClause
}
return "FALSE"
default:
// Fall through to specific teams
}
}
// Collect matching teams
var idStrs []string
var teamIDSeen bool
for _, team := range filter.User.Teams {
if team.Role == fleet.RoleAdmin ||
team.Role == fleet.RoleMaintainer ||
team.Role == fleet.RoleObserverPlus ||
(team.Role == fleet.RoleObserver && filter.IncludeObserver) {
idStrs = append(idStrs, strconv.Itoa(int(team.ID)))
if filter.TeamID != nil && *filter.TeamID == team.ID {
teamIDSeen = true
}
}
}
if len(idStrs) == 0 {
// User has no global role and no teams allowed by includeObserver.
return "FALSE"
}
if filter.TeamID != nil {
if teamIDSeen {
// all good, this user has the right to see the requested team
return defaultAllowClause
}
return "FALSE"
}
return fmt.Sprintf("%s.team_id IN (%s)", filterTableAlias, strings.Join(idStrs, ","))
}
// whereFilterTeams returns the appropriate condition to use in the WHERE
// clause to render only the appropriate teams.
//

View File

@ -1032,3 +1032,200 @@ func Test_buildWildcardMatchPhrase(t *testing.T) {
})
}
}
func TestWhereFilterGlobalOrTeamIDByTeams(t *testing.T) {
t.Parallel()
testCases := []struct {
filter fleet.TeamFilter
expected string
}{
// No teams or global role
{
filter: fleet.TeamFilter{
User: &fleet.User{},
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{Teams: []fleet.UserTeam{}},
},
expected: "FALSE",
},
// Global role
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
},
expected: "hosts.team_id = 0",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
},
expected: "hosts.team_id = 0",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
IncludeObserver: true,
},
expected: "hosts.team_id = 0",
},
// Team roles
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
},
},
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
},
},
IncludeObserver: true,
},
expected: "hosts.team_id IN (1)",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 2}},
},
},
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
},
},
},
expected: "hosts.team_id IN (2)",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
},
},
IncludeObserver: true,
},
expected: "hosts.team_id IN (1,2)",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
// Invalid role should be ignored
{Role: "bad", Team: fleet.Team{ID: 37}},
},
},
},
expected: "hosts.team_id IN (2)",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
{Role: fleet.RoleAdmin, Team: fleet.Team{ID: 3}},
// Invalid role should be ignored
},
},
},
expected: "hosts.team_id IN (2,3)",
},
{
filter: fleet.TeamFilter{
TeamID: ptr.Uint(1),
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
IncludeObserver: true,
TeamID: ptr.Uint(1),
},
expected: "hosts.team_id = 1",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
IncludeObserver: false,
TeamID: ptr.Uint(1),
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
IncludeObserver: false,
TeamID: ptr.Uint(1),
},
expected: "hosts.team_id = 1",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
},
},
TeamID: ptr.Uint(3),
},
expected: "FALSE",
},
{
filter: fleet.TeamFilter{
User: &fleet.User{
Teams: []fleet.UserTeam{
{Role: fleet.RoleObserver, Team: fleet.Team{ID: 1}},
{Role: fleet.RoleMaintainer, Team: fleet.Team{ID: 2}},
},
},
TeamID: ptr.Uint(2),
},
expected: "hosts.team_id = 2",
},
}
for _, tt := range testCases {
tt := tt
t.Run("", func(t *testing.T) {
t.Parallel()
ds := &Datastore{logger: log.NewNopLogger()}
sql := ds.whereFilterGlobalOrTeamIDByTeams(tt.filter, "hosts")
assert.Equal(t, tt.expected, sql)
})
}
}

View File

@ -1124,7 +1124,7 @@ func (ds *Datastore) DeleteOutOfDateVulnerabilities(ctx context.Context, source
return nil
}
func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool) (*fleet.Software, error) {
func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool, tmFilter *fleet.TeamFilter) (*fleet.Software, error) {
q := dialect.From(goqu.I("software").As("s")).
Select(
"s.id",
@ -1151,6 +1151,13 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, teamID *uint, in
goqu.On(goqu.I("s.id").Eq(goqu.I("scv.software_id"))),
)
if tmFilter != nil {
q = q.LeftJoin(
goqu.I("software_host_counts").As("shc"),
goqu.On(goqu.I("s.id").Eq(goqu.I("shc.software_id"))),
)
}
if includeCVEScores {
q = q.
LeftJoin(
@ -1180,6 +1187,11 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, teamID *uint, in
)
}
// filter by teams
if tmFilter != nil {
q = q.Where(goqu.L(ds.whereFilterGlobalOrTeamIDByTeams(*tmFilter, "shc")))
}
sql, args, err := q.ToSQL()
if err != nil {
return nil, err

View File

@ -102,7 +102,7 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
host1Software := getHostSoftware(host1)
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, nil, false)
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, nil, false, nil)
require.NoError(t, err)
require.NotNil(t, soft1ByID)
assert.Equal(t, host1Software[0], *soft1ByID)
@ -293,7 +293,7 @@ func testSoftwareLoadVulnerabilities(t *testing.T, ds *Datastore) {
}
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
softByID, err := ds.SoftwareByID(context.Background(), host.HostSoftware.Software[0].ID, nil, false)
softByID, err := ds.SoftwareByID(context.Background(), host.HostSoftware.Software[0].ID, nil, false, nil)
require.NoError(t, err)
require.NotNil(t, softByID)
require.Len(t, softByID.Vulnerabilities, 2)
@ -1052,7 +1052,7 @@ func testSoftwareSyncHostsSoftware(t *testing.T, ds *Datastore) {
cmpNameVersionCount(want, team1Counts)
checkTableTotalCount(3)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
nilSoftware, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false)
nilSoftware, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false, nil)
assert.Nil(t, nilSoftware)
assert.ErrorIs(t, err, sql.ErrNoRows)
@ -1078,7 +1078,7 @@ func testSoftwareSyncHostsSoftware(t *testing.T, ds *Datastore) {
// composite pk (software_id, team_id), so we expect more rows
checkTableTotalCount(8)
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false)
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false, nil)
require.NoError(t, err)
software1[0].ID = host1.HostSoftware.Software[0].ID
assert.Equal(t, software1[0], *soft1ByID)
@ -2010,7 +2010,7 @@ func testSoftwareByIDNoDuplicatedVulns(t *testing.T, ds *Datastore) {
}
for _, s := range hostA.Software {
result, err := ds.SoftwareByID(ctx, s.ID, nil, true)
result, err := ds.SoftwareByID(ctx, s.ID, nil, true, nil)
require.NoError(t, err)
require.Len(t, result.Vulnerabilities, 1)
}
@ -2104,7 +2104,7 @@ func testSoftwareByIDIncludesCVEPublishedDate(t *testing.T, ds *Datastore) {
for _, teamID := range []*uint{nil, &team1.ID} {
// Test that scores are not included if includeCVEScores = false
withoutScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, false)
withoutScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, false, nil)
require.NoError(t, err)
if tC.hasVuln {
require.Len(t, withoutScores.Vulnerabilities, 1)
@ -2117,7 +2117,7 @@ func testSoftwareByIDIncludesCVEPublishedDate(t *testing.T, ds *Datastore) {
require.Empty(t, withoutScores.Vulnerabilities)
}
withScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, true)
withScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, true, nil)
require.NoError(t, err)
if tC.hasVuln {
require.Len(t, withScores.Vulnerabilities, 1)
@ -2317,7 +2317,7 @@ func testDeleteOutOfDateVulnerabilities(t *testing.T, ds *Datastore) {
err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, 2*time.Hour)
require.NoError(t, err)
storedSoftware, err := ds.SoftwareByID(ctx, host.Software[0].ID, nil, false)
storedSoftware, err := ds.SoftwareByID(ctx, host.Software[0].ID, nil, false, nil)
require.NoError(t, err)
require.Equal(t, 1, len(storedSoftware.Vulnerabilities))
require.Equal(t, "CVE-2023-001", storedSoftware.Vulnerabilities[0].CVE)
@ -2368,7 +2368,7 @@ func testDeleteSoftwareCPEs(t *testing.T, ds *Datastore) {
require.NoError(t, err)
test.ElementsMatchSkipID(t, cpes[1:], storedCPEs)
storedSoftware, err := ds.SoftwareByID(ctx, cpes[0].SoftwareID, nil, false)
storedSoftware, err := ds.SoftwareByID(ctx, cpes[0].SoftwareID, nil, false, nil)
require.NoError(t, err)
require.Empty(t, storedSoftware.GenerateCPE)
})
@ -2892,6 +2892,7 @@ func testUpdateHostSoftwareDeadlock(t *testing.T, ds *Datastore) {
err := g.Wait()
require.NoError(t, err)
}
func testVerifySoftwareChecksum(t *testing.T, ds *Datastore) {
ctx := context.Background()
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())

View File

@ -12,21 +12,26 @@ import (
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint) (*fleet.SoftwareTitle, error) {
const selectSoftwareTitleStmt = `
func (ds *Datastore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
selectSoftwareTitleStmt := fmt.Sprintf(`
SELECT
st.id,
st.name,
st.source,
st.browser,
sthc.hosts_count,
sthc.updated_at as counts_updated_at
SUM(sthc.hosts_count) as hosts_count,
MAX(sthc.updated_at) as counts_updated_at
FROM software_titles st
JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id
WHERE st.id = ?
WHERE st.id = ? AND %s
AND sthc.team_id = ?
AND sthc.hosts_count > 0
`
GROUP BY
st.id,
st.name,
st.source,
st.browser
`, ds.whereFilterGlobalOrTeamIDByTeams(tmFilter, "sthc"))
teamIDVal := uint(0)
if teamID != nil {
teamIDVal = *teamID
@ -39,7 +44,7 @@ AND sthc.hosts_count > 0
return nil, ctxerr.Wrap(ctx, err, "get software title")
}
selectSoftwareVersionsStmt, args, err := selectSoftwareVersionsSQL([]uint{id}, teamIDVal, true)
selectSoftwareVersionsStmt, args, err := ds.selectSoftwareVersionsSQL([]uint{id}, tmFilter, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building versions statement")
}
@ -55,6 +60,7 @@ AND sthc.hosts_count > 0
func (ds *Datastore) ListSoftwareTitles(
ctx context.Context,
opt fleet.SoftwareTitleListOptions,
tmFilter fleet.TeamFilter,
) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
if opt.ListOptions.After != "" {
return nil, 0, nil, fleet.NewInvalidArgumentError("after", "not supported for software titles")
@ -109,11 +115,11 @@ func (ds *Datastore) ListSoftwareTitles(
// the application logic. This is because we need to support MySQL 5.7
// and there's no good way to do an aggregation that builds a structure
// (like a JSON) object for nested arrays.
var teamID uint
if opt.TeamID != nil {
teamID = *opt.TeamID
}
getVersionsStmt, args, err := selectSoftwareVersionsSQL(titleIDs, teamID, false)
getVersionsStmt, args, err := ds.selectSoftwareVersionsSQL(
titleIDs,
tmFilter,
false,
)
if err != nil {
return nil, 0, nil, ctxerr.Wrap(ctx, err, "build get versions stmt")
}
@ -223,7 +229,7 @@ GROUP BY st.id`
return stmt, args
}
func selectSoftwareVersionsSQL(titleIDs []uint, teamID uint, withCounts bool) (string, []any, error) {
func (ds *Datastore) selectSoftwareVersionsSQL(titleIDs []uint, tmFilter fleet.TeamFilter, withCounts bool) (string, []any, error) {
selectVersionsStmt := `
SELECT
s.title_id,
@ -234,7 +240,7 @@ FROM software s
LEFT JOIN software_host_counts shc ON shc.software_id = s.id
LEFT JOIN software_cve scve ON shc.software_id = scve.software_id
WHERE s.title_id IN (?)
AND shc.team_id = ?
AND %s
AND shc.hosts_count > 0
GROUP BY s.id`
@ -243,10 +249,11 @@ GROUP BY s.id`
extraSelect = "MAX(shc.hosts_count) AS hosts_count,"
}
selectVersionsStmt = fmt.Sprintf(selectVersionsStmt, extraSelect)
selectVersionsStmt, args, err := sqlx.In(selectVersionsStmt, titleIDs, teamID)
selectVersionsStmt = fmt.Sprintf(selectVersionsStmt, extraSelect, ds.whereFilterGlobalOrTeamIDByTeams(tmFilter, "shc"))
selectVersionsStmt, args, err := sqlx.In(selectVersionsStmt, titleIDs)
if err != nil {
return "", nil, fmt.Errorf("bulding sqlx.In query: %w", err)
return "", nil, fmt.Errorf("building sqlx.In query: %w", err)
}
return selectVersionsStmt, args, nil
}

View File

@ -2,6 +2,7 @@ package mysql
import (
"context"
"sort"
"testing"
"time"
@ -20,6 +21,7 @@ func TestSoftwareTitles(t *testing.T) {
}{
{"SyncHostsSoftwareTitles", testSoftwareSyncHostsSoftwareTitles},
{"OrderSoftwareTitles", testOrderSoftwareTitles},
{"TeamFilterSoftwareTitles", testTeamFilterSoftwareTitles},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -288,7 +290,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderDescending,
}})
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 7)
require.Equal(t, "bar", titles[0].Name)
@ -312,7 +314,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "hosts_count",
OrderDirection: fleet.OrderAscending,
}})
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 7)
require.Equal(t, "bar", titles[0].Name)
@ -336,7 +338,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderAscending,
}})
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 7)
require.Equal(t, "bar", titles[0].Name)
@ -360,7 +362,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{
OrderKey: "name",
OrderDirection: fleet.OrderDescending,
}})
}}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, 7)
require.Equal(t, "foo", titles[0].Name)
@ -382,10 +384,85 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
}
func listSoftwareTitlesCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareTitleListOptions, returnSorted bool) []fleet.SoftwareTitle {
titles, count, _, err := ds.ListSoftwareTitles(context.Background(), opts)
titles, count, _, err := ds.ListSoftwareTitles(context.Background(), opts, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
require.NoError(t, err)
require.Len(t, titles, expectedListCount)
require.NoError(t, err)
require.Equal(t, expectedFullCount, count)
return titles
}
func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
require.NoError(t, err)
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
require.NoError(t, ds.AddHostsToTeam(ctx, &team1.ID, []uint{host1.ID}))
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
require.NoError(t, ds.AddHostsToTeam(ctx, &team2.ID, []uint{host2.ID}))
user1, err := ds.NewUser(ctx, &fleet.User{Name: "user1", Password: []byte("test"), Email: "test1@email.com", GlobalRole: ptr.String(fleet.RoleAdmin)})
require.NoError(t, err)
user2, err := ds.NewUser(ctx, &fleet.User{Name: "user2", Password: []byte("test"), Email: "test2@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleAdmin}}})
require.NoError(t, err)
user3, err := ds.NewUser(ctx, &fleet.User{Name: "user3", Password: []byte("test"), Email: "test3@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleAdmin}}})
require.NoError(t, err)
software1 := []fleet.Software{
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
}
software2 := []fleet.Software{
{Name: "foo", Version: "0.0.4", Source: "chrome_extensions"},
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
}
_, err = ds.UpdateHostSoftware(ctx, host1.ID, software1)
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(ctx, host2.ID, software2)
require.NoError(t, err)
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
// Testing the global user
titles, count, _, err := ds.ListSoftwareTitles(context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}}, fleet.TeamFilter{
User: user1,
IncludeObserver: true,
})
sortTitlesByName(titles)
require.NoError(t, err)
require.Len(t, titles, 2)
require.Equal(t, 2, count)
require.Equal(t, uint(1), titles[0].VersionsCount)
require.Equal(t, uint(2), titles[1].VersionsCount)
// Testing the team 1 user
titles, count, _, err = ds.ListSoftwareTitles(context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}, TeamID: &team1.ID}, fleet.TeamFilter{
User: user2,
IncludeObserver: true,
})
require.NoError(t, err)
require.Len(t, titles, 1)
require.Equal(t, 1, count)
require.Equal(t, uint(1), titles[0].VersionsCount)
// Testing the team 2 user
titles, count, _, err = ds.ListSoftwareTitles(context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}, TeamID: &team2.ID}, fleet.TeamFilter{
User: user3,
IncludeObserver: true,
})
require.NoError(t, err)
require.Len(t, titles, 2)
require.Equal(t, 2, count)
require.Equal(t, uint(1), titles[0].VersionsCount)
require.Equal(t, uint(1), titles[1].VersionsCount)
}
func sortTitlesByName(titles []fleet.SoftwareTitle) {
sort.Slice(titles, func(i, j int) bool { return titles[i].Name < titles[j].Name })
}

View File

@ -462,8 +462,8 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// Software Titles
ListSoftwareTitles(ctx context.Context, opt SoftwareTitleListOptions) ([]SoftwareTitle, int, *PaginationMetadata, error)
SoftwareTitleByID(ctx context.Context, id uint, teamID *uint) (*SoftwareTitle, error)
ListSoftwareTitles(ctx context.Context, opt SoftwareTitleListOptions, tmFilter TeamFilter) ([]SoftwareTitle, int, *PaginationMetadata, error)
SoftwareTitleByID(ctx context.Context, id uint, teamID *uint, tmFilter TeamFilter) (*SoftwareTitle, error)
///////////////////////////////////////////////////////////////////////////////
// SoftwareStore
@ -486,7 +486,7 @@ type Datastore interface {
// case it will return true) or if a matching record already exists it will update its
// updated_at timestamp (in which case it will return false).
InsertSoftwareVulnerability(ctx context.Context, vuln SoftwareVulnerability, source VulnerabilitySource) (bool, error)
SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool) (*Software, error)
SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool, tmFilter *TeamFilter) (*Software, error)
// ListSoftwareByHostIDShort lists software by host ID, but does not include CPEs or vulnerabilites.
// It is meant to be used when only minimal software fields are required eg when updating host software.
ListSoftwareByHostIDShort(ctx context.Context, hostID uint) ([]Software, error)

View File

@ -62,7 +62,7 @@ func NewNudgeConfig(macOSUpdates MacOSUpdates) (*NudgeConfig, error) {
RequiredMinimumOSVersion: macOSUpdates.MinimumVersion.Value,
AboutUpdateURLs: []nudgeAboutUpdateURLs{{
Language: "en",
AboutUpdateURL: "https://fleetdm.com/docs/using-fleet/mdm-macos-updates",
AboutUpdateURL: "https://fleetdm.com/learn-more-about/os-updates",
}},
}},
UserInterface: nudgeUserInterface{

View File

@ -352,9 +352,9 @@ type DeleteIntegrationsFromTeamsFunc func(ctx context.Context, deletedIntgs flee
type TeamExistsFunc func(ctx context.Context, teamID uint) (bool, error)
type ListSoftwareTitlesFunc func(ctx context.Context, opt fleet.SoftwareTitleListOptions) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error)
type ListSoftwareTitlesFunc func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error)
type SoftwareTitleByIDFunc func(ctx context.Context, id uint, teamID *uint) (*fleet.SoftwareTitle, error)
type SoftwareTitleByIDFunc func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error)
type ListSoftwareForVulnDetectionFunc func(ctx context.Context, hostID uint) ([]fleet.Software, error)
@ -372,7 +372,7 @@ type ListSoftwareCPEsFunc func(ctx context.Context) ([]fleet.SoftwareCPE, error)
type InsertSoftwareVulnerabilityFunc func(ctx context.Context, vuln fleet.SoftwareVulnerability, source fleet.VulnerabilitySource) (bool, error)
type SoftwareByIDFunc func(ctx context.Context, id uint, teamID *uint, includeCVEScores bool) (*fleet.Software, error)
type SoftwareByIDFunc func(ctx context.Context, id uint, teamID *uint, includeCVEScores bool, tmFilter *fleet.TeamFilter) (*fleet.Software, error)
type ListSoftwareByHostIDShortFunc func(ctx context.Context, hostID uint) ([]fleet.Software, error)
@ -3231,18 +3231,18 @@ func (s *DataStore) TeamExists(ctx context.Context, teamID uint) (bool, error) {
return s.TeamExistsFunc(ctx, teamID)
}
func (s *DataStore) ListSoftwareTitles(ctx context.Context, opt fleet.SoftwareTitleListOptions) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
func (s *DataStore) ListSoftwareTitles(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
s.mu.Lock()
s.ListSoftwareTitlesFuncInvoked = true
s.mu.Unlock()
return s.ListSoftwareTitlesFunc(ctx, opt)
return s.ListSoftwareTitlesFunc(ctx, opt, tmFilter)
}
func (s *DataStore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint) (*fleet.SoftwareTitle, error) {
func (s *DataStore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
s.mu.Lock()
s.SoftwareTitleByIDFuncInvoked = true
s.mu.Unlock()
return s.SoftwareTitleByIDFunc(ctx, id, teamID)
return s.SoftwareTitleByIDFunc(ctx, id, teamID, tmFilter)
}
func (s *DataStore) ListSoftwareForVulnDetection(ctx context.Context, hostID uint) ([]fleet.Software, error) {
@ -3301,11 +3301,11 @@ func (s *DataStore) InsertSoftwareVulnerability(ctx context.Context, vuln fleet.
return s.InsertSoftwareVulnerabilityFunc(ctx, vuln, source)
}
func (s *DataStore) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool) (*fleet.Software, error) {
func (s *DataStore) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool, tmFilter *fleet.TeamFilter) (*fleet.Software, error) {
s.mu.Lock()
s.SoftwareByIDFuncInvoked = true
s.mu.Unlock()
return s.SoftwareByIDFunc(ctx, id, teamID, includeCVEScores)
return s.SoftwareByIDFunc(ctx, id, teamID, includeCVEScores, tmFilter)
}
func (s *DataStore) ListSoftwareByHostIDShort(ctx context.Context, hostID uint) ([]fleet.Software, error) {

View File

@ -4910,6 +4910,25 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
// attempt to create an async script execution request, succeeds because script is added to queue.
s.Do("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusAccepted)
// attempt to run a script on a plain osquery host
plainOsqueryHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-time.Minute),
OsqueryHostID: ptr.String("plain-osquery-host"),
NodeKey: ptr.String("plain-osquery-host"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%s.local", "plain-osquery-host"),
HardwareSerial: uuid.New().String(),
Platform: "linux",
})
require.NoError(t, err)
res = s.Do("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: plainOsqueryHost.ID, ScriptContents: "echo"}, http.StatusUnprocessableEntity)
require.Contains(t, extractServerErrorText(res.Body), fleet.RunScriptDisabledErrMsg)
res = s.Do("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: plainOsqueryHost.ID, ScriptContents: "echo"}, http.StatusUnprocessableEntity)
require.Contains(t, extractServerErrorText(res.Body), fleet.RunScriptDisabledErrMsg)
}
func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() {
@ -5071,6 +5090,25 @@ func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() {
require.NoError(t, err)
s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptID: &script.ID}, http.StatusConflict, &runResp)
// attempt to run a script on a plain osquery host
plainOsqueryHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-time.Minute),
OsqueryHostID: ptr.String("plain-osquery-host-2"),
NodeKey: ptr.String("plain-osquery-host-2"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%s.local", "plain-osquery-host-2"),
HardwareSerial: uuid.New().String(),
Platform: "linux",
})
require.NoError(t, err)
res = s.Do("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: plainOsqueryHost.ID, ScriptID: &script.ID}, http.StatusUnprocessableEntity)
require.Contains(t, extractServerErrorText(res.Body), fleet.RunScriptDisabledErrMsg)
res = s.Do("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: plainOsqueryHost.ID, ScriptID: &script.ID}, http.StatusUnprocessableEntity)
require.Contains(t, extractServerErrorText(res.Body), fleet.RunScriptDisabledErrMsg)
}
func (s *integrationEnterpriseTestSuite) TestEnqueueSameScriptTwice() {
@ -6582,8 +6620,8 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
{
Name: "foo",
Source: "homebrew",
VersionsCount: 1, // NOTE: this value is 1 because the team has only 1 matching host
HostsCount: 1, // NOTE: this value is 1 because the team has only 1 matching host
VersionsCount: 1, // NOTE: this value is 1 because the team has only 1 matching host in the team
HostsCount: 1, // NOTE: this value is 1 because the team has only 1 matching host in the team
Versions: []fleet.SoftwareVersion{
{Version: "0.0.1", Vulnerabilities: nil}, // NOTE: this only includes versions present in the team
},
@ -6942,3 +6980,359 @@ func (s *integrationEnterpriseTestSuite) createHosts(t *testing.T, platforms ...
}
return hosts
}
func (s *integrationEnterpriseTestSuite) TestSoftwareAuth() {
t := s.T()
ctx := context.Background()
// create two hosts, one belongs to team1 and one has no team
host, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name()),
NodeKey: ptr.String(t.Name()),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "darwin",
})
require.NoError(t, err)
tmHost, err := s.ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + "tm"),
NodeKey: ptr.String(t.Name() + "tm"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()+"tm"),
Platform: "linux",
})
require.NoError(t, err)
// Create two teams, team1 and team2.
team1, err := s.ds.NewTeam(ctx, &fleet.Team{
ID: 42,
Name: "team1",
Description: "desc team1",
})
require.NoError(t, err)
require.NoError(t, s.ds.AddHostsToTeam(ctx, &team1.ID, []uint{tmHost.ID}))
team2, err := s.ds.NewTeam(ctx, &fleet.Team{
ID: 43,
Name: "team2",
Description: "desc team2",
})
require.NoError(t, err)
allSoftware := []fleet.Software{
{Name: "foo", Version: "0.0.1", Source: "homebrew"},
{Name: "foo", Version: "0.0.3", Source: "homebrew"},
{Name: "bar", Version: "0.0.4", Source: "apps"},
}
// add all the software entries to the "no team host"
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, allSoftware)
require.NoError(t, err)
require.NoError(t, s.ds.LoadHostSoftware(ctx, host, false))
// add only one version of "foo" to the team host
_, err = s.ds.UpdateHostSoftware(ctx, tmHost.ID, []fleet.Software{allSoftware[0]})
require.NoError(t, err)
require.NoError(t, s.ds.LoadHostSoftware(ctx, tmHost, false))
// calculate hosts counts
hostsCountTs := time.Now().UTC()
require.NoError(t, s.ds.SyncHostsSoftware(ctx, hostsCountTs))
require.NoError(t, s.ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, s.ds.SyncHostsSoftwareTitles(ctx, hostsCountTs))
// add variations of user roles to different teams
extraTestUsers := make(map[string]fleet.User)
for k, u := range map[string]struct {
Email string
GlobalRole *string
Teams *[]fleet.UserTeam
}{
"team-1-admin": {
Email: "team-1-admin@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team1,
Role: fleet.RoleAdmin,
}}),
},
"team-1-maintainer": {
Email: "team-1-maintainer@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team1,
Role: fleet.RoleMaintainer,
}}),
},
"team-1-observer": {
Email: "team-1-observer@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team1,
Role: fleet.RoleObserver,
}}),
},
"team-2-admin": {
Email: "team-2-admin@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team2,
Role: fleet.RoleAdmin,
}}),
},
"team-2-maintainer": {
Email: "team-2-maintainer@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team2,
Role: fleet.RoleMaintainer,
}}),
},
"team-2-observer": {
Email: "team-2-observer@example.com",
Teams: &([]fleet.UserTeam{{
Team: *team2,
Role: fleet.RoleObserver,
}}),
},
} {
uu := u
cur := createUserResponse{}
s.DoJSON("POST", "/api/latest/fleet/users/admin", createUserRequest{
UserPayload: fleet.UserPayload{
Email: &uu.Email,
Password: &test.GoodPassword,
Name: &uu.Email,
Teams: uu.Teams,
AdminForcedPasswordReset: ptr.Bool(false),
},
}, http.StatusOK, &cur)
extraTestUsers[k] = *cur.User
}
// List all software titles with an admin
var listSoftwareTitlesResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &listSoftwareTitlesResp)
var softwareFoo, softwareBar *fleet.SoftwareTitle
for _, s := range listSoftwareTitlesResp.SoftwareTitles {
s := s
switch s.Name {
case "foo":
softwareFoo = &s
case "bar":
softwareBar = &s
}
}
require.NotNil(t, softwareFoo)
require.NotNil(t, softwareBar)
var teamFooVersion *fleet.SoftwareVersion
for _, sv := range softwareFoo.Versions {
sv := sv
if sv.Version == "0.0.1" {
teamFooVersion = &sv
}
}
require.NotNil(t, teamFooVersion)
for _, tc := range []struct {
name string
user fleet.User
shouldFailGlobalRead bool
shouldFailTeamRead bool
}{
{
name: "global-admin",
user: s.users["admin1@example.com"],
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "global-maintainer",
user: s.users["user1@example.com"],
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "global-observer",
user: s.users["user2@example.com"],
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "team-admin-belongs-to-team",
user: extraTestUsers["team-1-admin"],
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-maintainer-belongs-to-team",
user: extraTestUsers["team-1-maintainer"],
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-observer-belongs-to-team",
user: extraTestUsers["team-1-observer"],
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-admin-does-not-belong-to-team",
user: extraTestUsers["team-2-admin"],
shouldFailGlobalRead: true,
shouldFailTeamRead: true,
},
{
name: "team-maintainer-does-not-belong-to-team",
user: extraTestUsers["team-2-maintainer"],
shouldFailGlobalRead: true,
shouldFailTeamRead: true,
},
{
name: "team-observer-does-not-belong-to-team",
user: extraTestUsers["team-2-observer"],
shouldFailGlobalRead: true,
shouldFailTeamRead: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
// to make the request as the user
s.token = s.getTestToken(tc.user.Email, test.GoodPassword)
if tc.shouldFailGlobalRead {
// List all software titles
var listSoftwareTitlesResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusForbidden, &listSoftwareTitlesResp)
// List all software versions
var resp listSoftwareVersionsResponse
s.DoJSON("GET", "/api/latest/fleet/software/versions", listSoftwareTitlesRequest{}, http.StatusForbidden, &resp)
// Get a global software title
var getSoftwareTitleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", softwareBar.ID), getSoftwareTitleRequest{}, http.StatusForbidden, &getSoftwareTitleResp)
// Get a global software version
var getSoftwareResp getSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/versions/%d", softwareBar.Versions[0].ID), getSoftwareRequest{}, http.StatusForbidden, &getSoftwareResp)
// Get a global software vesion using the deprecated endpoint
getSoftwareResp = getSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/%d", softwareBar.Versions[0].ID), getSoftwareRequest{}, http.StatusForbidden, &getSoftwareResp)
// Get a count of software vesions using the deprecated endpoint
var countSoftwareResp countSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software/count", getSoftwareRequest{}, http.StatusForbidden, &countSoftwareResp)
// List all software versions using the deprecated endpoint
var softwareListResp listSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software", listSoftwareRequest{}, http.StatusForbidden, &softwareListResp)
} else {
// List all software titles
var listSoftwareTitlesResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &listSoftwareTitlesResp)
require.Equal(t, 2, listSoftwareTitlesResp.Count)
require.NotEmpty(t, listSoftwareTitlesResp.CountsUpdatedAt)
// List all software versions
var resp listSoftwareVersionsResponse
s.DoJSON("GET", "/api/latest/fleet/software/versions", listSoftwareRequest{}, http.StatusOK, &resp)
require.Equal(t, 3, resp.Count)
require.NotEmpty(t, resp.CountsUpdatedAt)
// Get a global software title
var getSoftwareTitleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", softwareBar.ID), getSoftwareTitleRequest{}, http.StatusOK, &getSoftwareTitleResp)
// Get a global software version
var getSoftwareResp getSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/versions/%d", softwareBar.Versions[0].ID), getSoftwareRequest{}, http.StatusOK, &getSoftwareResp)
// Get a global software vesion using the deprecated endpoint
getSoftwareResp = getSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/%d", softwareBar.Versions[0].ID), getSoftwareRequest{}, http.StatusOK, &getSoftwareResp)
// Get a global count of software vesions using the deprecated endpoint
var countSoftwareResp countSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software/count", countSoftwareRequest{}, http.StatusOK, &countSoftwareResp)
require.Equal(t, 3, countSoftwareResp.Count)
// List all software versions using the deprecated endpoint
var softwareListResp listSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software", listSoftwareRequest{}, http.StatusOK, &softwareListResp)
require.Equal(t, countSoftwareResp.Count, 3)
}
if tc.shouldFailTeamRead {
// List all software titles for a team
var listSoftwareTitlesResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{SoftwareTitleListOptions: fleet.SoftwareTitleListOptions{TeamID: &team1.ID}}, http.StatusForbidden, &listSoftwareTitlesResp)
// List software versions for a team.
var resp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/versions", listSoftwareRequest{SoftwareListOptions: fleet.SoftwareListOptions{TeamID: &team1.ID}}, http.StatusForbidden, &resp)
// Get a team software title
var getSoftwareTitleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", softwareFoo.ID), getSoftwareTitleRequest{}, http.StatusForbidden, &getSoftwareTitleResp)
// Get a team software version
var getSoftwareResp getSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/versions/%d", teamFooVersion.ID), getSoftwareRequest{}, http.StatusForbidden, &getSoftwareResp)
// Get a team software vesion using the deprecated endpoint
getSoftwareResp = getSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/%d", teamFooVersion.ID), getSoftwareRequest{}, http.StatusForbidden, &getSoftwareResp)
// Get a count of team software vesions using the deprecated endpoint
var countSoftwareResp countSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software/count", getSoftwareRequest{}, http.StatusForbidden, &countSoftwareResp)
// List all software versions using the deprecated endpoint for a team
var softwareListResp listSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software", listSoftwareRequest{}, http.StatusForbidden, &softwareListResp)
} else {
// List all software titles for a team
var listSoftwareTitlesResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{SoftwareTitleListOptions: fleet.SoftwareTitleListOptions{TeamID: &team1.ID}}, http.StatusOK, &listSoftwareTitlesResp)
require.Equal(t, 1, listSoftwareTitlesResp.Count)
require.NotEmpty(t, listSoftwareTitlesResp.CountsUpdatedAt)
// List software versions for a team.
var resp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/versions", listSoftwareRequest{SoftwareListOptions: fleet.SoftwareListOptions{TeamID: &team1.ID}}, http.StatusOK, &resp)
require.Equal(t, 1, resp.Count)
require.NotEmpty(t, resp.CountsUpdatedAt)
// Get a team software title
var getSoftwareTitleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", softwareFoo.ID), getSoftwareTitleRequest{}, http.StatusOK, &getSoftwareTitleResp)
// Get a team software version
var getSoftwareResp getSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/versions/%d", teamFooVersion.ID), getSoftwareRequest{}, http.StatusOK, &getSoftwareResp)
// Get a team software vesion using the deprecated endpoint
getSoftwareResp = getSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/%d", teamFooVersion.ID), getSoftwareRequest{}, http.StatusOK, &getSoftwareResp)
// Get a team count of software vesions using the deprecated endpoint
var countSoftwareResp countSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software/count", countSoftwareRequest{SoftwareListOptions: fleet.SoftwareListOptions{TeamID: &team1.ID}}, http.StatusOK, &countSoftwareResp)
require.Equal(t, 1, countSoftwareResp.Count)
// List all software versions using the deprecated endpoint for a team
var softwareListResp listSoftwareResponse
s.DoJSON("GET", "/api/latest/fleet/software", listSoftwareRequest{SoftwareListOptions: fleet.SoftwareListOptions{TeamID: &team1.ID}}, http.StatusOK, &softwareListResp)
require.Equal(t, countSoftwareResp.Count, 1)
}
})
}
// set the admin token again to avoid breaking other tests
s.token = s.getTestAdminToken()
}

View File

@ -142,6 +142,13 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript
return nil, ctxerr.Wrap(ctx, err, "get host lite")
}
if host.OrbitNodeKey == nil || *host.OrbitNodeKey == "" {
// fleetd is required to run scripts so if the host is enrolled via plain osquery we return
// an error
svc.authz.SkipAuthorization(ctx)
return nil, fleet.NewUserMessageError(errors.New(fleet.RunScriptDisabledErrMsg), http.StatusUnprocessableEntity)
}
maxPending := maxPendingScripts
// must check that only one of script id or contents is provided before

View File

@ -32,8 +32,8 @@ func TestHostRunScript(t *testing.T) {
}
}
teamHost := &fleet.Host{ID: 1, Hostname: "host-team", TeamID: ptr.Uint(1), SeenTime: time.Now()}
noTeamHost := &fleet.Host{ID: 2, Hostname: "host-no-team", TeamID: nil, SeenTime: time.Now()}
teamHost := &fleet.Host{ID: 1, Hostname: "host-team", TeamID: ptr.Uint(1), SeenTime: time.Now(), OrbitNodeKey: ptr.String("abc")}
noTeamHost := &fleet.Host{ID: 2, Hostname: "host-no-team", TeamID: nil, SeenTime: time.Now(), OrbitNodeKey: ptr.String("def")}
nonExistingHost := &fleet.Host{ID: 3, Hostname: "no-such-host", TeamID: nil}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil

View File

@ -2,11 +2,14 @@ package service
import (
"context"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
/////////////////////////////////////////////////////////////////////////////////
@ -156,9 +159,29 @@ func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, inc
return nil, authz.ForbiddenWithInternal("team does not exist", nil, nil, nil)
}
}
software, err := svc.ds.SoftwareByID(ctx, id, teamID, includeCVEScores)
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
software, err := svc.ds.SoftwareByID(ctx, id, teamID, includeCVEScores, &fleet.TeamFilter{
User: vc.User,
IncludeObserver: true,
})
if err != nil {
return nil, err
if fleet.IsNotFound(err) {
// here we use a global admin as filter because we want
// to check if the software version exists
filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
if _, err = svc.ds.SoftwareByID(ctx, id, teamID, includeCVEScores, &filter); err != nil {
return nil, ctxerr.Wrap(ctx, err, "checked using a global admin")
}
return nil, fleet.NewPermissionError("Error: You dont have permission to view specified software. It is installed on hosts that belong to team you dont have permissions to view.")
}
return nil, ctxerr.Wrap(ctx, err, "getting software version by id")
}
return software, nil

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