mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
1315 lines
54 KiB
Go
1315 lines
54 KiB
Go
package fleet
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
)
|
|
|
|
type HostStatus string
|
|
|
|
const (
|
|
// StatusOnline host is active.
|
|
StatusOnline = HostStatus("online")
|
|
// StatusOffline no communication with host for OfflineDuration.
|
|
StatusOffline = HostStatus("offline")
|
|
// StatusMIA no communication with host for MIADuration.
|
|
StatusMIA = HostStatus("mia")
|
|
// StatusNew means the host has enrolled in the interval defined by
|
|
// NewDuration. It is independent of offline and online.
|
|
StatusNew = HostStatus("new")
|
|
// StatusMissing means the host is missing for 30 days. It is identical
|
|
// with StatusMIA, but StatusMIA is deprecated.
|
|
StatusMissing = HostStatus("missing")
|
|
|
|
// NewDuration if a host has been created within this time period it's
|
|
// considered new.
|
|
NewDuration = 24 * time.Hour
|
|
|
|
// MIADuration if a host hasn't been in communication for this period it
|
|
// is considered MIA.
|
|
MIADuration = 30 * 24 * time.Hour
|
|
|
|
// OnlineIntervalBuffer is the additional time in seconds to add to the
|
|
// online interval to avoid flapping of hosts that check in a bit later
|
|
// than their expected checkin interval.
|
|
OnlineIntervalBuffer = 60
|
|
|
|
// HostIdentiferNotFound is the error message returned when a search for a host by its
|
|
// identifier (hostname, UUID, or serial number) does not return any results.
|
|
HostIdentiferNotFound = "Host doesn't exist. Make sure you provide a valid hostname, UUID, or serial number. Learn more about host identifiers: https://fleetdm.com/learn-more-about/host-identifiers"
|
|
)
|
|
|
|
func (s HostStatus) IsValid() bool {
|
|
switch s {
|
|
case StatusOnline, StatusOffline, StatusNew, StatusMissing, StatusMIA:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MDMEnrollStatus defines the possible MDM enrollment statuses.
|
|
type MDMEnrollStatus string
|
|
|
|
const (
|
|
MDMEnrollStatusManual = MDMEnrollStatus("manual")
|
|
MDMEnrollStatusAutomatic = MDMEnrollStatus("automatic")
|
|
MDMEnrollStatusPending = MDMEnrollStatus("pending")
|
|
MDMEnrollStatusUnenrolled = MDMEnrollStatus("unenrolled")
|
|
MDMEnrollStatusEnrolled = MDMEnrollStatus("enrolled") // combination of "manual" and "automatic"
|
|
)
|
|
|
|
// OSSettingsStatus defines the possible statuses of the host's OS settings, which is derived from the
|
|
// status of MDM configuration profiles and non-profile settings applied the host.
|
|
type OSSettingsStatus string
|
|
|
|
const (
|
|
OSSettingsVerified OSSettingsStatus = "verified"
|
|
OSSettingsVerifying OSSettingsStatus = "verifying"
|
|
OSSettingsPending OSSettingsStatus = "pending"
|
|
OSSettingsFailed OSSettingsStatus = "failed"
|
|
)
|
|
|
|
func (s OSSettingsStatus) IsValid() bool {
|
|
switch s {
|
|
case OSSettingsFailed, OSSettingsPending, OSSettingsVerifying, OSSettingsVerified:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MDMBootstrapPackageStatus defines the possible statuses of the host's MDM bootstrap package,
|
|
// which is derived from the status of the MDM command to install the bootstrap package.
|
|
//
|
|
// See https://developer.apple.com/documentation/devicemanagement/installenterpriseapplicationresponse
|
|
type MDMBootstrapPackageStatus string
|
|
|
|
const (
|
|
// MDMBootstrapPackageInstalled means the bootstrap package has been installed on the host. It
|
|
// corresponds to InstallEnterpriseApplicationResponse.Status "Acknowledged".
|
|
MDMBootstrapPackageInstalled = MDMBootstrapPackageStatus("installed")
|
|
// MDMBootstrapPackageFailed means the bootstrap package failed to install on the host. It
|
|
// corresponds to InstallEnterpriseApplicationResponse.Status "Error".
|
|
MDMBootstrapPackageFailed = MDMBootstrapPackageStatus("failed")
|
|
// MDMBootstrapPackagePending means the bootstrap package is pending installation on the host.
|
|
// It applies if no InstallEnterpriseApplicationResponse has been received or if the response is
|
|
// anything other than InstallEnterpriseApplicationResponse.Status "Acknowledged" or "Error".
|
|
MDMBootstrapPackagePending = MDMBootstrapPackageStatus("pending")
|
|
)
|
|
|
|
func (s MDMBootstrapPackageStatus) IsValid() bool {
|
|
switch s {
|
|
case MDMBootstrapPackageInstalled, MDMBootstrapPackagePending, MDMBootstrapPackageFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// NOTE: any changes to the hosts filters is likely to impact at least the following
|
|
// endpoints, due to how they share the same implementation at the Datastore level:
|
|
//
|
|
// - GET /hosts (list hosts)
|
|
// - GET /hosts/count (count hosts, which calls svc.CountHosts or svc.CountHostsInLabel)
|
|
// - GET /labels/{id}/hosts (list hosts in label)
|
|
// - GET /hosts/report
|
|
// - POST /hosts/delete (calls svc.hostIDsFromFilters)
|
|
// - POST /hosts/transfer/filter (calls svc.hostIDsFromFilters)
|
|
//
|
|
// Make sure the docs are updated accordingly and all endpoints behave as expected.
|
|
type HostListOptions struct {
|
|
ListOptions
|
|
|
|
// DeviceMapping joins device user email mapping for each host if available
|
|
DeviceMapping bool
|
|
|
|
// AdditionalFilters selects which host additional fields should be
|
|
// populated.
|
|
AdditionalFilters []string
|
|
// StatusFilter selects the online status of the hosts.
|
|
StatusFilter HostStatus
|
|
// TeamFilter selects the hosts for specified team
|
|
TeamFilter *uint
|
|
|
|
PolicyIDFilter *uint
|
|
PolicyResponseFilter *bool
|
|
|
|
// Deprecated: SoftwareIDFilter is deprecated as of Fleet 4.42. It is
|
|
// maintained for backwards compatibility. Use SoftwareVersionIDFilter
|
|
// instead.
|
|
SoftwareIDFilter *uint
|
|
// SoftwareVersionIDFilter filters the hosts by the software version ID that
|
|
// they use. This identifies a specific version of a "software title".
|
|
SoftwareVersionIDFilter *uint
|
|
// SoftwareTitleIDFilter filers the hosts by the software title ID that they
|
|
// use. This identifies a "software title" independent of the specific
|
|
// version.
|
|
SoftwareTitleIDFilter *uint
|
|
// SoftwareStatusFilter filters the hosts by the status of the software installer, if any,
|
|
// managed by Fleet. If specified, the SoftwareTitleIDFilter must also be specified.
|
|
SoftwareStatusFilter *SoftwareInstallerStatus
|
|
|
|
OSIDFilter *uint
|
|
OSNameFilter *string
|
|
OSVersionFilter *string
|
|
OSVersionIDFilter *uint
|
|
|
|
DisableIssues bool
|
|
|
|
// MacOSSettingsFilter filters the hosts by the status of MDM configuration profiles
|
|
// applied to the hosts.
|
|
MacOSSettingsFilter OSSettingsStatus
|
|
|
|
// MacOSSettingsDiskEncryptionFilter filters the hosts by the status of the disk encryption
|
|
// MDM profile.
|
|
MacOSSettingsDiskEncryptionFilter DiskEncryptionStatus
|
|
|
|
// OSSettingsFilter filters the hosts by the status of MDM configuration profiles and
|
|
// non-profile settings applied to the hosts.
|
|
OSSettingsFilter OSSettingsStatus
|
|
// OSSettingsDiskEncryptionFilter filters the hosts by the status of the disk encryption
|
|
// OS setting.
|
|
OSSettingsDiskEncryptionFilter DiskEncryptionStatus
|
|
|
|
// MDMBootstrapPackageFilter filters the hosts by the status of the MDM bootstrap package.
|
|
MDMBootstrapPackageFilter *MDMBootstrapPackageStatus
|
|
|
|
// MDMIDFilter filters the hosts by MDM ID.
|
|
MDMIDFilter *uint
|
|
// MDMNameFilter filters the hosts by MDM solution name (e.g. one of the
|
|
// fleet.WellKnownMDM... constants).
|
|
MDMNameFilter *string
|
|
// MDMEnrollmentStatusFilter filters the host by their MDM enrollment status.
|
|
MDMEnrollmentStatusFilter MDMEnrollStatus
|
|
// MunkiIssueIDFilter filters the hosts by munki issue ID.
|
|
MunkiIssueIDFilter *uint
|
|
|
|
// LowDiskSpaceFilter filters the hosts by low disk space (defined as a host
|
|
// with less than N gigs of disk space available). Note that this is a Fleet
|
|
// Premium feature, Fleet Free ignores the setting (it forces it to nil to
|
|
// disable it).
|
|
LowDiskSpaceFilter *int
|
|
|
|
// PopulateSoftware adds the `Software` field to all Hosts returned.
|
|
PopulateSoftware bool
|
|
|
|
// PopulateSoftwareVulnerabilityDetails adds description, fix version, etc. fields to software vulnerabilities
|
|
// (this is a Premium feature that gets forced to false on Fleet Free)
|
|
PopulateSoftwareVulnerabilityDetails bool
|
|
|
|
// PopulatePolicies adds the `Policies` array field to all Hosts returned.
|
|
PopulatePolicies bool
|
|
|
|
// PopulateUsers adds the `Users` array field to all Hosts returned
|
|
PopulateUsers bool
|
|
|
|
// PopulateLabels adds the `Labels` array field to all host responses returned
|
|
PopulateLabels bool
|
|
|
|
// VulnerabilityFilter filters the hosts by the presence of a vulnerability (CVE)
|
|
VulnerabilityFilter *string
|
|
|
|
// ConnectedToFleetFilter filters hosts that have an active MDM
|
|
// connection with this Fleet instance.
|
|
ConnectedToFleetFilter *bool
|
|
}
|
|
|
|
// TODO(Sarah): Are we missing any filters here? Should all MDM filters be included?
|
|
func (h HostListOptions) Empty() bool {
|
|
return h.ListOptions.Empty() &&
|
|
!h.DeviceMapping &&
|
|
len(h.AdditionalFilters) == 0 &&
|
|
h.StatusFilter == "" &&
|
|
h.TeamFilter == nil &&
|
|
h.PolicyIDFilter == nil &&
|
|
h.PolicyResponseFilter == nil &&
|
|
h.SoftwareIDFilter == nil &&
|
|
h.SoftwareVersionIDFilter == nil &&
|
|
h.SoftwareTitleIDFilter == nil &&
|
|
h.SoftwareStatusFilter == nil &&
|
|
h.OSIDFilter == nil &&
|
|
h.OSNameFilter == nil &&
|
|
h.OSVersionFilter == nil &&
|
|
!h.DisableIssues &&
|
|
h.MacOSSettingsFilter == "" &&
|
|
h.MacOSSettingsDiskEncryptionFilter == "" &&
|
|
h.MDMBootstrapPackageFilter == nil &&
|
|
h.MDMIDFilter == nil &&
|
|
h.MDMNameFilter == nil &&
|
|
h.MDMEnrollmentStatusFilter == "" &&
|
|
h.MunkiIssueIDFilter == nil &&
|
|
h.LowDiskSpaceFilter == nil &&
|
|
h.OSSettingsFilter == "" &&
|
|
h.OSSettingsDiskEncryptionFilter == ""
|
|
}
|
|
|
|
type HostUser struct {
|
|
Uid uint `json:"uid" db:"uid"`
|
|
Username string `json:"username" db:"username"`
|
|
Type string `json:"type" db:"user_type"`
|
|
GroupName string `json:"groupname" db:"groupname"`
|
|
Shell string `json:"shell" db:"shell"`
|
|
}
|
|
|
|
type Host struct {
|
|
UpdateCreateTimestamps
|
|
HostSoftware
|
|
ID uint `json:"id" csv:"id"`
|
|
// OsqueryHostID is the key used in the request context that is
|
|
// used to retrieve host information. It is sent from osquery and may currently be
|
|
// a GUID or a Host Name, but in either case, it MUST be unique
|
|
OsqueryHostID *string `json:"-" db:"osquery_host_id" csv:"-"`
|
|
DetailUpdatedAt time.Time `json:"detail_updated_at" db:"detail_updated_at" csv:"detail_updated_at"` // Time that the host details were last updated
|
|
LabelUpdatedAt time.Time `json:"label_updated_at" db:"label_updated_at" csv:"label_updated_at"` // Time that the host labels were last updated
|
|
PolicyUpdatedAt time.Time `json:"policy_updated_at" db:"policy_updated_at" csv:"policy_updated_at"` // Time that the host policies were last updated
|
|
LastEnrolledAt time.Time `json:"last_enrolled_at" db:"last_enrolled_at" csv:"last_enrolled_at"` // Time that the host last enrolled
|
|
SeenTime time.Time `json:"seen_time" db:"seen_time" csv:"seen_time"` // Time that the host was last "seen"
|
|
RefetchRequested bool `json:"refetch_requested" db:"refetch_requested" csv:"refetch_requested"`
|
|
NodeKey *string `json:"-" db:"node_key" csv:"-"`
|
|
OrbitNodeKey *string `json:"-" db:"orbit_node_key" csv:"-"`
|
|
Hostname string `json:"hostname" db:"hostname" csv:"hostname"` // there is a fulltext index on this field
|
|
UUID string `json:"uuid" db:"uuid" csv:"uuid"` // there is a fulltext index on this field
|
|
// Platform is the host's platform as defined by osquery's os_version.platform.
|
|
Platform string `json:"platform" csv:"platform"`
|
|
OsqueryVersion string `json:"osquery_version" db:"osquery_version" csv:"osquery_version"`
|
|
OrbitVersion *string `json:"orbit_version" db:"orbit_version" csv:"orbit_version"`
|
|
DesktopVersion *string `json:"fleet_desktop_version" db:"fleet_desktop_version" csv:"fleet_desktop_version"`
|
|
ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled" csv:"scripts_enabled"`
|
|
OSVersion string `json:"os_version" db:"os_version" csv:"os_version"`
|
|
Build string `json:"build" csv:"build"`
|
|
PlatformLike string `json:"platform_like" db:"platform_like" csv:"platform_like"`
|
|
CodeName string `json:"code_name" db:"code_name" csv:"code_name"`
|
|
Uptime time.Duration `json:"uptime" csv:"uptime"`
|
|
Memory int64 `json:"memory" sql:"type:bigint" db:"memory" csv:"memory"`
|
|
// system_info fields
|
|
CPUType string `json:"cpu_type" db:"cpu_type" csv:"cpu_type"`
|
|
CPUSubtype string `json:"cpu_subtype" db:"cpu_subtype" csv:"cpu_subtype"`
|
|
CPUBrand string `json:"cpu_brand" db:"cpu_brand" csv:"cpu_brand"`
|
|
CPUPhysicalCores int `json:"cpu_physical_cores" db:"cpu_physical_cores" csv:"cpu_physical_cores"`
|
|
CPULogicalCores int `json:"cpu_logical_cores" db:"cpu_logical_cores" csv:"cpu_logical_cores"`
|
|
HardwareVendor string `json:"hardware_vendor" db:"hardware_vendor" csv:"hardware_vendor"`
|
|
HardwareModel string `json:"hardware_model" db:"hardware_model" csv:"hardware_model"`
|
|
HardwareVersion string `json:"hardware_version" db:"hardware_version" csv:"hardware_version"`
|
|
HardwareSerial string `json:"hardware_serial" db:"hardware_serial" csv:"hardware_serial"`
|
|
ComputerName string `json:"computer_name" db:"computer_name" csv:"computer_name"`
|
|
// PrimaryNetworkInterfaceID if present indicates to primary network for the host, the details of which
|
|
// can be found in the NetworkInterfaces element with the same ip_address.
|
|
PrimaryNetworkInterfaceID *uint `json:"primary_ip_id,omitempty" db:"primary_ip_id" csv:"primary_ip_id"`
|
|
NetworkInterfaces []*NetworkInterface `json:"-" db:"-" csv:"-"`
|
|
PublicIP string `json:"public_ip" db:"public_ip" csv:"public_ip"`
|
|
PrimaryIP string `json:"primary_ip" db:"primary_ip" csv:"primary_ip"`
|
|
PrimaryMac string `json:"primary_mac" db:"primary_mac" csv:"primary_mac"`
|
|
DistributedInterval uint `json:"distributed_interval" db:"distributed_interval" csv:"distributed_interval"`
|
|
ConfigTLSRefresh uint `json:"config_tls_refresh" db:"config_tls_refresh" csv:"config_tls_refresh"`
|
|
LoggerTLSPeriod uint `json:"logger_tls_period" db:"logger_tls_period" csv:"logger_tls_period"`
|
|
TeamID *uint `json:"team_id" db:"team_id" csv:"team_id"`
|
|
|
|
// Loaded via JOIN in DB
|
|
PackStats []PackStats `json:"pack_stats" csv:"-"`
|
|
// TeamName is the name of the team, loaded by JOIN to the teams table.
|
|
TeamName *string `json:"team_name" db:"team_name" csv:"team_name"`
|
|
// Additional is the additional information from the host
|
|
// additional_queries. This should be stored in a separate DB table.
|
|
Additional *json.RawMessage `json:"additional,omitempty" db:"additional" csv:"-"`
|
|
|
|
// Users currently in the host
|
|
Users []HostUser `json:"users,omitempty" csv:"-"`
|
|
|
|
GigsDiskSpaceAvailable float64 `json:"gigs_disk_space_available" db:"gigs_disk_space_available" csv:"gigs_disk_space_available"`
|
|
PercentDiskSpaceAvailable float64 `json:"percent_disk_space_available" db:"percent_disk_space_available" csv:"percent_disk_space_available"`
|
|
GigsTotalDiskSpace float64 `json:"gigs_total_disk_space" db:"gigs_total_disk_space" csv:"gigs_total_disk_space"`
|
|
|
|
// DiskEncryptionEnabled is only returned by GET /host/{id} and so is not
|
|
// exportable as CSV (which is the result of List Hosts endpoint). It is
|
|
// a *bool because for some Linux we set it to NULL and omit it from the JSON
|
|
// response if the host does not have disk encryption enabled. It is also
|
|
// omitted if we don't have encryption information yet.
|
|
DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled" csv:"-"`
|
|
|
|
HostIssues `json:"issues,omitempty" csv:"-"`
|
|
|
|
// DeviceMapping is in fact included in the CSV export, but it is not directly
|
|
// encoded from this column, it is processed before marshaling, hence why the
|
|
// struct tag here has csv:"-".
|
|
DeviceMapping *json.RawMessage `json:"device_mapping,omitempty" db:"device_mapping" csv:"-"`
|
|
|
|
MDM MDMHostData `json:"mdm" db:"mdm_host_data" csv:"-"`
|
|
|
|
// RefetchCriticalQueriesUntil can be set to a timestamp up to which the
|
|
// "critical" queries will be constantly reported to the host that checks in
|
|
// to be re-executed until a condition is met (or the timestamp expires). The
|
|
// notion of "critical query" is voluntarily loosely defined so that future
|
|
// requirements may use this mechanism. The difference with RefetchRequested
|
|
// is that the latter is a one-time request, while this one is a persistent
|
|
// until the timestamp expires. The initial use-case is to check for a host
|
|
// to be unenrolled from its old MDM solution, in the "migrate to Fleet MDM"
|
|
// workflow (both Apple and Windows).
|
|
//
|
|
// In the future, if we want to use it for more than one use-case, we could
|
|
// add a "reason" field with well-known labels so we know what condition(s)
|
|
// are expected to clear the timestamp. For now there's a single use-case
|
|
// so we don't need this.
|
|
RefetchCriticalQueriesUntil *time.Time `json:"refetch_critical_queries_until" db:"refetch_critical_queries_until" csv:"-"`
|
|
|
|
// DEPAssignedToFleet is set to true if the host is assigned to Fleet in Apple Business Manager.
|
|
// It is a *bool becase we want it to be returned from only a subset of endpoints related to
|
|
// Orbit and Fleet Desktop. Otherwise, it will be set to NULL so it is omitted from JSON
|
|
// responses.
|
|
//
|
|
// The boolean is based on information ingested from the Apple DEP API that is stored in the
|
|
// host_dep_assignments table.
|
|
DEPAssignedToFleet *bool `json:"dep_assigned_to_fleet,omitempty" db:"dep_assigned_to_fleet" csv:"-"`
|
|
|
|
// LastRestartedAt is a UNIX timestamp that indicates when the Host was last restarted.
|
|
LastRestartedAt time.Time `json:"last_restarted_at" db:"last_restarted_at" csv:"last_restarted_at"`
|
|
|
|
// Policies is the list of policies and whether it passes for the host
|
|
Policies *[]*HostPolicy `json:"policies,omitempty" csv:"-"`
|
|
}
|
|
|
|
type AndroidHost struct {
|
|
*Host
|
|
*android.Device
|
|
}
|
|
|
|
func (ah *AndroidHost) SetNodeKey(enterpriseSpecificID string) {
|
|
if ah.Host == nil || ah.Device == nil {
|
|
return
|
|
}
|
|
ah.Device.EnterpriseSpecificID = ptr.String(enterpriseSpecificID)
|
|
// We use node_key as a unique identifier for the host table row.
|
|
// Since this key is used by other hosts, we use a prefix to avoid conflicts.
|
|
hostNodeKey := "android/" + enterpriseSpecificID
|
|
ah.Host.NodeKey = &hostNodeKey
|
|
}
|
|
|
|
func (ah *AndroidHost) IsValid() bool {
|
|
return !(ah == nil || ah.Host == nil || ah.Device == nil ||
|
|
ah.Host.NodeKey == nil || ah.Device.EnterpriseSpecificID == nil ||
|
|
*ah.Host.NodeKey != "android/"+*ah.Device.EnterpriseSpecificID)
|
|
}
|
|
|
|
// HostOrbitInfo maps to the host_orbit_info table in the database, which maps to the orbit_info agent table.
|
|
type HostOrbitInfo struct {
|
|
Version string `json:"version" db:"version"`
|
|
DesktopVersion *string `json:"desktop_version" db:"desktop_version"`
|
|
ScriptsEnabled *bool `json:"scripts_enabled" db:"scripts_enabled"`
|
|
}
|
|
|
|
// HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with
|
|
// the same name, see the comments/docs for the Host field above.
|
|
type HostHealth struct {
|
|
UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"`
|
|
OsVersion string `json:"os_version,omitempty" db:"os_version"`
|
|
DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled"`
|
|
FailingPoliciesCount int `json:"failing_policies_count"`
|
|
FailingCriticalPoliciesCount *int `json:"failing_critical_policies_count,omitempty"` // Fleet Premium Only
|
|
VulnerableSoftware []HostHealthVulnerableSoftware `json:"vulnerable_software,omitempty"`
|
|
FailingPolicies []*HostHealthFailingPolicy `json:"failing_policies,omitempty"`
|
|
Platform string `json:"-" db:"platform"` // Needed to fetch failing policies. Not returned in HTTP responses.
|
|
TeamID *uint `json:"team_id,omitempty" db:"team_id"` // Needed to verify that user can access this host's health data. Not returned in HTTP responses.
|
|
}
|
|
|
|
type HostHealthVulnerableSoftware struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
type HostHealthFailingPolicy struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Critical *bool `json:"critical,omitempty"` // Fleet Premium Only
|
|
Resolution *string `json:"resolution"`
|
|
}
|
|
|
|
func (hh HostHealth) AuthzType() string {
|
|
return "host_health"
|
|
}
|
|
|
|
type MDMHostData struct {
|
|
// For CSV columns, since the CSV is flattened, we keep the "mdm." prefix
|
|
// along with the column name.
|
|
|
|
// EnrollmentStatus is a string representation of state derived from
|
|
// booleans stored in the host_mdm table, loaded by JOIN in datastore
|
|
EnrollmentStatus *string `json:"enrollment_status" db:"-" csv:"mdm.enrollment_status"`
|
|
// DEPProfileError is a boolean representing whether Fleet received a "FAILED" response when
|
|
// attempting to assign a DEP profile for the host.
|
|
// See https://developer.apple.com/documentation/devicemanagement/assignprofileresponse
|
|
//
|
|
// It is not filled in by all host-returning datastore methods.
|
|
DEPProfileError bool `json:"dep_profile_error" db:"dep_profile_error" csv:"mdm.dep_profile_error"`
|
|
// ServerURL is the server_url stored in the host_mdm table, loaded by
|
|
// JOIN in datastore
|
|
ServerURL *string `json:"server_url" db:"-" csv:"mdm.server_url"`
|
|
// Name is the name of the MDM solution for the host.
|
|
Name string `json:"name" db:"name" csv:"-"`
|
|
|
|
// EncryptionKeyAvailable indicates if Fleet was able to retrieve and
|
|
// decode an encryption key for the host.
|
|
EncryptionKeyAvailable bool `json:"encryption_key_available" db:"-" csv:"-"`
|
|
|
|
// this is set to nil if the key exists but decryptable is NULL in the db, 1
|
|
// if decryptable, 0 if non-decryptable and -1 if no disk encryption key row
|
|
// exists for this host. Used internally to determine the disk_encryption
|
|
// status and action_required fields. See MDMHostData.Scan as for where this
|
|
// gets filled.
|
|
rawDecryptable *int
|
|
|
|
// OSSettings contains information related to operating systems settings that are managed for
|
|
// MDM-enrolled hosts and/or Linux hosts with disk encryption enabled, which don't require MDM.
|
|
//
|
|
// Note: Additional information for macOS hosts is currently stored in MacOSSettings.
|
|
OSSettings *HostMDMOSSettings `json:"os_settings,omitempty" db:"-" csv:"-"`
|
|
|
|
// Profiles is a list of HostMDMProfiles for the host. Note that as for many
|
|
// other host fields, it is not filled in by all host-returning datastore methods.
|
|
//
|
|
// It is a pointer to a slice so that when set, it gets marhsaled even
|
|
// if the slice is empty, but when unset, it doesn't get marshaled
|
|
// (e.g. we don't return that information for the List Hosts endpoint).
|
|
Profiles *[]HostMDMProfile `json:"profiles,omitempty" db:"profiles" csv:"-"`
|
|
|
|
// MacOSSettings indicates macOS-specific MDM settings for the host, such
|
|
// as disk encryption status and whether any user action is required to
|
|
// complete the disk encryption process.
|
|
//
|
|
// It is not filled in by all host-returning datastore methods.
|
|
MacOSSettings *MDMHostMacOSSettings `json:"macos_settings,omitempty" db:"-" csv:"-"`
|
|
|
|
// MacOSSetup indicates macOS-specific MDM setup for the host, such
|
|
// as the status of the bootstrap package.
|
|
//
|
|
// It is not filled in by all host-returning datastore methods.
|
|
MacOSSetup *HostMDMMacOSSetup `json:"macos_setup,omitempty" db:"-" csv:"-"`
|
|
|
|
// The DeviceStatus and PendingAction fields are not stored in the database
|
|
// directly, they are read from the GetHostLockWipeStatus datastore method
|
|
// and determined from those results. They are not filled by all
|
|
// host-returning methods.
|
|
DeviceStatus *string `json:"device_status,omitempty" db:"-" csv:"-"`
|
|
PendingAction *string `json:"pending_action,omitempty" db:"-" csv:"-"`
|
|
|
|
// ConnectedToFleet indicates if the host has an active MDM connection
|
|
// with this Fleet instance. This boolean is not filled by all
|
|
// host-returning methods.
|
|
ConnectedToFleet *bool `json:"connected_to_fleet" csv:"-" db:"connected_to_fleet"`
|
|
}
|
|
|
|
type HostMDMOSSettings struct {
|
|
DiskEncryption HostMDMDiskEncryption `json:"disk_encryption" db:"-" csv:"-"`
|
|
}
|
|
|
|
type HostMDMDiskEncryption struct {
|
|
Status *DiskEncryptionStatus `json:"status" db:"-" csv:"-"`
|
|
Detail string `json:"detail" db:"-" csv:"-"`
|
|
}
|
|
|
|
type DiskEncryptionStatus string
|
|
|
|
const (
|
|
DiskEncryptionVerified DiskEncryptionStatus = "verified"
|
|
DiskEncryptionVerifying DiskEncryptionStatus = "verifying"
|
|
DiskEncryptionActionRequired DiskEncryptionStatus = "action_required"
|
|
DiskEncryptionEnforcing DiskEncryptionStatus = "enforcing"
|
|
DiskEncryptionFailed DiskEncryptionStatus = "failed"
|
|
DiskEncryptionRemovingEnforcement DiskEncryptionStatus = "removing_enforcement"
|
|
)
|
|
|
|
func (s DiskEncryptionStatus) addrOf() *DiskEncryptionStatus {
|
|
return &s
|
|
}
|
|
|
|
func (s DiskEncryptionStatus) IsValid() bool {
|
|
switch s {
|
|
case
|
|
DiskEncryptionVerifying,
|
|
DiskEncryptionVerified,
|
|
DiskEncryptionActionRequired,
|
|
DiskEncryptionEnforcing,
|
|
DiskEncryptionFailed,
|
|
DiskEncryptionRemovingEnforcement:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type ActionRequiredState string
|
|
|
|
const (
|
|
ActionRequiredLogOut ActionRequiredState = "log_out"
|
|
ActionRequiredRotateKey ActionRequiredState = "rotate_key"
|
|
)
|
|
|
|
func (s ActionRequiredState) addrOf() *ActionRequiredState {
|
|
return &s
|
|
}
|
|
|
|
type MDMHostMacOSSettings struct {
|
|
DiskEncryption *DiskEncryptionStatus `json:"disk_encryption" csv:"-"`
|
|
ActionRequired *ActionRequiredState `json:"action_required" csv:"-"`
|
|
}
|
|
|
|
type HostMDMMacOSSetup struct {
|
|
BootstrapPackageStatus MDMBootstrapPackageStatus `db:"bootstrap_package_status" json:"bootstrap_package_status" csv:"-"`
|
|
Result []byte `db:"result" json:"-" csv:"-"`
|
|
Detail string `db:"-" json:"detail" csv:"-"`
|
|
BootstrapPackageName string `db:"bootstrap_package_name" json:"bootstrap_package_name" csv:"-"`
|
|
}
|
|
|
|
// PopulateOSSettingsAndMacOSSettings populates the OSSettings and MacOSSettings
|
|
// on the MDMHostData struct. It determines the disk encryption status for the
|
|
// host based on the file-vault profile in its list of profiles and whether its
|
|
// disk encryption key is available and decryptable. The file-vault profile
|
|
// identifier is received as an argument to avoid a circular dependency.
|
|
//
|
|
// NOTE: This overwrites both OSSettings and MacOSSettings on the MDMHostData struct. Any existing
|
|
// data in those fields will be lost.
|
|
func (d *MDMHostData) PopulateOSSettingsAndMacOSSettings(profiles []HostMDMAppleProfile, fileVaultIdentifier string) {
|
|
var settings MDMHostMacOSSettings
|
|
|
|
var fvprof *HostMDMAppleProfile
|
|
for _, p := range profiles {
|
|
p := p
|
|
if p.Identifier == fileVaultIdentifier {
|
|
fvprof = &p
|
|
break
|
|
}
|
|
}
|
|
if fvprof != nil {
|
|
switch fvprof.OperationType {
|
|
case MDMOperationTypeInstall:
|
|
switch {
|
|
case fvprof.Status != nil && (*fvprof.Status == MDMDeliveryVerifying || *fvprof.Status == MDMDeliveryVerified):
|
|
if d.rawDecryptable != nil && *d.rawDecryptable == 1 { //nolint:gocritic // ignore ifElseChain
|
|
// if a FileVault profile has been successfully installed on the host
|
|
// AND we have fetched and are able to decrypt the key
|
|
switch *fvprof.Status {
|
|
case MDMDeliveryVerifying:
|
|
settings.DiskEncryption = DiskEncryptionVerifying.addrOf()
|
|
case MDMDeliveryVerified:
|
|
settings.DiskEncryption = DiskEncryptionVerified.addrOf()
|
|
}
|
|
} else if d.rawDecryptable != nil {
|
|
// if a FileVault profile has been successfully installed on the host
|
|
// but either we didn't get an encryption key or we're not able to
|
|
// decrypt the key we've got
|
|
settings.DiskEncryption = DiskEncryptionActionRequired.addrOf()
|
|
settings.ActionRequired = ActionRequiredRotateKey.addrOf()
|
|
} else {
|
|
// if [a FileVault profile is pending to be installed or] the
|
|
// matching row in host_disk_encryption_keys has a field decryptable
|
|
// = NULL
|
|
switch *fvprof.Status {
|
|
case MDMDeliveryVerifying, MDMDeliveryVerified:
|
|
settings.DiskEncryption = DiskEncryptionVerifying.addrOf()
|
|
case MDMDeliveryPending:
|
|
settings.DiskEncryption = DiskEncryptionEnforcing.addrOf()
|
|
}
|
|
}
|
|
|
|
case fvprof.Status != nil && *fvprof.Status == MDMDeliveryFailed:
|
|
// if a FileVault profile failed to be installed [or removed]
|
|
settings.DiskEncryption = DiskEncryptionFailed.addrOf()
|
|
|
|
default:
|
|
// if a FileVault profile is pending to be installed [or the matching
|
|
// row in host_disk_encryption_keys has a field decryptable = NULL]
|
|
settings.DiskEncryption = DiskEncryptionEnforcing.addrOf()
|
|
}
|
|
|
|
case MDMOperationTypeRemove:
|
|
switch {
|
|
case fvprof.Status != nil && *fvprof.Status == MDMDeliveryVerifying:
|
|
// successfully removed, same as if no filevault profile was found
|
|
|
|
case fvprof.Status != nil && *fvprof.Status == MDMDeliveryFailed:
|
|
// if a FileVault profile failed to be [installed or] removed
|
|
settings.DiskEncryption = DiskEncryptionFailed.addrOf()
|
|
|
|
default:
|
|
// if a FileVault profile is pending to be removed
|
|
settings.DiskEncryption = DiskEncryptionRemovingEnforcement.addrOf()
|
|
}
|
|
}
|
|
}
|
|
d.MacOSSettings = &settings
|
|
|
|
var hde HostMDMDiskEncryption
|
|
if settings.DiskEncryption != nil {
|
|
hde.Status = settings.DiskEncryption
|
|
}
|
|
if fvprof != nil {
|
|
hde.Detail = fvprof.Detail
|
|
}
|
|
d.OSSettings = &HostMDMOSSettings{DiskEncryption: hde}
|
|
}
|
|
|
|
func (d *MDMHostData) ProfileStatusFromDiskEncryptionState(currStatus *MDMDeliveryStatus) *MDMDeliveryStatus {
|
|
if d.MacOSSettings == nil || d.MacOSSettings.DiskEncryption == nil {
|
|
return currStatus
|
|
}
|
|
switch *d.MacOSSettings.DiskEncryption {
|
|
case DiskEncryptionActionRequired, DiskEncryptionEnforcing, DiskEncryptionRemovingEnforcement:
|
|
return &MDMDeliveryPending
|
|
case DiskEncryptionFailed:
|
|
return &MDMDeliveryFailed
|
|
case DiskEncryptionVerifying:
|
|
return &MDMDeliveryVerifying
|
|
case DiskEncryptionVerified:
|
|
return &MDMDeliveryVerified
|
|
default:
|
|
return currStatus
|
|
}
|
|
}
|
|
|
|
// Only exposed for Datastore tests, to be able to assert the rawDecryptable
|
|
// unexported field.
|
|
func (d *MDMHostData) TestGetRawDecryptable() *int {
|
|
return d.rawDecryptable
|
|
}
|
|
|
|
// Scan implements the Scanner interface for sqlx, to support unmarshaling a
|
|
// JSON object from the database into a MDMHostData struct.
|
|
func (d *MDMHostData) Scan(v interface{}) error {
|
|
var dst struct {
|
|
MDMHostData
|
|
RawDecryptable *int `json:"raw_decryptable"`
|
|
}
|
|
switch v := v.(type) {
|
|
case []byte:
|
|
if err := json.Unmarshal(v, &dst); err != nil {
|
|
return err
|
|
}
|
|
*d = dst.MDMHostData
|
|
d.rawDecryptable = dst.RawDecryptable
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported type: %T", v)
|
|
}
|
|
}
|
|
|
|
// IsOsqueryEnrolled returns true if the host is enrolled via osquery.
|
|
func (h *Host) IsOsqueryEnrolled() bool {
|
|
return h.OsqueryHostID != nil && *h.OsqueryHostID != ""
|
|
}
|
|
|
|
// IsDEPAssignedToFleet returns true if the host was assigned to the Fleet
|
|
// server in ABM.
|
|
func (h *Host) IsDEPAssignedToFleet() bool {
|
|
return h.DEPAssignedToFleet != nil && *h.DEPAssignedToFleet
|
|
}
|
|
|
|
// IsLUKSSupported returns true if the host's platform is Linux and running
|
|
// one of the supported OS versions.
|
|
func (h *Host) IsLUKSSupported() bool {
|
|
return h.Platform == "ubuntu" || strings.Contains(h.OSVersion, "Fedora") // fedora h.Platform reports as "rhel"
|
|
}
|
|
|
|
// IsEligibleForWindowsMDMUnenrollment returns true if the host must be
|
|
// unenrolled from Fleet's Windows MDM (if it MDM was disabled).
|
|
func (h *Host) IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM bool) bool {
|
|
return h.FleetPlatform() == "windows" &&
|
|
h.IsOsqueryEnrolled() &&
|
|
isConnectedToFleetMDM
|
|
}
|
|
|
|
// HostDisplayName returns ComputerName if it isn't empty. Otherwise, it returns Hostname if it isn't
|
|
// empty. If Hostname is empty and both HardwareSerial and HardwareModel are not empty, it returns a
|
|
// composite string with HardwareModel and HardwareSerial. If all else fails, it returns an empty
|
|
// string.
|
|
func HostDisplayName(computerName string, hostname string, hardwareModel string, hardwareSerial string) string {
|
|
switch {
|
|
case computerName != "":
|
|
return computerName
|
|
case hostname != "":
|
|
return hostname
|
|
case hardwareModel != "" && hardwareSerial != "":
|
|
return fmt.Sprintf("%s (%s)", hardwareModel, hardwareSerial)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (h *Host) DisplayName() string {
|
|
return HostDisplayName(h.ComputerName, h.Hostname, h.HardwareModel, h.HardwareSerial)
|
|
}
|
|
|
|
type HostIssues struct {
|
|
FailingPoliciesCount uint64 `json:"failing_policies_count" db:"failing_policies_count" csv:"-"`
|
|
CriticalVulnerabilitiesCount *uint64 `json:"critical_vulnerabilities_count,omitempty" db:"critical_vulnerabilities_count" csv:"-"` // We set it to nil if the license is not premium
|
|
TotalIssuesCount uint64 `json:"total_issues_count" db:"total_issues_count" csv:"issues"` // when exporting in CSV, we want that value as the "issues" column
|
|
}
|
|
|
|
func (h Host) AuthzType() string {
|
|
return "host"
|
|
}
|
|
|
|
// HostDetail provides the full host metadata along with associated labels and
|
|
// packs. It also includes policies, batteries, maintenance window, and MDM profiles, as applicable.
|
|
type HostDetail struct {
|
|
Host
|
|
// Labels is the list of labels the host is a member of.
|
|
Labels []*Label `json:"labels"`
|
|
// Packs is the list of packs the host is a member of.
|
|
Packs []*Pack `json:"packs"`
|
|
// Batteries is the list of batteries for the host. It is a pointer to a
|
|
// slice so that when set, it gets marhsaled even if the slice is empty,
|
|
// but when unset, it doesn't get marshaled (e.g. we don't return that
|
|
// information for the List Hosts endpoint).
|
|
Batteries *[]*HostBattery `json:"batteries,omitempty"`
|
|
|
|
// MaintenanceWindow contains the host user's calendar IANA timezone and the start time of the next scheduled maintenance window.
|
|
MaintenanceWindow *HostMaintenanceWindow `json:"maintenance_window,omitempty"`
|
|
}
|
|
|
|
type HostMaintenanceWindow struct {
|
|
// StartsAt is the start time of the future maintenance window, retrieved from calendar_events,
|
|
// represented as a time.Time in the host's associated google calendar user's timezone, which is represented as a time.Location
|
|
StartsAt time.Time `json:"starts_at" db:"start_time"`
|
|
// TimeZone is the IANA timezone of the user's google calendar, retrieved from calendar_events
|
|
TimeZone *string `json:"timezone" db:"timezone"`
|
|
}
|
|
|
|
const (
|
|
HostKind = "host"
|
|
)
|
|
|
|
// HostSummary is a structure which represents a data summary about the total
|
|
// set of hosts in the database. This structure is returned by the HostService
|
|
// method GetHostSummary
|
|
type HostSummary struct {
|
|
TeamID *uint `json:"team_id,omitempty" db:"-"`
|
|
TotalsHostsCount uint `json:"totals_hosts_count" db:"total"`
|
|
OnlineCount uint `json:"online_count" db:"online"`
|
|
OfflineCount uint `json:"offline_count" db:"offline"`
|
|
MIACount uint `json:"mia_count" db:"mia"`
|
|
Missing30DaysCount uint `json:"missing_30_days_count" db:"missing_30_days_count"`
|
|
NewCount uint `json:"new_count" db:"new"`
|
|
AllLinuxCount uint `json:"all_linux_count" db:"-"`
|
|
LowDiskSpaceCount *uint `json:"low_disk_space_count,omitempty" db:"low_disk_space"`
|
|
BuiltinLabels []*LabelSummary `json:"builtin_labels" db:"-"`
|
|
Platforms []*HostSummaryPlatform `json:"platforms" db:"-"`
|
|
}
|
|
|
|
// HostSummaryPlatform represents the hosts statistics for a given platform,
|
|
// as returned inside the HostSummary struct by the GetHostSummary service.
|
|
type HostSummaryPlatform struct {
|
|
Platform string `json:"platform" db:"platform"`
|
|
HostsCount uint `json:"hosts_count" db:"total"`
|
|
}
|
|
|
|
// Status calculates the online status of the host
|
|
func (h *Host) Status(now time.Time) HostStatus {
|
|
// The logic in this function should remain synchronized with
|
|
// GenerateHostStatusStatistics and CountHostsInTargets
|
|
// NOTE: As of Fleet 4.15 StatusMIA is deprecated and will be removed in Fleet 5.0
|
|
|
|
onlineInterval := h.ConfigTLSRefresh
|
|
if h.DistributedInterval < h.ConfigTLSRefresh {
|
|
onlineInterval = h.DistributedInterval
|
|
}
|
|
|
|
// Add a small buffer to prevent flapping
|
|
onlineInterval += OnlineIntervalBuffer
|
|
|
|
switch {
|
|
case h.SeenTime.Add(time.Duration(onlineInterval) * time.Second).Before(now): //nolint:gosec // dismiss G115
|
|
return StatusOffline
|
|
default:
|
|
return StatusOnline
|
|
}
|
|
}
|
|
|
|
func (h *Host) IsNew(now time.Time) bool {
|
|
withDuration := h.CreatedAt.Add(NewDuration)
|
|
if withDuration.After(now) ||
|
|
withDuration.Equal(now) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// FleetPlatform returns the host's generic platform as supported by Fleet.
|
|
func (h *Host) FleetPlatform() string {
|
|
return PlatformFromHost(h.Platform)
|
|
}
|
|
|
|
// SupportsOsquery returns whether the device runs osquery.
|
|
func (h *Host) SupportsOsquery() bool {
|
|
return h.Platform != "ios" && h.Platform != "ipados" && h.Platform != "android"
|
|
}
|
|
|
|
// HostLinuxOSs are the possible linux values for Host.Platform.
|
|
var HostLinuxOSs = []string{
|
|
"linux", "ubuntu", "debian", "rhel", "centos", "sles", "kali", "gentoo", "amzn", "pop", "arch", "linuxmint", "void", "nixos", "endeavouros", "manjaro", "opensuse-leap", "opensuse-tumbleweed", "tuxedo",
|
|
}
|
|
|
|
func IsLinux(hostPlatform string) bool {
|
|
for _, linuxPlatform := range HostLinuxOSs {
|
|
if linuxPlatform == hostPlatform {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func IsUnixLike(hostPlatform string) bool {
|
|
unixLikeOSs := HostLinuxOSs
|
|
unixLikeOSs = append(unixLikeOSs, "darwin")
|
|
for _, p := range unixLikeOSs {
|
|
if p == hostPlatform {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PlatformFromHost converts the given host platform into
|
|
// the generic platforms known by osquery
|
|
// https://osquery.readthedocs.io/en/stable/deployment/configuration/
|
|
// and supported by Fleet.
|
|
//
|
|
// Returns empty string if hostPlatform is unknnown.
|
|
func PlatformFromHost(hostPlatform string) string {
|
|
switch {
|
|
case IsLinux(hostPlatform):
|
|
return "linux"
|
|
case hostPlatform == "darwin", hostPlatform == "windows",
|
|
// Some customers have custom agents that support ChromeOS
|
|
// TODO remove this once that customer migrates to Fleetd for Chrome
|
|
hostPlatform == "CrOS",
|
|
// Fleet now supports Chrome via fleetd
|
|
hostPlatform == "chrome",
|
|
hostPlatform == "ios",
|
|
hostPlatform == "ipados",
|
|
hostPlatform == "android":
|
|
return hostPlatform
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// ExpandPlatform returns the list of platforms corresponding to the (possibly
|
|
// generic) platform provided. For example, "linux" expands to all the platform
|
|
// identifiers considered to be linux, while "debian" returns only "debian",
|
|
// "windows" => "windows", etc.
|
|
func ExpandPlatform(platform string) []string {
|
|
switch platform {
|
|
case "linux":
|
|
// return a copy to make sure the caller cannot modify the slice
|
|
linuxOSs := make([]string, len(HostLinuxOSs))
|
|
copy(linuxOSs, HostLinuxOSs)
|
|
return linuxOSs
|
|
default:
|
|
return []string{platform}
|
|
}
|
|
}
|
|
|
|
// List of valid sources for HostDeviceMapping (host_emails table in the
|
|
// database).
|
|
const (
|
|
DeviceMappingGoogleChromeProfiles = "google_chrome_profiles"
|
|
DeviceMappingMDMIdpAccounts = "mdm_idp_accounts"
|
|
DeviceMappingCustomInstaller = "custom_installer" // set by fleetd via device-authenticated API
|
|
DeviceMappingCustomOverride = "custom_override" // set by user via user-authenticated API
|
|
|
|
DeviceMappingCustomPrefix = "custom_" // if host_emails.source starts with this, replace with DeviceMappingCustomReplacement
|
|
DeviceMappingCustomReplacement = "custom" // replaces a source that starts with CustomPrefix - in the UI, we want to display those as only "custom"
|
|
)
|
|
|
|
// HostDeviceMapping represents a mapping of a user email address to a host,
|
|
// as reported by the specified source (e.g. Google Chrome Profiles).
|
|
type HostDeviceMapping struct {
|
|
ID uint `json:"-" db:"id"`
|
|
HostID uint `json:"-" db:"host_id"`
|
|
Email string `json:"email" db:"email"`
|
|
Source string `json:"source" db:"source"`
|
|
}
|
|
|
|
type HostMunkiInfo struct {
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// HostMDM represents a host_mdm row, with information about the MDM solution
|
|
// used by a host. Note that it uses a different JSON representation than its
|
|
// struct - it implements a custom JSON marshaler.
|
|
type HostMDM struct {
|
|
HostID uint `db:"host_id" json:"-" csv:"-"`
|
|
Enrolled bool `db:"enrolled" json:"-" csv:"-"`
|
|
ServerURL string `db:"server_url" json:"-" csv:"-"`
|
|
InstalledFromDep bool `db:"installed_from_dep" json:"-" csv:"-"`
|
|
IsServer bool `db:"is_server" json:"-" csv:"-"`
|
|
MDMID *uint `db:"mdm_id" json:"-" csv:"-"`
|
|
Name string `db:"name" json:"-" csv:"-"`
|
|
DEPProfileAssignStatus *string `db:"dep_profile_assign_status" json:"-" csv:"-"`
|
|
}
|
|
|
|
// HasJSONProfileAssigned returns true if Fleet has assigned an ADE/DEP JSON
|
|
// profile to the host, and it'll be enrolled into Fleet the next time the host
|
|
// performs automatic enrollment.
|
|
func (h *HostMDM) HasJSONProfileAssigned() bool {
|
|
// TODO: get rid of h != nil with a better solution once we stablish
|
|
// the pattern for dealing with a nil HostMDM
|
|
return h != nil &&
|
|
h.DEPProfileAssignStatus != nil &&
|
|
*h.DEPProfileAssignStatus == string(DEPAssignProfileResponseSuccess)
|
|
}
|
|
|
|
// HostMunkiIssue represents a single munki issue for a host.
|
|
type HostMunkiIssue struct {
|
|
MunkiIssueID uint `db:"munki_issue_id" json:"id"`
|
|
Name string `db:"name" json:"name"`
|
|
IssueType string `db:"issue_type" json:"type"`
|
|
HostIssueCreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
}
|
|
|
|
// List of well-known MDM solution names. Those correspond to names stored in
|
|
// the mobile_device_management_solutions table.
|
|
const (
|
|
UnknownMDMName = ""
|
|
WellKnownMDMKandji = "Kandji"
|
|
WellKnownMDMJamf = "Jamf"
|
|
WellKnownMDMJumpCloud = "JumpCloud"
|
|
WellKnownMDMVMWare = "VMware Workspace ONE"
|
|
WellKnownMDMIntune = "Intune"
|
|
WellKnownMDMSimpleMDM = "SimpleMDM"
|
|
WellKnownMDMFleet = "Fleet"
|
|
)
|
|
|
|
var mdmNameFromServerURLChecks = map[string]string{
|
|
"kandji": WellKnownMDMKandji,
|
|
"jamf": WellKnownMDMJamf,
|
|
"jumpcloud": WellKnownMDMJumpCloud,
|
|
"airwatch": WellKnownMDMVMWare,
|
|
"awmdm": WellKnownMDMVMWare,
|
|
"microsoft": WellKnownMDMIntune,
|
|
"simplemdm": WellKnownMDMSimpleMDM,
|
|
"fleetdm": WellKnownMDMFleet,
|
|
}
|
|
|
|
// MDMNameFromServerURL returns the MDM solution name corresponding to the
|
|
// given server URL. If no match is found, it returns the unknown MDM name.
|
|
func MDMNameFromServerURL(serverURL string) string {
|
|
serverURL = strings.ToLower(serverURL)
|
|
|
|
for check, name := range mdmNameFromServerURLChecks {
|
|
if strings.Contains(serverURL, check) {
|
|
return name
|
|
}
|
|
}
|
|
return UnknownMDMName
|
|
}
|
|
|
|
func (h *HostMDM) EnrollmentStatus() string {
|
|
switch {
|
|
case h.Enrolled && !h.InstalledFromDep:
|
|
return "On (manual)"
|
|
case h.Enrolled && h.InstalledFromDep:
|
|
return "On (automatic)"
|
|
case !h.Enrolled && h.InstalledFromDep:
|
|
return "Pending"
|
|
default:
|
|
return "Off"
|
|
}
|
|
}
|
|
|
|
func (h *HostMDM) MarshalJSON() ([]byte, error) {
|
|
if h == nil {
|
|
return []byte("null"), nil
|
|
}
|
|
if h.IsServer {
|
|
return []byte("null"), nil
|
|
}
|
|
var jsonMDM struct {
|
|
EnrollmentStatus string `json:"enrollment_status"`
|
|
ServerURL string `json:"server_url"`
|
|
Name string `json:"name,omitempty"`
|
|
ID *uint `json:"id,omitempty"`
|
|
}
|
|
|
|
jsonMDM.ServerURL = h.ServerURL
|
|
jsonMDM.EnrollmentStatus = h.EnrollmentStatus()
|
|
jsonMDM.Name = h.Name
|
|
jsonMDM.ID = h.MDMID
|
|
return json.Marshal(jsonMDM)
|
|
}
|
|
|
|
func (h *HostMDM) UnmarshalJSON(b []byte) error {
|
|
// fail attempts to unmarshal in this struct, to prevent using e.g.
|
|
// getMacadminsDataResponse in tests, as it can't unmarshal in a meaningful
|
|
// way.
|
|
return errors.New("JSON unmarshaling is not supported for HostMDM")
|
|
}
|
|
|
|
// HostBattery represents a host's battery, as reported by the osquery battery
|
|
// table.
|
|
type HostBattery struct {
|
|
ID uint `json:"-" db:"id"`
|
|
HostID uint `json:"-" db:"host_id"`
|
|
SerialNumber string `json:"-" db:"serial_number"`
|
|
CycleCount int `json:"cycle_count" db:"cycle_count"`
|
|
Health string `json:"health" db:"health"`
|
|
}
|
|
|
|
type MacadminsData struct {
|
|
Munki *HostMunkiInfo `json:"munki"`
|
|
MDM *HostMDM `json:"mobile_device_management"`
|
|
MunkiIssues []*HostMunkiIssue `json:"munki_issues"`
|
|
}
|
|
|
|
type AggregatedMunkiVersion struct {
|
|
HostMunkiInfo
|
|
HostsCount int `json:"hosts_count" db:"hosts_count"`
|
|
}
|
|
|
|
// MunkiIssue represents a single munki issue, as returned by the list hosts
|
|
// endpoint when a muniki issue ID is provided as filter.
|
|
type MunkiIssue struct {
|
|
ID uint `json:"id" db:"id"`
|
|
Name string `json:"name" db:"name"`
|
|
IssueType string `json:"type" db:"issue_type"`
|
|
}
|
|
|
|
type AggregatedMunkiIssue struct {
|
|
MunkiIssue
|
|
HostsCount int `json:"hosts_count" db:"hosts_count"`
|
|
}
|
|
|
|
type AggregatedMDMStatus struct {
|
|
EnrolledManualHostsCount int `json:"enrolled_manual_hosts_count" db:"enrolled_manual_hosts_count"`
|
|
EnrolledAutomatedHostsCount int `json:"enrolled_automated_hosts_count" db:"enrolled_automated_hosts_count"`
|
|
PendingHostsCount int `json:"pending_hosts_count" db:"pending_hosts_count"`
|
|
UnenrolledHostsCount int `json:"unenrolled_hosts_count" db:"unenrolled_hosts_count"`
|
|
HostsCount int `json:"hosts_count" db:"hosts_count"`
|
|
}
|
|
|
|
// AggregatedMDMData contains aggregated data from mdm installations.
|
|
type AggregatedMDMData struct {
|
|
CountsUpdatedAt time.Time `json:"counts_updated_at"`
|
|
MDMStatus AggregatedMDMStatus `json:"mobile_device_management_enrollment_status"`
|
|
MDMSolutions []AggregatedMDMSolutions `json:"mobile_device_management_solution"`
|
|
}
|
|
|
|
// MDMSolution represents a single MDM solution, as returned by the list hosts
|
|
// endpoint when an MDM Solution ID is provided as filter.
|
|
type MDMSolution struct {
|
|
ID uint `json:"id" db:"id"`
|
|
Name string `json:"name" db:"name"`
|
|
ServerURL string `json:"server_url" db:"server_url"`
|
|
}
|
|
|
|
type AggregatedMDMSolutions struct {
|
|
MDMSolution
|
|
HostsCount int `json:"hosts_count" db:"hosts_count"`
|
|
}
|
|
|
|
type AggregatedMacadminsData struct {
|
|
CountsUpdatedAt time.Time `json:"counts_updated_at"`
|
|
MunkiVersions []AggregatedMunkiVersion `json:"munki_versions"`
|
|
MunkiIssues []AggregatedMunkiIssue `json:"munki_issues"`
|
|
MDMStatus AggregatedMDMStatus `json:"mobile_device_management_enrollment_status"`
|
|
MDMSolutions []AggregatedMDMSolutions `json:"mobile_device_management_solution"`
|
|
}
|
|
|
|
// HostVulnerabilitySummary type used with webhooks and third-party vulnerability automations.
|
|
// Contains all pertinent host info plus the installed paths of all affected software.
|
|
type HostVulnerabilitySummary struct {
|
|
// ID Is the ID of the host
|
|
ID uint `json:"id" db:"id"`
|
|
// Hostname the host's hostname
|
|
Hostname string `json:"hostname" db:"hostname"`
|
|
// DisplayName either the 'computer_name' or the 'host_name' (whatever is not empty)
|
|
DisplayName string `json:"display_name" db:"display_name"`
|
|
// SoftwareInstalledPaths paths of vulnerable software installed on the host.
|
|
SoftwareInstalledPaths []string `json:"software_installed_paths,omitempty" db:"software_installed_paths"`
|
|
}
|
|
|
|
func (hvs *HostVulnerabilitySummary) AddSoftwareInstalledPath(p string) {
|
|
if p != "" {
|
|
hvs.SoftwareInstalledPaths = append(hvs.SoftwareInstalledPaths, p)
|
|
}
|
|
}
|
|
|
|
type OSVersions struct {
|
|
CountsUpdatedAt time.Time `json:"counts_updated_at"`
|
|
OSVersions []OSVersion `json:"os_versions"`
|
|
}
|
|
|
|
type VulnerableOS struct {
|
|
OSVersion
|
|
ResolvedInVersion *string `json:"resolved_in_version"`
|
|
}
|
|
|
|
type OSVersion struct {
|
|
// ID is the unique id of the operating system.
|
|
ID uint `json:"id,omitempty"`
|
|
// OSVersionID is a uniqe NameOnly/Version combination for the operating system.
|
|
OSVersionID uint `json:"os_version_id"`
|
|
// HostsCount is the number of hosts that have reported the operating system.
|
|
HostsCount int `json:"hosts_count"`
|
|
// Name is the name and alphanumeric version of the operating system. e.g., "Microsoft Windows 11 Enterprise",
|
|
// "Ubuntu", or "macOS". NOTE: In Fleet 5.0, this field will no longer include the alphanumeric version.
|
|
Name string `json:"name"`
|
|
// NameOnly is the name of the operating system, e.g., "Microsoft Windows 11 Enterprise",
|
|
// "Ubuntu", or "macOS". NOTE: In Fleet 5.0, this field be removed.
|
|
NameOnly string `json:"name_only"`
|
|
// Version is the alphanumeric version of the operating system, e.g., "21H2", "20.4.0", or "12.5".
|
|
Version string `json:"version"`
|
|
// Platform is the platform of the operating system, e.g., "windows", "ubuntu", or "darwin".
|
|
Platform string `json:"platform"`
|
|
// GeneratedCPE is the Common Platform Enumeration (CPE) name for the operating system.
|
|
// It is currently only generated for Operating Systems scanned for vulnerabilities
|
|
// in NVD (macOS only)
|
|
GeneratedCPEs []string `json:"generated_cpes,omitempty"`
|
|
// Vulnerabilities are the vulnerabilities associated with the operating system.
|
|
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
|
|
}
|
|
|
|
type HostDetailOptions struct {
|
|
IncludeCVEScores bool
|
|
IncludeCriticalVulnerabilitiesCount bool
|
|
IncludePolicies bool
|
|
ExcludeSoftware bool
|
|
}
|
|
|
|
// EnrollHostLimiter defines the methods to support enforcement of enrolled
|
|
// hosts limit, as defined by the user's license.
|
|
type EnrollHostLimiter interface {
|
|
CanEnrollNewHost(ctx context.Context) (ok bool, err error)
|
|
SyncEnrolledHostIDs(ctx context.Context) error
|
|
}
|
|
|
|
type HostMDMCheckinInfo struct {
|
|
HardwareSerial string `json:"hardware_serial" db:"hardware_serial"`
|
|
InstalledFromDEP bool `json:"installed_from_dep" db:"installed_from_dep"`
|
|
DisplayName string `json:"display_name" db:"display_name"`
|
|
TeamID uint `json:"team_id" db:"team_id"`
|
|
DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"`
|
|
OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"`
|
|
|
|
SCEPRenewalInProgress bool `json:"-" db:"scep_renewal_in_progress"`
|
|
Platform string `json:"-" db:"platform"`
|
|
}
|
|
|
|
type HostDiskEncryptionKey struct {
|
|
HostID uint `json:"-" db:"host_id"`
|
|
Base64Encrypted string `json:"-" db:"base64_encrypted"`
|
|
Decryptable *bool `json:"-" db:"decryptable"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
DecryptedValue string `json:"key" db:"-"`
|
|
ClientError string `json:"-" db:"client_error"`
|
|
}
|
|
|
|
// HostSoftwareInstalledPath represents where in the file system a software on a host was installed
|
|
type HostSoftwareInstalledPath struct {
|
|
// ID row id
|
|
ID uint `db:"id"`
|
|
// HostID is the id of the host where the software in question is installed
|
|
HostID uint `db:"host_id"`
|
|
// SoftwareID is the id of the software
|
|
SoftwareID uint `db:"software_id"`
|
|
// InstalledPath is the file system path where the software is installed
|
|
InstalledPath string `db:"installed_path"`
|
|
// TeamIdentifier (not to be confused with Fleet's team IDs) is the Apple's "Team ID" (aka "Developer ID"
|
|
// or "Signing ID") of signed applications, see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id.
|
|
TeamIdentifier string `db:"team_identifier"`
|
|
}
|
|
|
|
// HostMacOSProfile represents a macOS profile installed on a host as reported by the macos_profiles
|
|
// table of the macadmins oquery extension
|
|
type HostMacOSProfile struct {
|
|
// DisplayName is the display name of the profile.
|
|
DisplayName string `json:"display_name" db:"display_name"`
|
|
// Identifier is the identifier of the profile.
|
|
Identifier string `json:"identifier" db:"identifier"`
|
|
// InstallDate is the date the profile was installed on the host as reported by the host's clock.
|
|
InstallDate time.Time `json:"install_date" db:"install_date"`
|
|
}
|
|
|
|
// HostLite contains a subset of Host fields.
|
|
type HostLite struct {
|
|
ID uint `db:"id"`
|
|
TeamID *uint `db:"team_id"`
|
|
Hostname string `db:"hostname"`
|
|
OsqueryHostID string `db:"osquery_host_id"`
|
|
NodeKey string `db:"node_key"`
|
|
UUID string `db:"uuid"`
|
|
HardwareSerial string `db:"hardware_serial"`
|
|
SeenTime time.Time `db:"seen_time"`
|
|
DistributedInterval uint `db:"distributed_interval"`
|
|
ConfigTLSRefresh uint `db:"config_tls_refresh"`
|
|
}
|
|
|
|
// IsEligibleForDEPMigration returns true if the host fulfills all requirements
|
|
// for DEP migration from a third-party provider into Fleet.
|
|
func IsEligibleForDEPMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetMDM bool) bool {
|
|
return mdmInfo != nil &&
|
|
host.IsOsqueryEnrolled() &&
|
|
host.IsDEPAssignedToFleet() &&
|
|
mdmInfo.HasJSONProfileAssigned() &&
|
|
mdmInfo.Enrolled &&
|
|
// as a special case for migration with user interaction, we
|
|
// also check the information stored in host_mdm, and assume
|
|
// the host needs migration if it's not Fleet
|
|
//
|
|
// this is because we can't always rely on nano setting
|
|
// `nano_enrollment.active = 1` since sometimes Fleet won't get
|
|
// the checkout message from the host.
|
|
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet)
|
|
}
|
|
|
|
var macOSADEMigrationOnlyLastVersion = semver.MustParse("14")
|
|
|
|
// IsEligibleForManualMigration returns true if the host is manually enrolled into a 3rd party MDM
|
|
// and is able to migrate to Fleet.
|
|
func IsEligibleForManualMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetMDM bool) (bool, error) {
|
|
goodVersion, err := IsMacOSMajorVersionOK(host)
|
|
if err != nil {
|
|
return false, fmt.Errorf("checking macOS version for manual migration eligibility: %w", err)
|
|
}
|
|
|
|
return goodVersion &&
|
|
host.IsOsqueryEnrolled() &&
|
|
!host.IsDEPAssignedToFleet() &&
|
|
mdmInfo != nil &&
|
|
!mdmInfo.InstalledFromDep &&
|
|
!mdmInfo.HasJSONProfileAssigned() &&
|
|
mdmInfo.Enrolled &&
|
|
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet), nil
|
|
}
|
|
|
|
func IsMacOSMajorVersionOK(host *Host) (bool, error) {
|
|
if host == nil {
|
|
return false, nil
|
|
}
|
|
|
|
parts := strings.Split(host.OSVersion, " ")
|
|
|
|
if len(parts) < 2 || parts[0] != "macOS" {
|
|
return false, nil
|
|
}
|
|
|
|
version, err := VersionToSemverVersion(parts[1])
|
|
if err != nil {
|
|
return false, fmt.Errorf("parsing macOS version \"%s\": %w", parts[1], err)
|
|
}
|
|
|
|
if version.GreaterThan(macOSADEMigrationOnlyLastVersion) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|