fleet/server/fleet/app.go
Scott Gress 4697123f6e
Stop setup experience on software install fail: admin (#33968)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33110 
**Related issue:** Resolves #33109  

# Details

This PR implements the new "cancel setup if any software fails on macos"
flag, including both backend and frontend logic.

Half of the file changes are updating test expectations / auto-generated
schema.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually

## New Fleet configuration settings

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
`macos_setup` is still excluded from generate-girtops
- [X] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
Documented [here](https://github.com/fleetdm/fleet/pull/33016/files)
- [X] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [X] Verified that any relevant UI is disabled when GitOps mode is
enabled


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Added a macOS setup option: “Cancel setup if software install fails.”
  - Configure at global or team level; team settings override global.
- Toggle available in Setup Experience > Install software > Advanced
options.
  - Saved state persists and can be updated without leaving the page.
  - Devices honor the resolved setting during provisioning.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-10-08 17:51:52 +01:00

1567 lines
55 KiB
Go

package fleet
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/url"
"reflect"
"regexp"
"sort"
"time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/pkg/rawjson"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/ptr"
)
// SMTP settings names returned from API, these map to SMTPAuthType and
// SMTPAuthMethod
const (
AuthMethodNameCramMD5 = "authmethod_cram_md5"
AuthMethodNameLogin = "authmethod_login"
AuthMethodNamePlain = "authmethod_plain"
AuthTypeNameUserNamePassword = "authtype_username_password"
AuthTypeNameNone = "authtype_none"
)
func (c AppConfig) AuthzType() string {
return "app_config"
}
const (
AppConfigKind = "config"
MaskedPassword = "********"
)
type SSOProviderSettings struct {
// EntityID is a uri that identifies this service provider
EntityID string `json:"entity_id"`
// IssuerURI is the uri that identifies the identity provider
//
// Deprecated: Not used, only left here to not break the API
// ("unsupported key provided" error)
IssuerURI string `json:"issuer_uri"`
// Metadata contains IDP metadata XML
Metadata string `json:"metadata"`
// MetadataURL is a URL provided by the IDP which can be used to download
// metadata
MetadataURL string `json:"metadata_url"`
// IDPName is a human friendly name for the IDP
IDPName string `json:"idp_name"`
}
func (s SSOProviderSettings) IsEmpty() bool {
return s == (SSOProviderSettings{})
}
// SSOSettings wire format for SSO settings
type SSOSettings struct {
SSOProviderSettings
// IDPImageURL is a link to a logo or other image that is used for UX
IDPImageURL string `json:"idp_image_url"`
// EnableSSO flag to determine whether or not to enable SSO
EnableSSO bool `json:"enable_sso"`
// EnableSSOIdPLogin flag to determine whether or not to allow IdP-initiated
// login.
EnableSSOIdPLogin bool `json:"enable_sso_idp_login"`
// EnableJITProvisioning allows user accounts to be created the first time
// users try to log in
EnableJITProvisioning bool `json:"enable_jit_provisioning"`
// EnableJITRoleSync is deprecated.
//
// EnableJITRoleSync sets whether the roles of existing accounts will be updated
// every time SSO users log in (does not have effect if EnableJITProvisioning is false).
EnableJITRoleSync bool `json:"enable_jit_role_sync"`
// SSOServerURL is an optional URL to use for SSO authentication.
// When set, SSO will only work from this URL, not from the server URL.
// This is useful for organizations with separate URLs for admin access vs agent/API access.
SSOServerURL string `json:"sso_server_url"`
}
// ConditionalAccessSettings holds the global settings for the "Conditional access" feature.
type ConditionalAccessSettings struct {
// MicrosoftEntraTenantID is the Entra's tenant ID.
MicrosoftEntraTenantID string `json:"microsoft_entra_tenant_id"`
// MicrosoftEntraConnectionConfigured is true when the tenant has been configured
// for "Conditional access" on Entra and Fleet.
MicrosoftEntraConnectionConfigured bool `json:"microsoft_entra_connection_configured"`
}
// SMTPSettings is part of the AppConfig which defines the wire representation
// of the app config endpoints
type SMTPSettings struct {
// SMTPEnabled indicates whether the user has selected that SMTP is
// enabled in the UI.
SMTPEnabled bool `json:"enable_smtp"`
// SMTPConfigured is a flag that indicates if smtp has been successfully
// tested with the settings provided by an admin user.
SMTPConfigured bool `json:"configured"`
// SMTPSenderAddress is the email address that will appear in emails sent
// from Fleet
SMTPSenderAddress string `json:"sender_address"`
// SMTPServer is the host name of the SMTP server Fleet will use to send mail
SMTPServer string `json:"server"`
// SMTPPort port SMTP server will use
SMTPPort uint `json:"port"`
// SMTPAuthenticationType type of authentication for SMTP
SMTPAuthenticationType string `json:"authentication_type"`
// SMTPUserName must be provided if SMTPAuthenticationType is UserNamePassword
SMTPUserName string `json:"user_name"`
// SMTPPassword must be provided if SMTPAuthenticationType is UserNamePassword
SMTPPassword string `json:"password"`
// SMTPEnableSSLTLS whether to use SSL/TLS for SMTP
SMTPEnableTLS bool `json:"enable_ssl_tls"`
// SMTPAuthenticationMethod authentication method smtp server will use
SMTPAuthenticationMethod string `json:"authentication_method"`
// SMTPDomain optional domain for SMTP
SMTPDomain string `json:"domain"`
// SMTPVerifySSLCerts defaults to true but can be turned off if self signed
// SSL certs are used by the SMTP server
SMTPVerifySSLCerts bool `json:"verify_ssl_certs"`
// SMTPEnableStartTLS detects of TLS is enabled on mail server and starts to use it (default true)
SMTPEnableStartTLS bool `json:"enable_start_tls"`
}
// VulnerabilitySettings is part of the AppConfig which defines how fleet will behave
// while scanning for vulnerabilities in the host software
type VulnerabilitySettings struct {
// DatabasesPath is the directory where fleet will store the different databases
DatabasesPath string `json:"databases_path"`
}
// MDMAppleABMAssignmentInfo represents an user definition of the association
// between an ABM token (via organization name) and the teams used to associate
// hosts when they're ingested during the ABM sync.
type MDMAppleABMAssignmentInfo struct {
OrganizationName string `json:"organization_name"`
MacOSTeam string `json:"macos_team"`
IOSTeam string `json:"ios_team"`
IpadOSTeam string `json:"ipados_team"`
}
// MDMAppleVolumePurchasingProgramInfo represents an user definition of the association
// between a VPP token (via location) and the team associations.
type MDMAppleVolumePurchasingProgramInfo struct {
Location string `json:"location"`
Teams []string `json:"teams"`
}
// MDM is part of AppConfig and defines the mdm settings.
type MDM struct {
// AppleServerURL is an alternate URL to be used in MDM configuration profiles to differentiate MDM
// requests from fleetd requests on customer networks. AppleServerURL DNS should resolve to the
// same IP as the Fleet Server URL.
// If not set, the server will use Fleet server URL (recommended).
AppleServerURL string `json:"apple_server_url"`
// Deprecated: use AppleBussinessManager instead
DeprecatedAppleBMDefaultTeam string `json:"apple_bm_default_team,omitempty"`
// AppleBusinessManager defines the associations between ABM tokens
// and the teams used to assign hosts when they're ingested from ABM.
AppleBusinessManager optjson.Slice[MDMAppleABMAssignmentInfo] `json:"apple_business_manager"`
// AppleBMEnabledAndConfigured is set to true if Fleet has been
// configured with the required Apple BM key pair or token. It can't be set
// manually via the PATCH /config API, it's only set automatically when
// the server starts.
AppleBMEnabledAndConfigured bool `json:"apple_bm_enabled_and_configured"`
// AppleBMTermsExpired is set to true if an Apple Business Manager request
// failed due to Apple's terms and conditions having changed and need the
// user to explicitly accept them. It cannot be set manually via the
// PATCH /config API, it is only set automatically, internally, by detecting
// the 403 Forbidden error with body T_C_NOT_SIGNED returned by the Apple BM
// API.
//
// It is set to true as soon as one of the ABM tokens receives this error
// code, and is set to false only once all ABM tokens have agreed to the new
// terms.
AppleBMTermsExpired bool `json:"apple_bm_terms_expired"`
// EnabledAndConfigured is set to true if Fleet has been
// configured with the required APNS and SCEP certificates. It can't be set
// manually via the PATCH /config API, it's only set automatically when
// the server starts.
//
// TODO: should ideally be renamed to AppleEnabledAndConfigured, but it
// implies a lot of changes to existing code across both frontend and
// backend, should be done only after careful analysis.
EnabledAndConfigured bool `json:"enabled_and_configured"`
// MacOSUpdates defines the OS update settings for macOS devices.
MacOSUpdates AppleOSUpdateSettings `json:"macos_updates"`
// IOSUpdates defines the OS update settings for iOS devices.
IOSUpdates AppleOSUpdateSettings `json:"ios_updates"`
// IPadOSUpdates defines the OS update settings for iPadOS devices.
IPadOSUpdates AppleOSUpdateSettings `json:"ipados_updates"`
// WindowsUpdates defines the OS update settings for Windows devices.
WindowsUpdates WindowsUpdates `json:"windows_updates"`
MacOSSettings MacOSSettings `json:"macos_settings"`
MacOSSetup MacOSSetup `json:"macos_setup"`
MacOSMigration MacOSMigration `json:"macos_migration"`
WindowsMigrationEnabled bool `json:"windows_migration_enabled"`
EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"`
// WindowsEnabledAndConfigured indicates if Fleet MDM is enabled for Windows.
// There is no other configuration required for Windows other than enabling
// the support, but it is still called "EnabledAndConfigured" for consistency
// with the similarly named macOS-specific fields.
WindowsEnabledAndConfigured bool `json:"windows_enabled_and_configured"`
EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"`
RequireBitLockerPIN optjson.Bool `json:"windows_require_bitlocker_pin"`
WindowsSettings WindowsSettings `json:"windows_settings"`
VolumePurchasingProgram optjson.Slice[MDMAppleVolumePurchasingProgramInfo] `json:"volume_purchasing_program"`
// AndroidEnabledAndConfigured is set to true if Fleet successfully bound to an Android Management Enterprise
AndroidEnabledAndConfigured bool `json:"android_enabled_and_configured"`
AndroidSettings AndroidSettings `json:"android_settings"`
/////////////////////////////////////////////////////////////////
// WARNING: If you add to this struct make sure it's taken into
// account in the AppConfig Clone implementation!
/////////////////////////////////////////////////////////////////
}
type DiskEncryptionConfig struct {
// Enabled indicates if disk encryption is enabled.
Enabled bool
// BitLockerPINRequired indicates if a PIN is required for BitLocker disk encryption.
BitLockerPINRequired bool
}
type UIGitOpsModeConfig struct {
GitopsModeEnabled bool `json:"gitops_mode_enabled"`
RepositoryURL string `json:"repository_url"`
}
func (c *AppConfig) MDMUrl() string {
if c.MDM.AppleServerURL == "" {
return c.ServerSettings.ServerURL
}
return c.MDM.AppleServerURL
}
// versionStringRegex is used to validate that a version string is in the x.y.z
// format only (no prerelease or build metadata).
var versionStringRegex = regexp.MustCompile(`^\d+(\.\d+)?(\.\d+)?$`)
// AppleOSUpdateSettings is the common type that contains the settings
// for OS updates on Apple devices.
type AppleOSUpdateSettings struct {
// MinimumVersion is the required minimum operating system version.
MinimumVersion optjson.String `json:"minimum_version"`
// Deadline the required installation date for Nudge to enforce the required
// operating system version.
Deadline optjson.String `json:"deadline"`
}
// Configured returns a boolean indicating if updates are configured
func (m AppleOSUpdateSettings) Configured() bool {
return m.Deadline.Value != "" &&
m.MinimumVersion.Value != ""
}
func (m AppleOSUpdateSettings) Validate() error {
// if no settings are provided it's okay to skip further validation
if m.MinimumVersion.Value == "" && m.Deadline.Value == "" {
// if one is set and empty, the other must be set and empty too, otherwise
// it's as if only one was provided.
if m.MinimumVersion.Set && !m.Deadline.Set {
return errors.New("deadline is required when minimum_version is provided")
} else if !m.MinimumVersion.Set && m.Deadline.Set {
return errors.New("minimum_version is required when deadline is provided")
}
return nil
}
if m.MinimumVersion.Value != "" && m.Deadline.Value == "" {
return errors.New("deadline is required when minimum_version is provided")
}
if m.Deadline.Value != "" && m.MinimumVersion.Value == "" {
return errors.New("minimum_version is required when deadline is provided")
}
if !versionStringRegex.MatchString(m.MinimumVersion.Value) {
return errors.New(`minimum_version accepts version numbers only. (E.g., "13.0.1.") NOT "Ventura 13" or "13.0.1 (22A400)"`)
}
if _, err := time.Parse("2006-01-02", m.Deadline.Value); err != nil {
return errors.New(`deadline accepts YYYY-MM-DD format only (E.g., "2023-06-01.")`)
}
return nil
}
// WindowsUpdates is part of AppConfig and defines the Windows update settings.
type WindowsUpdates struct {
DeadlineDays optjson.Int `json:"deadline_days"`
GracePeriodDays optjson.Int `json:"grace_period_days"`
}
// Equal returns true if the values of the fields of w and other are equal. It
// returns false otherwise. If e.g. w.DeadlineDays.Value == 0 but its .Valid
// field == false (i.e. it is null), it is not equal to
// other.DeadlineDays.Value == 0 with its .Valid field == true.
func (w WindowsUpdates) Equal(other WindowsUpdates) bool {
if w.DeadlineDays.Value != other.DeadlineDays.Value || w.DeadlineDays.Valid != other.DeadlineDays.Valid {
return false
}
if w.GracePeriodDays.Value != other.GracePeriodDays.Value || w.GracePeriodDays.Valid != other.GracePeriodDays.Valid {
return false
}
return true
}
func (w WindowsUpdates) Validate() error {
const (
minDeadline = 0
maxDeadline = 30
minGracePeriod = 0
maxGracePeriod = 7
)
// both must be specified or not specified
if w.DeadlineDays.Valid != w.GracePeriodDays.Valid {
if w.DeadlineDays.Valid && !w.GracePeriodDays.Valid {
return errors.New("grace_period_days is required when deadline_days is provided")
} else if !w.DeadlineDays.Valid && w.GracePeriodDays.Valid {
return errors.New("deadline_days is required when grace_period_days is provided")
}
}
// if both are unspecified, nothing more to validate, updates are not enforced.
if !w.DeadlineDays.Valid {
return nil
}
// at this point, both fields are set
if w.DeadlineDays.Value < minDeadline || w.DeadlineDays.Value > maxDeadline {
return fmt.Errorf("deadline_days must be an integer between %d and %d", minDeadline, maxDeadline)
}
if w.GracePeriodDays.Value < minGracePeriod || w.GracePeriodDays.Value > maxGracePeriod {
return fmt.Errorf("grace_period_days must be an integer between %d and %d", minGracePeriod, maxGracePeriod)
}
return nil
}
// MacOSSettings contains settings specific to macOS.
type MacOSSettings struct {
// CustomSettings is a slice of configuration profile file paths.
//
// NOTE: These are only present here for informational purposes.
// (The source of truth for profiles is in MySQL.)
CustomSettings []MDMProfileSpec `json:"custom_settings"`
DeprecatedEnableDiskEncryption *bool `json:"enable_disk_encryption,omitempty"`
// NOTE: make sure to update the ToMap/FromMap methods when adding/updating fields.
}
func (s MacOSSettings) GetMDMProfileSpecs() []MDMProfileSpec {
return s.CustomSettings
}
func (s MacOSSettings) ToMap() map[string]interface{} {
return map[string]interface{}{
"custom_settings": s.CustomSettings,
"enable_disk_encryption": s.DeprecatedEnableDiskEncryption,
}
}
type WithMDMProfileSpecs interface {
GetMDMProfileSpecs() []MDMProfileSpec
}
// Compile-time interface check
var _ WithMDMProfileSpecs = MacOSSettings{}
// FromMap sets the macOS settings from the provided map, which is the map type
// from the ApplyTeams spec struct. It returns a map of fields that were set in
// the map (ie. the key was present even if empty) or an error. If the
// operation updates an existing team, it should be called on the existing
// MacOSSettings so that its fields are replaced only if present in the map.
func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, error) {
set := make(map[string]bool)
extractLabelField := func(parentMap map[string]interface{}, fieldName string) []string {
var ret []string
if labels, ok := parentMap[fieldName].([]interface{}); ok {
for _, label := range labels {
if strLabel, ok := label.(string); ok {
ret = append(ret, strLabel)
}
}
}
return ret
}
if v, ok := m["custom_settings"]; ok {
set["custom_settings"] = true
vals, ok := v.([]interface{})
if v == nil || ok {
csSpecs := make([]MDMProfileSpec, 0, len(vals))
for _, v := range vals {
if m, ok := v.(map[string]interface{}); ok {
var spec MDMProfileSpec
// extract the Path field
if path, ok := m["path"].(string); ok {
spec.Path = path
}
spec.Labels = extractLabelField(m, "labels")
spec.LabelsIncludeAll = extractLabelField(m, "labels_include_all")
spec.LabelsExcludeAny = extractLabelField(m, "labels_exclude_any")
spec.LabelsIncludeAny = extractLabelField(m, "labels_include_any")
csSpecs = append(csSpecs, spec)
} else if m, ok := v.(string); ok { // for backwards compatibility with the old way to define profiles
csSpecs = append(csSpecs, MDMProfileSpec{Path: m})
} else {
return nil, &json.UnmarshalTypeError{
Value: fmt.Sprintf("%T", v),
Type: reflect.TypeOf(s.CustomSettings),
Field: "macos_settings.custom_settings",
}
}
}
s.CustomSettings = csSpecs
}
}
if v, ok := m["enable_disk_encryption"]; ok {
set["enable_disk_encryption"] = true
b, ok := v.(bool)
if !ok {
// error, must be a bool
return nil, &json.UnmarshalTypeError{
Value: fmt.Sprintf("%T", v),
Type: reflect.TypeOf(s.DeprecatedEnableDiskEncryption).Elem(),
Field: "macos_settings.enable_disk_encryption",
}
}
s.DeprecatedEnableDiskEncryption = ptr.Bool(b)
}
return set, nil
}
// MacOSSetup contains settings related to the setup of DEP enrolled devices.
type MacOSSetup struct {
BootstrapPackage optjson.String `json:"bootstrap_package"`
EnableEndUserAuthentication bool `json:"enable_end_user_authentication"`
MacOSSetupAssistant optjson.String `json:"macos_setup_assistant"`
EnableReleaseDeviceManually optjson.Bool `json:"enable_release_device_manually"`
Script optjson.String `json:"script"`
Software optjson.Slice[*MacOSSetupSoftware] `json:"software"`
ManualAgentInstall optjson.Bool `json:"manual_agent_install"`
RequireAllSoftware bool `json:"require_all_software_macos"`
}
func (mos *MacOSSetup) SetDefaultsIfNeeded() {
if mos == nil {
return
}
if !mos.BootstrapPackage.Valid {
mos.BootstrapPackage = optjson.SetString("")
}
if !mos.MacOSSetupAssistant.Valid {
mos.MacOSSetupAssistant = optjson.SetString("")
}
if !mos.EnableReleaseDeviceManually.Valid {
mos.EnableReleaseDeviceManually = optjson.SetBool(false)
}
if !mos.Script.Valid {
mos.Script = optjson.SetString("")
}
if !mos.Software.Valid {
mos.Software = optjson.SetSlice([]*MacOSSetupSoftware{})
}
if !mos.ManualAgentInstall.Valid {
mos.ManualAgentInstall = optjson.SetBool(false)
}
}
func NewMacOSSetupWithDefaults() *MacOSSetup {
mos := &MacOSSetup{}
mos.SetDefaultsIfNeeded()
return mos
}
// MacOSSetupSoftware represents a VPP app or a software package to install
// during the setup experience of a macOS device.
type MacOSSetupSoftware struct {
AppStoreID string `json:"app_store_id"`
PackagePath string `json:"package_path"`
}
// MacOSMigration contains settings related to the MDM migration work flow.
type MacOSMigration struct {
Enable bool `json:"enable"`
Mode MacOSMigrationMode `json:"mode"`
WebhookURL string `json:"webhook_url"`
}
// MacOSMigrationMode defines the possible modes that can be set if a user enables the MDM migration
// work flow in Fleet.
type MacOSMigrationMode string
const (
MacOSMigrationModeForced MacOSMigrationMode = "forced"
MacOSMigrationModeVoluntary MacOSMigrationMode = "voluntary"
)
// IsValid returns true if the mode is one of the valid modes.
func (s MacOSMigrationMode) IsValid() bool {
switch s {
case MacOSMigrationModeForced, MacOSMigrationModeVoluntary:
return true
default:
return false
}
}
// MDMEndUserAuthentication contains settings related to end user authentication
// to gate certain MDM features (eg: enrollment)
type MDMEndUserAuthentication struct {
// SSOSettings configure the IdP integration. Note that all keys under
// SSOProviderSettings are top-level keys under this struct, that's why
// it's embedded.
SSOProviderSettings
}
// AppConfig holds global server configuration that can be changed via the API.
//
// Note: management of deprecated fields is done on JSON-marshalling and uses
// the legacyConfig struct to list them.
//
// ///////////////////////////////////////////////////////////////
// WARNING: If you add or change fields of this struct make sure
// it's taken into account in the AppConfig Clone implementation!
// ///////////////////////////////////////////////////////////////
type AppConfig struct {
OrgInfo OrgInfo `json:"org_info"`
ServerSettings ServerSettings `json:"server_settings"`
// SMTPSettings holds the SMTP integration settings.
//
// This field is a pointer to avoid returning this information to non-global-admins.
SMTPSettings *SMTPSettings `json:"smtp_settings,omitempty"`
HostExpirySettings HostExpirySettings `json:"host_expiry_settings"`
ActivityExpirySettings ActivityExpirySettings `json:"activity_expiry_settings"`
// Features allows to globally enable or disable features
Features Features `json:"features"`
DeprecatedHostSettings *Features `json:"host_settings,omitempty"`
// AgentOptions holds osquery configuration.
//
// This field is a pointer to avoid returning this information to non-global-admins.
AgentOptions *json.RawMessage `json:"agent_options,omitempty"`
// SMTPTest is a flag that if set will cause the server to test email configuration
SMTPTest bool `json:"smtp_test,omitempty"`
// SSOSettings is single sign on integration settings.
//
// This field is a pointer to avoid returning this information to non-global-admins.
SSOSettings *SSOSettings `json:"sso_settings,omitempty"`
// FleetDesktop holds settings for Fleet Desktop that can be changed via the API.
FleetDesktop FleetDesktopSettings `json:"fleet_desktop"`
// VulnerabilitySettings defines how fleet will behave while scanning for vulnerabilities in the host software
VulnerabilitySettings VulnerabilitySettings `json:"vulnerability_settings"`
WebhookSettings WebhookSettings `json:"webhook_settings"`
Integrations Integrations `json:"integrations"`
MDM MDM `json:"mdm"`
UIGitOpsMode UIGitOpsModeConfig `json:"gitops"`
// Scripts is a slice of script file paths.
//
// NOTE: These are only present here for informational purposes.
// (The source of truth for scripts is in MySQL.)
Scripts optjson.Slice[string] `json:"scripts"`
YaraRules []YaraRule `json:"yara_rules,omitempty"`
// when true, strictDecoding causes the UnmarshalJSON method to return an
// error if there are unknown fields in the raw JSON.
strictDecoding bool
// this field is set to the list of legacy settings keys during UnmarshalJSON
// if any legacy settings were set in the raw JSON.
didUnmarshalLegacySettings []string
// ///////////////////////////////////////////////////////////////
// WARNING: If you add or change fields of this struct make sure
// it's taken into account in the AppConfig Clone implementation!
// ///////////////////////////////////////////////////////////////
}
// Obfuscate overrides credentials with obfuscated characters.
func (c *AppConfig) Obfuscate() {
if c.SMTPSettings != nil && c.SMTPSettings.SMTPPassword != "" {
c.SMTPSettings.SMTPPassword = MaskedPassword
}
for _, jiraIntegration := range c.Integrations.Jira {
jiraIntegration.APIToken = MaskedPassword
}
for _, zdIntegration := range c.Integrations.Zendesk {
zdIntegration.APIToken = MaskedPassword
}
// // TODO(hca): confirm that we're properly masking credentials in the new endpoints
// if c.Integrations.NDESSCEPProxy.Valid {
// c.Integrations.NDESSCEPProxy.Value.Password = MaskedPassword
// }
}
// Clone implements cloner.
func (c *AppConfig) Clone() (Cloner, error) {
return c.Copy(), nil
}
// Copy returns a copy of the AppConfig.
func (c *AppConfig) Copy() *AppConfig {
if c == nil {
return nil
}
clone := *c
// OrgInfo: nothing needs cloning
// FleetDesktopSettings: nothing needs cloning
if c.ServerSettings.DebugHostIDs != nil {
clone.ServerSettings.DebugHostIDs = make([]uint, len(c.ServerSettings.DebugHostIDs))
copy(clone.ServerSettings.DebugHostIDs, c.ServerSettings.DebugHostIDs)
}
if c.SMTPSettings != nil {
smtpSettings := *c.SMTPSettings
clone.SMTPSettings = &smtpSettings
}
// HostExpirySettings: nothing needs cloning
if c.Features.AdditionalQueries != nil {
aq := make(json.RawMessage, len(*c.Features.AdditionalQueries))
copy(aq, *c.Features.AdditionalQueries)
clone.Features.AdditionalQueries = &aq
}
if c.Features.DetailQueryOverrides != nil {
clone.Features.DetailQueryOverrides = make(map[string]*string, len(c.Features.DetailQueryOverrides))
for k, v := range c.Features.DetailQueryOverrides {
var s *string
if v != nil {
s = ptr.String(*v)
}
clone.Features.DetailQueryOverrides[k] = s
}
}
if c.AgentOptions != nil {
ao := make(json.RawMessage, len(*c.AgentOptions))
copy(ao, *c.AgentOptions)
clone.AgentOptions = &ao
}
if c.SSOSettings != nil {
ssoSettings := *c.SSOSettings
clone.SSOSettings = &ssoSettings
}
// FleetDesktop: nothing needs cloning
// VulnerabilitySettings: nothing needs cloning
if c.WebhookSettings.FailingPoliciesWebhook.PolicyIDs != nil {
clone.WebhookSettings.FailingPoliciesWebhook.PolicyIDs = make([]uint, len(c.WebhookSettings.FailingPoliciesWebhook.PolicyIDs))
copy(clone.WebhookSettings.FailingPoliciesWebhook.PolicyIDs, c.WebhookSettings.FailingPoliciesWebhook.PolicyIDs)
}
if c.Integrations.Jira != nil {
clone.Integrations.Jira = make([]*JiraIntegration, len(c.Integrations.Jira))
for i, j := range c.Integrations.Jira {
jira := *j
clone.Integrations.Jira[i] = &jira
}
}
if c.Integrations.Zendesk != nil {
clone.Integrations.Zendesk = make([]*ZendeskIntegration, len(c.Integrations.Zendesk))
for i, z := range c.Integrations.Zendesk {
zd := *z
clone.Integrations.Zendesk[i] = &zd
}
}
if len(c.Integrations.GoogleCalendar) > 0 {
clone.Integrations.GoogleCalendar = make([]*GoogleCalendarIntegration, len(c.Integrations.GoogleCalendar))
for i, g := range c.Integrations.GoogleCalendar {
gCal := *g
clone.Integrations.GoogleCalendar[i] = &gCal
clone.Integrations.GoogleCalendar[i].ApiKey = make(map[string]string, len(g.ApiKey))
maps.Copy(clone.Integrations.GoogleCalendar[i].ApiKey, g.ApiKey)
}
}
// // TODO(hca): do we want to cache the new grouped CAs datastore method?
// if len(c.Integrations.DigiCert.Value) > 0 {
// digicert := make([]DigiCertCA, len(c.Integrations.DigiCert.Value))
// copy(digicert, c.Integrations.DigiCert.Value)
// clone.Integrations.DigiCert = optjson.SetSlice(digicert)
// }
// if len(c.Integrations.CustomSCEPProxy.Value) > 0 {
// customSCEP := make([]CustomSCEPProxyCA, len(c.Integrations.CustomSCEPProxy.Value))
// copy(customSCEP, c.Integrations.CustomSCEPProxy.Value)
// clone.Integrations.CustomSCEPProxy = optjson.SetSlice(customSCEP)
// }
if c.MDM.MacOSSettings.CustomSettings != nil {
clone.MDM.MacOSSettings.CustomSettings = make([]MDMProfileSpec, len(c.MDM.MacOSSettings.CustomSettings))
for i, mps := range c.MDM.MacOSSettings.CustomSettings {
clone.MDM.MacOSSettings.CustomSettings[i] = *mps.Copy()
}
}
if c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
b := *c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption
clone.MDM.MacOSSettings.DeprecatedEnableDiskEncryption = &b
}
if c.Scripts.Set {
scripts := make([]string, len(c.Scripts.Value))
copy(scripts, c.Scripts.Value)
clone.Scripts = optjson.SetSlice(scripts)
}
if c.MDM.WindowsSettings.CustomSettings.Set {
windowsSettings := make([]MDMProfileSpec, len(c.MDM.WindowsSettings.CustomSettings.Value))
for i, mps := range c.MDM.WindowsSettings.CustomSettings.Value {
windowsSettings[i] = *mps.Copy()
}
clone.MDM.WindowsSettings.CustomSettings = optjson.SetSlice(windowsSettings)
}
if c.MDM.AndroidSettings.CustomSettings.Set {
androidSettings := make([]MDMProfileSpec, len(c.MDM.AndroidSettings.CustomSettings.Value))
for i, mps := range c.MDM.AndroidSettings.CustomSettings.Value {
androidSettings[i] = *mps.Copy()
}
clone.MDM.AndroidSettings.CustomSettings = optjson.SetSlice(androidSettings)
}
if c.MDM.AppleBusinessManager.Set {
abm := make([]MDMAppleABMAssignmentInfo, len(c.MDM.AppleBusinessManager.Value))
copy(abm, c.MDM.AppleBusinessManager.Value)
clone.MDM.AppleBusinessManager = optjson.SetSlice(abm)
}
if c.MDM.VolumePurchasingProgram.Set {
vpp := make([]MDMAppleVolumePurchasingProgramInfo, len(c.MDM.VolumePurchasingProgram.Value))
for i, s := range c.MDM.VolumePurchasingProgram.Value {
vpp[i].Location = s.Location
vpp[i].Teams = make([]string, len(s.Teams))
copy(vpp[i].Teams, s.Teams)
}
clone.MDM.VolumePurchasingProgram = optjson.SetSlice(vpp)
}
if c.MDM.MacOSSetup.Software.Set {
sw := make([]*MacOSSetupSoftware, len(c.MDM.MacOSSetup.Software.Value))
for i, s := range c.MDM.MacOSSetup.Software.Value {
s := *s
sw[i] = &s
}
clone.MDM.MacOSSetup.Software = optjson.SetSlice(sw)
}
// UIGitOpsMode: nothing needs cloning
if c.YaraRules != nil {
rules := make([]YaraRule, len(c.YaraRules))
copy(rules, c.YaraRules)
clone.YaraRules = rules
}
return &clone
}
// EnrichedAppConfig contains the AppConfig along with additional fleet
// instance configuration settings as returned by the
// "GET /api/latest/fleet/config" API endpoint (and fleetctl get config).
type EnrichedAppConfig struct {
AppConfig
enrichedAppConfigFields
}
// enrichedAppConfigFields are grouped separately to aid with JSON unmarshaling
type enrichedAppConfigFields struct {
UpdateInterval *UpdateIntervalConfig `json:"update_interval,omitempty"`
Vulnerabilities *VulnerabilitiesConfig `json:"vulnerabilities,omitempty"`
License *LicenseInfo `json:"license,omitempty"`
Logging *Logging `json:"logging,omitempty"`
Email *EmailConfig `json:"email,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface to make sure we serialize
// both AppConfig and enrichedAppConfigFields properly:
//
// - If this function is not defined, AppConfig.UnmarshalJSON gets promoted and
// will be called instead.
// - If we try to unmarshal everything in one go, AppConfig.UnmarshalJSON doesn't get
// called.
func (e *EnrichedAppConfig) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &e.AppConfig); err != nil {
return err
}
if err := json.Unmarshal(data, &e.enrichedAppConfigFields); err != nil {
return err
}
return nil
}
// MarshalJSON implements the json.Marshaler interface to make sure we serialize
// both AppConfig and enrichedAppConfigFields properly:
//
// - If this function is not defined, AppConfig.MarshalJSON gets promoted and
// will be called instead.
// - If we try to unmarshal everything in one go, AppConfig.MarshalJSON doesn't get
// called.
func (e *EnrichedAppConfig) MarshalJSON() ([]byte, error) {
// Marshal only the enriched fields
enrichedData, err := json.Marshal(e.enrichedAppConfigFields)
if err != nil {
return nil, err
}
// Marshal the base AppConfig
appConfigData, err := json.Marshal(e.AppConfig)
if err != nil {
return nil, err
}
// we need to marshal and combine both groups separately because
// AppConfig has a custom marshaler.
return rawjson.CombineRoots(enrichedData, appConfigData)
}
type Duration struct {
time.Duration
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d Duration) ValueOr(t time.Duration) time.Duration {
if d.Duration == 0 {
return t
}
return d.Duration
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
d.Duration = time.Duration(value)
return nil
case string:
var err error
d.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
return nil
default:
return fmt.Errorf("invalid duration type: %T", value)
}
}
type WebhookSettings struct {
ActivitiesWebhook ActivitiesWebhookSettings `json:"activities_webhook"`
HostStatusWebhook HostStatusWebhookSettings `json:"host_status_webhook"`
FailingPoliciesWebhook FailingPoliciesWebhookSettings `json:"failing_policies_webhook"`
VulnerabilitiesWebhook VulnerabilitiesWebhookSettings `json:"vulnerabilities_webhook"`
// Interval is the interval for running the webhooks.
//
// This value currently configures both the host status and failing policies webhooks.
Interval Duration `json:"interval"`
}
type ActivitiesWebhookSettings struct {
Enable bool `json:"enable_activities_webhook"`
DestinationURL string `json:"destination_url"`
}
type HostStatusWebhookSettings struct {
Enable bool `json:"enable_host_status_webhook"`
DestinationURL string `json:"destination_url"`
HostPercentage float64 `json:"host_percentage"`
DaysCount int `json:"days_count"`
}
// FailingPoliciesWebhookSettings holds the settings for failing policy webhooks.
type FailingPoliciesWebhookSettings struct {
// Enable indicates whether the webhook for failing policies is enabled.
Enable bool `json:"enable_failing_policies_webhook"`
// DestinationURL is the webhook's URL.
DestinationURL string `json:"destination_url"`
// PolicyIDs is a list of policy IDs for which the webhook will be configured.
PolicyIDs []uint `json:"policy_ids"`
// HostBatchSize allows sending multiple requests in batches of hosts for each policy.
// A value of 0 means no batching.
HostBatchSize int `json:"host_batch_size"`
}
// VulnerabilitiesWebhookSettings holds the settings for vulnerabilities webhooks.
type VulnerabilitiesWebhookSettings struct {
// Enable indicates whether the webhook for vulnerabilities is enabled.
Enable bool `json:"enable_vulnerabilities_webhook"`
// DestinationURL is the webhook's URL.
DestinationURL string `json:"destination_url"`
// HostBatchSize allows sending multiple requests in batches of hosts for each vulnerable software found.
// A value of 0 means no batching.
HostBatchSize int `json:"host_batch_size"`
}
func (c *AppConfig) ApplyDefaultsForNewInstalls() {
c.ServerSettings.EnableAnalytics = true
// Add default values for SMTPSettings.
var smtpSettings SMTPSettings
smtpSettings.SMTPEnabled = false
smtpSettings.SMTPPort = 587
smtpSettings.SMTPEnableStartTLS = true
smtpSettings.SMTPAuthenticationType = AuthTypeNameUserNamePassword
smtpSettings.SMTPAuthenticationMethod = AuthMethodNamePlain
smtpSettings.SMTPVerifySSLCerts = true
smtpSettings.SMTPEnableTLS = true
c.SMTPSettings = &smtpSettings
agentOptions := json.RawMessage(`{"config": {"options": {"pack_delimiter": "/", "logger_tls_period": 10, "distributed_plugin": "tls", "disable_distributed": false, "logger_tls_endpoint": "/api/osquery/log", "distributed_interval": 10, "distributed_tls_max_attempts": 3}, "decorators": {"load": ["SELECT uuid AS host_uuid FROM system_info;", "SELECT hostname AS hostname FROM system_info;"]}}, "overrides": {}}`)
c.AgentOptions = &agentOptions
// Make sure an empty SSOSettings is set.
var ssoSettings SSOSettings
c.SSOSettings = &ssoSettings
c.Features.ApplyDefaultsForNewInstalls()
c.ApplyDefaults()
}
func (c *AppConfig) ApplyDefaults() {
c.Features.ApplyDefaults()
c.WebhookSettings.Interval.Duration = 24 * time.Hour
}
// EnableStrictDecoding enables strict decoding of the AppConfig struct.
func (c *AppConfig) EnableStrictDecoding() { c.strictDecoding = true }
// DidUnmarshalLegacySettings returns the list of legacy settings keys that
// were set in the JSON used to unmarshal this AppConfig.
func (c *AppConfig) DidUnmarshalLegacySettings() []string { return c.didUnmarshalLegacySettings }
// UnmarshalJSON implements the json.Unmarshaler interface.
func (c *AppConfig) UnmarshalJSON(b []byte) error {
// Define a new type, this is to prevent infinite recursion when
// unmarshalling the AppConfig struct.
type aliasConfig AppConfig
compatConfig := struct {
*aliasConfig
}{
(*aliasConfig)(c),
}
decoder := json.NewDecoder(bytes.NewReader(b))
if c.strictDecoding {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(&compatConfig); err != nil {
return err
}
if _, err := decoder.Token(); err != io.EOF {
return errors.New("unexpected extra tokens found in config")
}
c.assignDeprecatedFields()
return nil
}
func (c AppConfig) MarshalJSON() ([]byte, error) {
// Define a new type, this is to prevent infinite recursion when
// marshalling the AppConfig struct.
c.assignDeprecatedFields()
// requirements are that if this value is not set, defaults to false.
// The default mashaler of optjson.Bool will convert this to `null` if
// it's not valid.
if !c.MDM.EnableDiskEncryption.Valid {
c.MDM.EnableDiskEncryption = optjson.SetBool(false)
}
if !c.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
c.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
}
type aliasConfig AppConfig
aa := aliasConfig(c)
return json.Marshal(aa)
}
func (c *AppConfig) assignDeprecatedFields() {
c.didUnmarshalLegacySettings = nil
// Define and assign legacy settings to new fields.
// This has the drawback of legacy fields taking precedence over new fields
// if both are defined.
//
// TODO: with optjson + the new approach we're using to handle legacy
// fields, legacy fields don't have to take precedence over new fields.
// Is it worth changing this behavior for `host_settings`/`features` at this point?
if c.DeprecatedHostSettings != nil {
c.didUnmarshalLegacySettings = append(c.didUnmarshalLegacySettings, "host_settings")
c.Features = *c.DeprecatedHostSettings
}
// if disk encryption is not set in the root config
// try to read the value from the legacy config
if !c.MDM.EnableDiskEncryption.Valid {
if c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption != nil {
c.didUnmarshalLegacySettings = append(c.didUnmarshalLegacySettings, "mdm.macos_settings.enable_disk_encryption")
c.MDM.EnableDiskEncryption = optjson.SetBool(*c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption)
}
}
// ensure the legacy configs are always nil
c.DeprecatedHostSettings = nil
c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption = nil
sort.Strings(c.didUnmarshalLegacySettings)
}
// OrgInfo contains general info about the organization using Fleet.
type OrgInfo struct {
OrgName string `json:"org_name"`
OrgLogoURL string `json:"org_logo_url"`
OrgLogoURLLightBackground string `json:"org_logo_url_light_background"`
// ContactURL is the URL displayed for users to contact support. By default,
// https://fleetdm.com/company/contact is used.
ContactURL string `json:"contact_url"`
}
const DefaultOrgInfoContactURL = "https://fleetdm.com/company/contact"
// ServerSettings contains general settings about the Fleet application.
type ServerSettings struct {
ServerURL string `json:"server_url"`
LiveQueryDisabled bool `json:"live_query_disabled"`
EnableAnalytics bool `json:"enable_analytics"`
DebugHostIDs []uint `json:"debug_host_ids,omitempty"`
DeferredSaveHost bool `json:"deferred_save_host"`
QueryReportsDisabled bool `json:"query_reports_disabled"`
ScriptsDisabled bool `json:"scripts_disabled"`
AIFeaturesDisabled bool `json:"ai_features_disabled"`
QueryReportCap int `json:"query_report_cap"`
}
const DefaultMaxQueryReportRows int = 1000
func (f *ServerSettings) GetQueryReportCap() int {
if f.QueryReportCap <= 0 {
return DefaultMaxQueryReportRows
}
return f.QueryReportCap
}
// HostExpirySettings contains settings pertaining to automatic host expiry.
type HostExpirySettings struct {
HostExpiryEnabled bool `json:"host_expiry_enabled"`
HostExpiryWindow int `json:"host_expiry_window"`
}
// ActivityExpirySettings contains settings pertaining to automatic activities cleanup.
type ActivityExpirySettings struct {
ActivityExpiryEnabled bool `json:"activity_expiry_enabled"`
ActivityExpiryWindow int `json:"activity_expiry_window"`
}
type Features struct {
EnableHostUsers bool `json:"enable_host_users"`
EnableSoftwareInventory bool `json:"enable_software_inventory"`
AdditionalQueries *json.RawMessage `json:"additional_queries,omitempty"`
DetailQueryOverrides map[string]*string `json:"detail_query_overrides,omitempty"`
/////////////////////////////////////////////////////////////////
// WARNING: If you add to this struct make sure it's taken into
// account in the Features Clone implementation!
/////////////////////////////////////////////////////////////////
}
func (f *Features) ApplyDefaultsForNewInstalls() {
// Software inventory is enabled only for new installs as
// we didn't want to enable software inventory from one version to the
// next in already running fleets
f.EnableSoftwareInventory = true
f.ApplyDefaults()
}
func (f *Features) ApplyDefaults() {
f.EnableHostUsers = true
}
// Clone implements cloner for Features.
func (f *Features) Clone() (Cloner, error) {
return f.Copy(), nil
}
// Copy returns a deep copy of the Features.
func (f *Features) Copy() *Features {
if f == nil {
return nil
}
// EnableHostUsers and EnableSoftwareInventory don't have fields that require
// cloning (all fields are basic value types, no pointers/slices/maps).
clone := *f
if f.AdditionalQueries != nil {
aq := make(json.RawMessage, len(*f.AdditionalQueries))
copy(aq, *f.AdditionalQueries)
clone.AdditionalQueries = &aq
}
if f.DetailQueryOverrides != nil {
clone.DetailQueryOverrides = make(map[string]*string, len(f.DetailQueryOverrides))
for k, v := range f.DetailQueryOverrides {
var s *string
if v != nil {
s = ptr.String(*v)
}
clone.DetailQueryOverrides[k] = s
}
}
return &clone
}
// FleetDesktopSettings contains settings used to configure Fleet Desktop.
type FleetDesktopSettings struct {
// TransparencyURL is the URL used for the “About Fleet” link in the Fleet Desktop menu.
TransparencyURL string `json:"transparency_url"`
}
// DefaultTransparencyURL is the default URL used for the “About Fleet” link in the Fleet Desktop menu.
const DefaultTransparencyURL = "https://fleetdm.com/transparency"
// SecureframeTransparencyURL is the URL used for the "About Fleet" link in Fleet Desktop when the Secureframe partnership config value is enabled
const SecureframeTransparencyURL = "https://fleetdm.com/better?utm_content=secureframe"
type OrderDirection int
const (
OrderAscending OrderDirection = iota
OrderDescending
// PerPageUnlimited is the value to pass to PerPage when we want
// "unlimited". If we ever find this limit to be too low, congratulations on
// incredible growth of the product!
PerPageUnlimited uint = 9999999
)
// ListOptions defines options related to paging and ordering to be used when
// listing objects
type ListOptions struct {
// Which page to return (must be positive integer)
Page uint `query:"page,optional"`
// How many results per page (must be positive integer, 0 indicates
// unlimited)
PerPage uint `query:"per_page,optional"`
// Key to use for ordering. Can be a comma-separated set of items, eg: host_count,id
OrderKey string `query:"order_key,optional"`
// Direction of ordering
OrderDirection OrderDirection `query:"order_direction,optional"`
// MatchQuery is the query string to match against columns of the entity
// (varies depending on entity, eg. hostname, IP address for hosts).
// Handling for this parameter must be implemented separately for each type.
MatchQuery string `query:"query,optional"`
// After denotes the row to start from. This is meant to be used in conjunction with OrderKey
// If OrderKey is "id", it'll assume After is a number and will try to convert it.
After string `query:"after,optional"`
// Used to request the pagination metadata in the response.
IncludeMetadata bool
// The following fields are for tests, to ensure a deterministic sort order
// when the single-column order key is not unique.
TestSecondaryOrderKey string `query:"-,optional"`
TestSecondaryOrderDirection OrderDirection `query:"-,optional"`
}
func (l ListOptions) Empty() bool {
return l == ListOptions{}
}
func (l ListOptions) UsesCursorPagination() bool {
return l.After != "" && l.OrderKey != ""
}
type ListQueryOptions struct {
ListOptions
// TeamID which team the queries belong to. If teamID is nil, then it is assumed the 'global'
// team.
TeamID *uint
// IsScheduled filters queries that are meant to run at a set interval.
IsScheduled *bool
// MergeInherited merges inherited global queries into the team list. Is only valid when TeamID
// is set.
MergeInherited bool
// Return queries that are scheduled to run on this platform. One of "macos",
// "windows", or "linux"
Platform *string
}
type ListActivitiesOptions struct {
ListOptions
Streamed *bool
}
// ApplySpecOptions are the options available when applying a YAML or JSON spec.
type ApplySpecOptions struct {
// Force indicates that any validation error in the incoming payload should
// be ignored and the spec should be applied anyway.
Force bool
// DryRun indicates that the spec should not be applied, but the validation
// errors should be returned.
DryRun bool
// TeamForPolicies is the name of the team to set in policy specs.
TeamForPolicies string
// NoCache indicates that cached_mysql calls should be bypassed on the server.
// This is needed where related data was just updated and we need that latest data from the DB.
NoCache bool
// Indicate whether or not the spec should be applied in overwrite mode.
// This means that any missing fields in the spec will be set to their default values.
// GitOps uses this mode.
Overwrite bool
}
type ApplyTeamSpecOptions struct {
ApplySpecOptions
DryRunAssumptions *TeamSpecsDryRunAssumptions
}
// ApplyClientSpecOptions embeds a ApplySpecOptions and adds additional client
// side configuration.
type ApplyClientSpecOptions struct {
ApplySpecOptions
// ExpandEnvConfigProfiles enables expansion of environment variables in
// configuration profiles.
ExpandEnvConfigProfiles bool
}
// RawQuery returns the ApplySpecOptions url-encoded for use in an URL's
// query string parameters. It only sets the parameters that are not the
// default values.
func (o *ApplySpecOptions) RawQuery() string {
if o == nil {
return ""
}
query := make(url.Values)
if o.Force {
query.Set("force", "true")
}
if o.DryRun {
query.Set("dry_run", "true")
}
if o.NoCache {
query.Set("no_cache", "true")
}
if o.Overwrite {
query.Set("overwrite", "true")
}
return query.Encode()
}
// EnrollSecret contains information about an enroll secret, name, and active
// status. Enroll secrets are used for osquery authentication.
type EnrollSecret struct {
// Secret is the actual secret key.
Secret string `json:"secret" db:"secret"`
// CreatedAt is the time this enroll secret was first added.
CreatedAt time.Time `json:"created_at" db:"created_at"`
// TeamID is the ID for the associated team. If no ID is set, then this is a
// global enroll secret.
TeamID *uint `json:"team_id,omitempty" db:"team_id"`
}
func (e *EnrollSecret) GetTeamID() *uint {
if e == nil {
return nil
}
return e.TeamID
}
func (e *EnrollSecret) AuthzType() string {
return "enroll_secret"
}
// ExtraAuthz implements authz.ExtraAuthzer.
func (e *EnrollSecret) ExtraAuthz() (map[string]interface{}, error) {
return map[string]interface{}{
"is_global_secret": e.TeamID == nil,
}, nil
}
// IsGlobalSecret returns whether the secret is global.
// This method is defined for the Policy Rego code (is_global_secret).
func (e *EnrollSecret) IsGlobalSecret() bool {
return e.TeamID == nil
}
const (
EnrollSecretKind = "enroll_secret"
EnrollSecretDefaultLength = 24
// Maximum number of enroll secrets that can be set per team, or globally.
// Make sure to change the documentation in docs/Contributing/reference/api-for-contributors.md
// if you change that value (look for the string `secrets`).
MaxEnrollSecretsCount = 50
)
// EnrollSecretSpec is the fleetctl spec type for enroll secrets.
type EnrollSecretSpec struct {
// Secrets is the list of enroll secrets.
Secrets []*EnrollSecret `json:"secrets"`
}
const (
// tierBasicDeprecated is for backward compatibility with previous tier names
tierBasicDeprecated = "basic"
// TierPremium is Fleet Premium aka the paid license.
TierPremium = "premium"
// TierFree is Fleet Free aka the free license.
TierFree = "free"
// TierTrial is Fleet Premium but in trial mode
// this is used to distinguish between Premium, enabling different functionality
// when the license is expired, like disabling certain features
TierTrial = "trial"
)
// Partnerships contains specialized configuration options for Fleet partners.
type Partnerships struct {
EnablePrimo bool `json:"enable_primo,omitempty"`
}
// LicenseInfo contains information about the Fleet license.
type LicenseInfo struct {
// Tier is the license tier (currently "free" or "premium")
Tier string `json:"tier"`
// Organization is the name of the licensed organization.
Organization string `json:"organization,omitempty"`
// DeviceCount is the number of licensed devices.
DeviceCount int `json:"device_count,omitempty"`
// Expiration is when the license expires.
Expiration time.Time `json:"expiration,omitempty"`
// Note is any additional terms of license
Note string `json:"note,omitempty"`
// AllowDisableTelemetry allows specific customers to not send analytics
AllowDisableTelemetry bool `json:"allow_disable_telemetry,omitempty"`
// ManagedCloud indicates whether this Fleet instance is a cloud instance.
// Currently only used to display UI features only present on cloud instances.
ManagedCloud bool `json:"managed_cloud"`
}
func (l *LicenseInfo) IsPremium() bool {
return l.Tier == TierPremium || l.Tier == tierBasicDeprecated || l.Tier == TierTrial
}
func (l *LicenseInfo) IsExpired() bool {
return l.Expiration.Before(time.Now())
}
func (l *LicenseInfo) ForceUpgrade() {
if l.Tier == tierBasicDeprecated {
l.Tier = TierPremium
}
}
// Both free and specific premium users are allowed to disable telemetry
func (l *LicenseInfo) IsAllowDisableTelemetry() bool {
return !l.IsPremium() || l.AllowDisableTelemetry
}
const (
HeaderLicenseKey = "X-Fleet-License"
HeaderLicenseValueExpired = "Expired"
)
type Logging struct {
Debug bool `json:"debug"`
Json bool `json:"json"`
Result LoggingPlugin `json:"result"`
Status LoggingPlugin `json:"status"`
Audit LoggingPlugin `json:"audit"`
}
type EmailConfig struct {
Backend string `json:"backend"`
Config interface{} `json:"config"`
}
type SESConfig struct {
Region string `json:"region"`
SourceARN string `json:"source_arn"`
}
type UpdateIntervalConfig struct {
OSQueryDetail time.Duration `json:"osquery_detail"`
OSQueryPolicy time.Duration `json:"osquery_policy"`
}
// VulnerabilitiesConfig contains the vulnerabilities configuration of the
// fleet instance (as configured for the cli, either via flags, env vars or the
// config file), not to be confused with VulnerabilitySettings which is the
// configuration in AppConfig.
type VulnerabilitiesConfig struct {
DatabasesPath string `json:"databases_path"`
Periodicity time.Duration `json:"periodicity"`
CPEDatabaseURL string `json:"cpe_database_url"`
CPETranslationsURL string `json:"cpe_translations_url"`
CVEFeedPrefixURL string `json:"cve_feed_prefix_url"`
CurrentInstanceChecks string `json:"current_instance_checks"`
DisableDataSync bool `json:"disable_data_sync"`
RecentVulnerabilityMaxAge time.Duration `json:"recent_vulnerability_max_age"`
DisableWinOSVulnerabilities bool `json:"disable_win_os_vulnerabilities"`
}
type LoggingPlugin struct {
Plugin string `json:"plugin"`
Config interface{} `json:"config"`
}
type FilesystemConfig struct {
config.FilesystemConfig
}
type WebhookConfig struct {
config.WebhookConfig
}
type PubSubConfig struct {
config.PubSubConfig
}
// FirehoseConfig shadows config.FirehoseConfig only exposing a subset of fields
type FirehoseConfig struct {
Region string `json:"region"`
StatusStream string `json:"status_stream"`
ResultStream string `json:"result_stream"`
AuditStream string `json:"audit_stream"`
}
// KinesisConfig shadows config.KinesisConfig only exposing a subset of fields
type KinesisConfig struct {
Region string `json:"region"`
StatusStream string `json:"status_stream"`
ResultStream string `json:"result_stream"`
AuditStream string `json:"audit_stream"`
}
// LambdaConfig shadows config.LambdaConfig only exposing a subset of fields
type LambdaConfig struct {
Region string `json:"region"`
StatusFunction string `json:"status_function"`
ResultFunction string `json:"result_function"`
AuditFunction string `json:"audit_function"`
}
// KafkaRESTConfig shadows config.KafkaRESTConfig
type KafkaRESTConfig struct {
StatusTopic string `json:"status_topic"`
ResultTopic string `json:"result_topic"`
AuditTopic string `json:"audit_topic"`
ProxyHost string `json:"proxyhost"`
}
// DeviceGlobalConfig is a subset of AppConfig with information used by the
// device endpoints
type DeviceGlobalConfig struct {
MDM DeviceGlobalMDMConfig `json:"mdm"`
Features DeviceFeatures `json:"features"`
}
// DeviceGlobalMDMConfig is a subset of AppConfig.MDM with information used by
// the device endpoints
type DeviceGlobalMDMConfig struct {
EnabledAndConfigured bool `json:"enabled_and_configured"`
RequireAllSoftware bool `json:"require_all_software_macos"`
}
// DeviceFeatures is a subset of AppConfig.Features with information used by
// the device endpoints.
type DeviceFeatures struct {
// EnableSoftwareInventory is the setting used by the device's team (or
// globally in the AppConfig if the device is not in any team).
EnableSoftwareInventory bool `json:"enable_software_inventory"`
}
// Version is the authz type used to check access control to the version endpoint.
type Version struct{}
// AuthzType implements authz.AuthzTyper.
func (v *Version) AuthzType() string {
return "version"
}
type WindowsSettings struct {
// NOTE: These are only present here for informational purposes.
// (The source of truth for profiles is in MySQL.)
CustomSettings optjson.Slice[MDMProfileSpec] `json:"custom_settings"`
}
func (ws WindowsSettings) GetMDMProfileSpecs() []MDMProfileSpec {
return ws.CustomSettings.Value
}
// Compile-time interface check
var _ WithMDMProfileSpecs = WindowsSettings{}
type AndroidSettings struct {
// NOTE: These are only present here for informational purposes.
// (The source of truth for profiles is in MySQL.)
CustomSettings optjson.Slice[MDMProfileSpec] `json:"custom_settings"`
}
func (ws AndroidSettings) GetMDMProfileSpecs() []MDMProfileSpec {
return ws.CustomSettings.Value
}
// Compile-time interface check
var _ WithMDMProfileSpecs = AndroidSettings{}
type YaraRuleSpec struct {
Path string `json:"path"`
}
type YaraRule struct {
Name string `json:"name"`
Contents string `json:"contents"`
}
func (r *YaraRule) Clone() (Cloner, error) {
return &YaraRule{
Name: r.Name,
Contents: r.Contents,
}, nil
}