mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 16:08:47 +00:00
1331 lines
42 KiB
Go
1331 lines
42 KiB
Go
package osquery_utils
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/service/async"
|
|
"github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
type DetailQuery struct {
|
|
// Query is the SQL query string.
|
|
Query string
|
|
// Discovery is the SQL query that defines whether the query will run on the host or not.
|
|
// If not set, Fleet makes sure the query will always run.
|
|
Discovery string
|
|
// Platforms is a list of platforms to run the query on. If this value is
|
|
// empty, run on all platforms.
|
|
Platforms []string
|
|
// IngestFunc translates a query result into an update to the host struct,
|
|
// around data that lives on the hosts table.
|
|
IngestFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error
|
|
// DirectIngestFunc gathers results from a query and directly works with the datastore to
|
|
// persist them. This is usually used for host data that is stored in a separate table.
|
|
// DirectTaskIngestFunc must not be set if this is set.
|
|
DirectIngestFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error
|
|
// DirectTaskIngestFunc is similar to DirectIngestFunc except that it uses a task to
|
|
// ingest the results. This is for ingestion that can be either sync or async.
|
|
// DirectIngestFunc must not be set if this is set.
|
|
DirectTaskIngestFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, task *async.Task, rows []map[string]string) error
|
|
}
|
|
|
|
// RunsForPlatform determines whether this detail query should run on the given platform
|
|
func (q *DetailQuery) RunsForPlatform(platform string) bool {
|
|
if len(q.Platforms) == 0 {
|
|
return true
|
|
}
|
|
for _, p := range q.Platforms {
|
|
if p == platform {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hostDetailQueries defines the detail queries that should be run on the host, as
|
|
// well as how the results of those queries should be ingested into the
|
|
// fleet.Host data model (via IngestFunc).
|
|
//
|
|
// This map should not be modified at runtime.
|
|
var hostDetailQueries = map[string]DetailQuery{
|
|
"network_interface_unix": {
|
|
Query: `
|
|
select
|
|
ia.address,
|
|
id.mac
|
|
from
|
|
interface_addresses ia
|
|
join interface_details id on id.interface = ia.interface
|
|
join routes r on r.interface = ia.interface
|
|
where
|
|
r.destination = '0.0.0.0'
|
|
and r.netmask = 0
|
|
and r.type = 'gateway'
|
|
and instr(ia.address, '.') > 0
|
|
order by
|
|
r.metric asc
|
|
limit 1
|
|
`,
|
|
Platforms: append(fleet.HostLinuxOSs, "darwin"),
|
|
IngestFunc: ingestNetworkInterface,
|
|
},
|
|
"network_interface_windows": {
|
|
Query: `
|
|
select
|
|
ia.address,
|
|
id.mac
|
|
from
|
|
interface_addresses ia
|
|
join interface_details id on id.interface = ia.interface
|
|
join routes r on r.interface = ia.address
|
|
where
|
|
r.destination = '0.0.0.0'
|
|
and r.netmask = 0
|
|
and r.type = 'remote'
|
|
and instr(ia.address, '.') > 0
|
|
order by
|
|
r.metric asc
|
|
limit 1
|
|
`,
|
|
Platforms: []string{"windows"},
|
|
IngestFunc: ingestNetworkInterface,
|
|
},
|
|
"os_version": {
|
|
// Collect operating system information for the `hosts` table.
|
|
// Note that data for `operating_system` and `host_operating_system` tables are ingested via
|
|
// the `os_unix_like` extra detail query below.
|
|
Query: "SELECT * FROM os_version LIMIT 1",
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_os_version expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
if build, ok := rows[0]["build"]; ok {
|
|
host.Build = build
|
|
}
|
|
|
|
host.Platform = rows[0]["platform"]
|
|
host.PlatformLike = rows[0]["platform_like"]
|
|
host.CodeName = rows[0]["codename"]
|
|
|
|
// On centos6 there is an osquery bug that leaves
|
|
// platform empty. Here we workaround.
|
|
if host.Platform == "" &&
|
|
strings.Contains(strings.ToLower(rows[0]["name"]), "centos") {
|
|
host.Platform = "centos"
|
|
}
|
|
|
|
if host.Platform != "windows" {
|
|
// Populate `host.OSVersion` for non-Windows hosts.
|
|
// Note Windows-specific registry query is required to populate `host.OSVersion` for
|
|
// Windows that is handled in `os_version_windows` detail query below.
|
|
host.OSVersion = fmt.Sprintf("%v %v", rows[0]["name"], parseOSVersion(
|
|
rows[0]["name"],
|
|
rows[0]["version"],
|
|
rows[0]["major"],
|
|
rows[0]["minor"],
|
|
rows[0]["patch"],
|
|
rows[0]["build"],
|
|
))
|
|
}
|
|
|
|
return nil
|
|
},
|
|
},
|
|
"os_version_windows": {
|
|
// Windows-specific registry query is required to populate `host.OSVersion` for Windows.
|
|
Query: `
|
|
SELECT
|
|
os.name,
|
|
os.codename as display_version
|
|
|
|
FROM
|
|
os_version os`,
|
|
Platforms: []string{"windows"},
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_os_version_windows expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
version := rows[0]["display_version"]
|
|
if version == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "unable to identify windows version",
|
|
"host", host.Hostname,
|
|
)
|
|
}
|
|
|
|
s := fmt.Sprintf("%v %v", rows[0]["name"], version)
|
|
// Shorten "Microsoft Windows" to "Windows" to facilitate display and sorting in UI
|
|
s = strings.Replace(s, "Microsoft Windows", "Windows", 1)
|
|
host.OSVersion = s
|
|
|
|
return nil
|
|
},
|
|
},
|
|
"osquery_flags": {
|
|
// Collect the interval info (used for online status
|
|
// calculation) from the osquery flags. We typically control
|
|
// distributed_interval (but it's not required), and typically
|
|
// do not control config_tls_refresh.
|
|
Query: `select name, value from osquery_flags where name in ("distributed_interval", "config_tls_refresh", "config_refresh", "logger_tls_period")`,
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
var configTLSRefresh, configRefresh uint
|
|
var configRefreshSeen, configTLSRefreshSeen bool
|
|
for _, row := range rows {
|
|
switch row["name"] {
|
|
|
|
case "distributed_interval":
|
|
interval, err := strconv.Atoi(EmptyToZero(row["value"]))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing distributed_interval: %w", err)
|
|
}
|
|
host.DistributedInterval = uint(interval)
|
|
|
|
case "config_tls_refresh":
|
|
// Prior to osquery 2.4.6, the flag was
|
|
// called `config_tls_refresh`.
|
|
interval, err := strconv.Atoi(EmptyToZero(row["value"]))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing config_tls_refresh: %w", err)
|
|
}
|
|
configTLSRefresh = uint(interval)
|
|
configTLSRefreshSeen = true
|
|
|
|
case "config_refresh":
|
|
// After 2.4.6 `config_tls_refresh` was
|
|
// aliased to `config_refresh`.
|
|
interval, err := strconv.Atoi(EmptyToZero(row["value"]))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing config_refresh: %w", err)
|
|
}
|
|
configRefresh = uint(interval)
|
|
configRefreshSeen = true
|
|
|
|
case "logger_tls_period":
|
|
interval, err := strconv.Atoi(EmptyToZero(row["value"]))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing logger_tls_period: %w", err)
|
|
}
|
|
host.LoggerTLSPeriod = uint(interval)
|
|
}
|
|
}
|
|
|
|
// Since the `config_refresh` flag existed prior to
|
|
// 2.4.6 and had a different meaning, we prefer
|
|
// `config_tls_refresh` if it was set, and use
|
|
// `config_refresh` as a fallback.
|
|
if configTLSRefreshSeen {
|
|
host.ConfigTLSRefresh = configTLSRefresh
|
|
} else if configRefreshSeen {
|
|
host.ConfigTLSRefresh = configRefresh
|
|
}
|
|
|
|
return nil
|
|
},
|
|
},
|
|
"osquery_info": {
|
|
Query: "select * from osquery_info limit 1",
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_osquery_info expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
host.OsqueryVersion = rows[0]["version"]
|
|
|
|
return nil
|
|
},
|
|
},
|
|
"system_info": {
|
|
Query: "select * from system_info limit 1",
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_system_info expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
host.Memory, err = strconv.ParseInt(EmptyToZero(rows[0]["physical_memory"]), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.Hostname = rows[0]["hostname"]
|
|
host.UUID = rows[0]["uuid"]
|
|
host.CPUType = rows[0]["cpu_type"]
|
|
host.CPUSubtype = rows[0]["cpu_subtype"]
|
|
host.CPUBrand = rows[0]["cpu_brand"]
|
|
host.CPUPhysicalCores, err = strconv.Atoi(EmptyToZero(rows[0]["cpu_physical_cores"]))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.CPULogicalCores, err = strconv.Atoi(EmptyToZero(rows[0]["cpu_logical_cores"]))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.HardwareVendor = rows[0]["hardware_vendor"]
|
|
host.HardwareModel = rows[0]["hardware_model"]
|
|
host.HardwareVersion = rows[0]["hardware_version"]
|
|
host.HardwareSerial = rows[0]["hardware_serial"]
|
|
host.ComputerName = rows[0]["computer_name"]
|
|
return nil
|
|
},
|
|
},
|
|
"uptime": {
|
|
Query: "select * from uptime limit 1",
|
|
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_uptime expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
uptimeSeconds, err := strconv.Atoi(EmptyToZero(rows[0]["total_seconds"]))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
host.Uptime = time.Duration(uptimeSeconds) * time.Second
|
|
|
|
return nil
|
|
},
|
|
},
|
|
"disk_space_unix": {
|
|
Query: `
|
|
SELECT (blocks_available * 100 / blocks) AS percent_disk_space_available,
|
|
round((blocks_available * blocks_size *10e-10),2) AS gigs_disk_space_available
|
|
FROM mounts WHERE path = '/' LIMIT 1;`,
|
|
Platforms: append(fleet.HostLinuxOSs, "darwin"),
|
|
DirectIngestFunc: directIngestDiskSpace,
|
|
},
|
|
"disk_space_windows": {
|
|
Query: `
|
|
SELECT ROUND((sum(free_space) * 100 * 10e-10) / (sum(size) * 10e-10)) AS percent_disk_space_available,
|
|
ROUND(sum(free_space) * 10e-10) AS gigs_disk_space_available
|
|
FROM logical_drives WHERE file_system = 'NTFS' LIMIT 1;`,
|
|
Platforms: []string{"windows"},
|
|
DirectIngestFunc: directIngestDiskSpace,
|
|
},
|
|
"kubequery_info": {
|
|
Query: `SELECT * from kubernetes_info`,
|
|
IngestFunc: ingestKubequeryInfo,
|
|
Discovery: discoveryTable("kubernetes_info"),
|
|
},
|
|
}
|
|
|
|
func ingestNetworkInterface(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "IngestFunc", "err",
|
|
fmt.Sprintf("detail_query_network_interface expected single result, got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
host.PrimaryIP = rows[0]["address"]
|
|
host.PrimaryMac = rows[0]["mac"]
|
|
host.PublicIP = publicip.FromContext(ctx)
|
|
return nil
|
|
}
|
|
|
|
func directIngestDiskSpace(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
logger.Log("component", "service", "method", "directIngestDiskSpace", "err",
|
|
fmt.Sprintf("detail_query_disk_space expected single result got %d", len(rows)))
|
|
return nil
|
|
}
|
|
|
|
gigsAvailable, err := strconv.ParseFloat(EmptyToZero(rows[0]["gigs_disk_space_available"]), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
percentAvailable, err := strconv.ParseFloat(EmptyToZero(rows[0]["percent_disk_space_available"]), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ds.SetOrUpdateHostDisksSpace(ctx, host.ID, gigsAvailable, percentAvailable)
|
|
}
|
|
|
|
func ingestKubequeryInfo(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
return fmt.Errorf("kubernetes_info expected single result got: %d", len(rows))
|
|
}
|
|
|
|
host.Hostname = fmt.Sprintf("kubequery %s", rows[0]["cluster_name"])
|
|
|
|
// These values are not provided by kubequery
|
|
host.OsqueryVersion = "kubequery"
|
|
host.Platform = "kubequery"
|
|
return nil
|
|
}
|
|
|
|
// extraDetailQueries defines extra detail queries that should be run on the host, as
|
|
// well as how the results of those queries should be ingested into the hosts related tables
|
|
// (via DirectIngestFunc).
|
|
//
|
|
// This map should not be modified at runtime.
|
|
var extraDetailQueries = map[string]DetailQuery{
|
|
"mdm": {
|
|
Query: `select enrolled, server_url, installed_from_dep, payload_identifier from mdm;`,
|
|
DirectIngestFunc: directIngestMDMMac,
|
|
Platforms: []string{"darwin"},
|
|
Discovery: discoveryTable("mdm"),
|
|
},
|
|
"mdm_windows": {
|
|
Query: `
|
|
SELECT * FROM (
|
|
SELECT "provider_id" AS "key", data as "value" FROM registry
|
|
WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Microsoft\Enrollments\%\ProviderID'
|
|
LIMIT 1
|
|
)
|
|
UNION ALL
|
|
SELECT * FROM (
|
|
SELECT "discovery_service_url" AS "key", data as "value" FROM registry
|
|
WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Microsoft\Enrollments\%\DiscoveryServiceFullURL'
|
|
LIMIT 1
|
|
)
|
|
UNION ALL
|
|
SELECT * FROM (
|
|
SELECT "autopilot" AS "key", 1=1 AS "value" FROM registry
|
|
WHERE path = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\AutopilotPolicyCache'
|
|
LIMIT 1
|
|
)
|
|
UNION ALL
|
|
SELECT * FROM (
|
|
SELECT "installation_type" AS "key", data as "value" FROM registry
|
|
WHERE path = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallationType'
|
|
LIMIT 1
|
|
)
|
|
;
|
|
`,
|
|
DirectIngestFunc: directIngestMDMWindows,
|
|
Platforms: []string{"windows"},
|
|
},
|
|
"munki_info": {
|
|
Query: `select version, errors, warnings from munki_info;`,
|
|
DirectIngestFunc: directIngestMunkiInfo,
|
|
Platforms: []string{"darwin"},
|
|
Discovery: discoveryTable("munki_info"),
|
|
},
|
|
"google_chrome_profiles": {
|
|
Query: `SELECT email FROM google_chrome_profiles WHERE NOT ephemeral AND email <> ''`,
|
|
DirectIngestFunc: directIngestChromeProfiles,
|
|
Discovery: discoveryTable("google_chrome_profiles"),
|
|
},
|
|
"battery": {
|
|
Query: `SELECT serial_number, cycle_count, health FROM battery;`,
|
|
Platforms: []string{"darwin"},
|
|
DirectIngestFunc: directIngestBattery,
|
|
// the "battery" table doesn't need a Discovery query as it is an official
|
|
// osquery table on darwin (https://osquery.io/schema/5.3.0#battery), it is
|
|
// always present.
|
|
},
|
|
"os_windows": {
|
|
// This query is used to populate the `operating_systems` and `host_operating_system`
|
|
// tables. Separately, the `hosts` table is populated via the `os_version` and
|
|
// `os_version_windows` detail queries above.
|
|
Query: `
|
|
SELECT
|
|
os.name,
|
|
os.platform,
|
|
os.arch,
|
|
k.version as kernel_version,
|
|
os.codename as display_version
|
|
|
|
FROM
|
|
os_version os,
|
|
kernel_info k`,
|
|
Platforms: []string{"windows"},
|
|
DirectIngestFunc: directIngestOSWindows,
|
|
},
|
|
"os_unix_like": {
|
|
// This query is used to populate the `operating_systems` and `host_operating_system`
|
|
// tables. Separately, the `hosts` table is populated via the `os_version` detail
|
|
// query above.
|
|
Query: `
|
|
SELECT
|
|
os.name,
|
|
os.major,
|
|
os.minor,
|
|
os.patch,
|
|
os.build,
|
|
os.arch,
|
|
os.platform,
|
|
os.version AS version,
|
|
k.version AS kernel_version
|
|
FROM
|
|
os_version os,
|
|
kernel_info k`,
|
|
Platforms: append(fleet.HostLinuxOSs, "darwin"),
|
|
DirectIngestFunc: directIngestOSUnixLike,
|
|
},
|
|
"orbit_info": {
|
|
Query: `SELECT version FROM orbit_info`,
|
|
DirectIngestFunc: directIngestOrbitInfo,
|
|
Discovery: discoveryTable("orbit_info"),
|
|
},
|
|
"disk_encryption_darwin": {
|
|
Query: `SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT "" AND filevault_status = 'on' LIMIT 1;`,
|
|
Platforms: []string{"darwin"},
|
|
DirectIngestFunc: directIngestDiskEncryption,
|
|
// the "disk_encryption" table doesn't need a Discovery query as it is an official
|
|
// osquery table on darwin and linux, it is always present.
|
|
},
|
|
"disk_encryption_linux": {
|
|
// This query doesn't do any filtering as we've seen what's possibly an osquery bug because it's returning bad
|
|
// results if we filter further, so we'll do the filtering in Go.
|
|
Query: `SELECT de.encrypted, m.path FROM disk_encryption de JOIN mounts m ON m.device_alias = de.name;`,
|
|
Platforms: fleet.HostLinuxOSs,
|
|
DirectIngestFunc: directIngestDiskEncryptionLinux,
|
|
// the "disk_encryption" table doesn't need a Discovery query as it is an official
|
|
// osquery table on darwin and linux, it is always present.
|
|
},
|
|
"disk_encryption_windows": {
|
|
Query: `SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1;`,
|
|
Platforms: []string{"windows"},
|
|
DirectIngestFunc: directIngestDiskEncryption,
|
|
// the "bitlocker_info" table doesn't need a Discovery query as it is an official
|
|
// osquery table on windows, it is always present.
|
|
},
|
|
}
|
|
|
|
// mdmQueries are used by the Fleet server to compliment certain MDM
|
|
// features.
|
|
// They are only sent to the device when Fleet's MDM is on and properly
|
|
// configured
|
|
var mdmQueries = map[string]DetailQuery{
|
|
"mdm_disk_encryption_key_darwin": {
|
|
// This query has two pre-requisites:
|
|
//
|
|
// 1. FileVault must be enabled with a personal recovery key.
|
|
// 2. The "FileVault Recovery Key Escrow" profile must be configured
|
|
// in the host.
|
|
//
|
|
// This file is safe to access and well [documented by Apple][1]:
|
|
//
|
|
// > If FileVault is enabled after this payload is installed on the system,
|
|
// > the FileVault PRK will be encrypted with the specified certificate,
|
|
// > wrapped with a CMS envelope and stored at /var/db/FileVaultPRK.dat. The
|
|
// > encrypted data will be made available to the MDM server as part of the
|
|
// > SecurityInfo command.
|
|
// >
|
|
// > Alternatively, if a site uses its own administration
|
|
// > software, it can extract the PRK from the foregoing
|
|
// > location at any time.
|
|
//
|
|
// [1]: https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow
|
|
Query: `SELECT to_base64(group_concat(line)) as filevault_key FROM file_lines WHERE path='/var/db/FileVaultPRK.dat'`,
|
|
Platforms: []string{"darwin"},
|
|
DirectIngestFunc: directIngestDiskEncryptionKeyDarwin,
|
|
Discovery: discoveryTable("file_lines"),
|
|
},
|
|
}
|
|
|
|
// discoveryTable returns a query to determine whether a table exists or not.
|
|
func discoveryTable(tableName string) string {
|
|
return fmt.Sprintf("SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = '%s';", tableName)
|
|
}
|
|
|
|
const usersQueryStr = `WITH cached_groups AS (select * from groups)
|
|
SELECT uid, username, type, groupname, shell
|
|
FROM users LEFT JOIN cached_groups USING (gid)
|
|
WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')`
|
|
|
|
func withCachedUsers(query string) string {
|
|
return fmt.Sprintf(query, usersQueryStr)
|
|
}
|
|
|
|
var windowsUpdateHistory = DetailQuery{
|
|
Query: `SELECT date, title FROM windows_update_history WHERE result_code = 'Succeeded'`,
|
|
Platforms: []string{"windows"},
|
|
Discovery: discoveryTable("windows_update_history"),
|
|
DirectIngestFunc: directIngestWindowsUpdateHistory,
|
|
}
|
|
|
|
var softwareMacOS = DetailQuery{
|
|
// Note that we create the cached_users CTE (the WITH clause) in order to suggest to SQLite
|
|
// that it generates the users once instead of once for each UNIONed query. We use CROSS JOIN to
|
|
// ensure that the nested loops in the query generation are ordered correctly for the _extensions
|
|
// tables that need a uid parameter. CROSS JOIN ensures that SQLite does not reorder the loop
|
|
// nesting, which is important as described in https://youtu.be/hcn3HIcHAAo?t=77.
|
|
Query: withCachedUsers(`WITH cached_users AS (%s)
|
|
SELECT
|
|
name AS name,
|
|
bundle_short_version AS version,
|
|
'Application (macOS)' AS type,
|
|
bundle_identifier AS bundle_identifier,
|
|
'apps' AS source,
|
|
last_opened_time AS last_opened_at
|
|
FROM apps
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Python)' AS type,
|
|
'' AS bundle_identifier,
|
|
'python_packages' AS source,
|
|
0 AS last_opened_at
|
|
FROM python_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Chrome)' AS type,
|
|
'' AS bundle_identifier,
|
|
'chrome_extensions' AS source,
|
|
0 AS last_opened_at
|
|
FROM cached_users CROSS JOIN chrome_extensions USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Firefox)' AS type,
|
|
'' AS bundle_identifier,
|
|
'firefox_addons' AS source,
|
|
0 AS last_opened_at
|
|
FROM cached_users CROSS JOIN firefox_addons USING (uid)
|
|
UNION
|
|
SELECT
|
|
name As name,
|
|
version AS version,
|
|
'Browser plugin (Safari)' AS type,
|
|
'' AS bundle_identifier,
|
|
'safari_extensions' AS source,
|
|
0 AS last_opened_at
|
|
FROM cached_users CROSS JOIN safari_extensions USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Atom)' AS type,
|
|
'' AS bundle_identifier,
|
|
'atom_packages' AS source,
|
|
0 AS last_opened_at
|
|
FROM cached_users CROSS JOIN atom_packages USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Homebrew)' AS type,
|
|
'' AS bundle_identifier,
|
|
'homebrew_packages' AS source,
|
|
0 AS last_opened_at
|
|
FROM homebrew_packages;
|
|
`),
|
|
Platforms: []string{"darwin"},
|
|
DirectIngestFunc: directIngestSoftware,
|
|
}
|
|
|
|
var scheduledQueryStats = DetailQuery{
|
|
Query: `
|
|
SELECT *,
|
|
(SELECT value from osquery_flags where name = 'pack_delimiter') AS delimiter
|
|
FROM osquery_schedule`,
|
|
DirectTaskIngestFunc: directIngestScheduledQueryStats,
|
|
}
|
|
|
|
var softwareLinux = DetailQuery{
|
|
Query: withCachedUsers(`WITH cached_users AS (%s)
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (deb)' AS type,
|
|
'deb_packages' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM deb_packages
|
|
WHERE status = 'install ok installed'
|
|
UNION
|
|
SELECT
|
|
package AS name,
|
|
version AS version,
|
|
'Package (Portage)' AS type,
|
|
'portage_packages' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM portage_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (RPM)' AS type,
|
|
'rpm_packages' AS source,
|
|
release AS release,
|
|
vendor AS vendor,
|
|
arch AS arch
|
|
FROM rpm_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (NPM)' AS type,
|
|
'npm_packages' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM npm_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Chrome)' AS type,
|
|
'chrome_extensions' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM cached_users CROSS JOIN chrome_extensions USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Firefox)' AS type,
|
|
'firefox_addons' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM cached_users CROSS JOIN firefox_addons USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Atom)' AS type,
|
|
'atom_packages' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM cached_users CROSS JOIN atom_packages USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Python)' AS type,
|
|
'python_packages' AS source,
|
|
'' AS release,
|
|
'' AS vendor,
|
|
'' AS arch
|
|
FROM python_packages;
|
|
`),
|
|
Platforms: fleet.HostLinuxOSs,
|
|
DirectIngestFunc: directIngestSoftware,
|
|
}
|
|
|
|
var softwareWindows = DetailQuery{
|
|
Query: withCachedUsers(`WITH cached_users AS (%s)
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Program (Windows)' AS type,
|
|
'programs' AS source,
|
|
publisher AS vendor
|
|
FROM programs
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Python)' AS type,
|
|
'python_packages' AS source,
|
|
'' AS vendor
|
|
FROM python_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (IE)' AS type,
|
|
'ie_extensions' AS source,
|
|
'' AS vendor
|
|
FROM ie_extensions
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Chrome)' AS type,
|
|
'chrome_extensions' AS source,
|
|
'' AS vendor
|
|
FROM cached_users CROSS JOIN chrome_extensions USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Browser plugin (Firefox)' AS type,
|
|
'firefox_addons' AS source,
|
|
'' AS vendor
|
|
FROM cached_users CROSS JOIN firefox_addons USING (uid)
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Chocolatey)' AS type,
|
|
'chocolatey_packages' AS source,
|
|
'' AS vendor
|
|
FROM chocolatey_packages
|
|
UNION
|
|
SELECT
|
|
name AS name,
|
|
version AS version,
|
|
'Package (Atom)' AS type,
|
|
'atom_packages' AS source,
|
|
'' AS vendor
|
|
FROM cached_users CROSS JOIN atom_packages USING (uid);
|
|
`),
|
|
Platforms: []string{"windows"},
|
|
DirectIngestFunc: directIngestSoftware,
|
|
}
|
|
|
|
var usersQuery = DetailQuery{
|
|
// Note we use the cached_groups CTE (`WITH` clause) here to suggest to SQLite that it generate
|
|
// the `groups` table only once. Without doing this, on some Windows systems (Domain Controllers)
|
|
// with many user accounts and groups, this query could be very expensive as the `groups` table
|
|
// was generated once for each user.
|
|
Query: usersQueryStr,
|
|
DirectIngestFunc: directIngestUsers,
|
|
}
|
|
|
|
// directIngestOrbitInfo ingests data from the orbit_info extension table.
|
|
func directIngestOrbitInfo(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
return ctxerr.Errorf(ctx, "directIngestOrbitInfo invalid number of rows: %d", len(rows))
|
|
}
|
|
version := rows[0]["version"]
|
|
if err := ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, version); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "directIngestOrbitInfo update host orbit info")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// directIngestOSWindows ingests selected operating system data from a host on a Windows platform
|
|
func directIngestOSWindows(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
return ctxerr.Errorf(ctx, "directIngestOSWindows invalid number of rows: %d", len(rows))
|
|
}
|
|
|
|
hostOS := fleet.OperatingSystem{
|
|
Name: rows[0]["name"],
|
|
Arch: rows[0]["arch"],
|
|
KernelVersion: rows[0]["kernel_version"],
|
|
Platform: rows[0]["platform"],
|
|
}
|
|
|
|
version := rows[0]["display_version"]
|
|
if version == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "unable to identify windows version",
|
|
"host", host.Hostname,
|
|
)
|
|
}
|
|
hostOS.Version = version
|
|
|
|
if err := ds.UpdateHostOperatingSystem(ctx, host.ID, hostOS); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "directIngestOSWindows update host operating system")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// directIngestOSUnixLike ingests selected operating system data from a host on a Unix-like platform
|
|
// (e.g., darwin or linux operating systems)
|
|
func directIngestOSUnixLike(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) != 1 {
|
|
return ctxerr.Errorf(ctx, "directIngestOSUnixLike invalid number of rows: %d", len(rows))
|
|
}
|
|
name := rows[0]["name"]
|
|
version := rows[0]["version"]
|
|
major := rows[0]["major"]
|
|
minor := rows[0]["minor"]
|
|
patch := rows[0]["patch"]
|
|
build := rows[0]["build"]
|
|
arch := rows[0]["arch"]
|
|
kernelVersion := rows[0]["kernel_version"]
|
|
platform := rows[0]["platform"]
|
|
|
|
hostOS := fleet.OperatingSystem{Name: name, Arch: arch, KernelVersion: kernelVersion, Platform: platform}
|
|
hostOS.Version = parseOSVersion(name, version, major, minor, patch, build)
|
|
|
|
if err := ds.UpdateHostOperatingSystem(ctx, host.ID, hostOS); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "directIngestOSUnixLike update host operating system")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseOSVersion returns a point release string for an operating system. Parsing rules
|
|
// depend on available data, which varies between operating systems.
|
|
func parseOSVersion(name string, version string, major string, minor string, patch string, build string) string {
|
|
var osVersion string
|
|
if strings.Contains(strings.ToLower(name), "ubuntu") {
|
|
// Ubuntu takes a different approach to updating patch IDs so we instead use
|
|
// the version string provided after removing the code name.
|
|
regx := regexp.MustCompile(`\(.*\)`)
|
|
osVersion = strings.TrimSpace(regx.ReplaceAllString(version, ""))
|
|
} else if major != "0" || minor != "0" || patch != "0" {
|
|
osVersion = fmt.Sprintf("%s.%s.%s", major, minor, patch)
|
|
} else {
|
|
osVersion = build
|
|
}
|
|
osVersion = strings.Trim(osVersion, ".")
|
|
|
|
return osVersion
|
|
}
|
|
|
|
func directIngestChromeProfiles(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
mapping := make([]*fleet.HostDeviceMapping, 0, len(rows))
|
|
for _, row := range rows {
|
|
mapping = append(mapping, &fleet.HostDeviceMapping{
|
|
HostID: host.ID,
|
|
Email: row["email"],
|
|
Source: "google_chrome_profiles",
|
|
})
|
|
}
|
|
return ds.ReplaceHostDeviceMapping(ctx, host.ID, mapping)
|
|
}
|
|
|
|
func directIngestBattery(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
mapping := make([]*fleet.HostBattery, 0, len(rows))
|
|
for _, row := range rows {
|
|
cycleCount, err := strconv.ParseInt(EmptyToZero(row["cycle_count"]), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mapping = append(mapping, &fleet.HostBattery{
|
|
HostID: host.ID,
|
|
SerialNumber: row["serial_number"],
|
|
CycleCount: int(cycleCount),
|
|
// database type is VARCHAR(40) and since there isn't a
|
|
// canonical list of strings we can get for health, we
|
|
// truncate the value just in case.
|
|
Health: fmt.Sprintf("%.40s", row["health"]),
|
|
})
|
|
}
|
|
return ds.ReplaceHostBatteries(ctx, host.ID, mapping)
|
|
}
|
|
|
|
func directIngestWindowsUpdateHistory(
|
|
ctx context.Context,
|
|
logger log.Logger,
|
|
host *fleet.Host,
|
|
ds fleet.Datastore,
|
|
rows []map[string]string,
|
|
) error {
|
|
// The windows update history table will also contain entries for the Defender Antivirus. Unfortunately
|
|
// there's no reliable way to differentiate between those entries and Cumulative OS updates.
|
|
// Since each antivirus update will have the same KB ID, but different 'dates', to
|
|
// avoid trying to insert duplicated data, we group by KB ID and then take the most 'out of
|
|
// date' update in each group.
|
|
|
|
uniq := make(map[uint]fleet.WindowsUpdate)
|
|
for _, row := range rows {
|
|
u, err := fleet.NewWindowsUpdate(row["title"], row["date"])
|
|
if err != nil {
|
|
level.Warn(logger).Log("op", "directIngestWindowsUpdateHistory", "skipped", err)
|
|
continue
|
|
}
|
|
|
|
if v, ok := uniq[u.KBID]; !ok || v.MoreRecent(u) {
|
|
uniq[u.KBID] = u
|
|
}
|
|
}
|
|
|
|
var updates []fleet.WindowsUpdate
|
|
for _, v := range uniq {
|
|
updates = append(updates, v)
|
|
}
|
|
|
|
return ds.InsertWindowsUpdates(ctx, host.ID, updates)
|
|
}
|
|
|
|
func directIngestScheduledQueryStats(ctx context.Context, logger log.Logger, host *fleet.Host, task *async.Task, rows []map[string]string) error {
|
|
packs := map[string][]fleet.ScheduledQueryStats{}
|
|
for _, row := range rows {
|
|
providedName := row["name"]
|
|
if providedName == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "host reported scheduled query with empty name",
|
|
"host", host.Hostname,
|
|
)
|
|
continue
|
|
}
|
|
delimiter := row["delimiter"]
|
|
if delimiter == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "host reported scheduled query with empty delimiter",
|
|
"host", host.Hostname,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Split with a limit of 2 in case query name includes the
|
|
// delimiter. Not much we can do if pack name includes the
|
|
// delimiter.
|
|
trimmedName := strings.TrimPrefix(providedName, "pack"+delimiter)
|
|
parts := strings.SplitN(trimmedName, delimiter, 2)
|
|
if len(parts) != 2 {
|
|
level.Debug(logger).Log(
|
|
"msg", "could not split pack and query names",
|
|
"host", host.Hostname,
|
|
"name", providedName,
|
|
"delimiter", delimiter,
|
|
)
|
|
continue
|
|
}
|
|
packName, scheduledName := parts[0], parts[1]
|
|
|
|
stats := fleet.ScheduledQueryStats{
|
|
ScheduledQueryName: scheduledName,
|
|
PackName: packName,
|
|
AverageMemory: cast.ToInt(row["average_memory"]),
|
|
Denylisted: cast.ToBool(row["denylisted"]),
|
|
Executions: cast.ToInt(row["executions"]),
|
|
Interval: cast.ToInt(row["interval"]),
|
|
// Cast to int first to allow cast.ToTime to interpret the unix timestamp.
|
|
LastExecuted: time.Unix(cast.ToInt64(row["last_executed"]), 0).UTC(),
|
|
OutputSize: cast.ToInt(row["output_size"]),
|
|
SystemTime: cast.ToInt(row["system_time"]),
|
|
UserTime: cast.ToInt(row["user_time"]),
|
|
WallTime: cast.ToInt(row["wall_time"]),
|
|
}
|
|
packs[packName] = append(packs[packName], stats)
|
|
}
|
|
|
|
packStats := []fleet.PackStats{}
|
|
for packName, stats := range packs {
|
|
packStats = append(
|
|
packStats,
|
|
fleet.PackStats{
|
|
PackName: packName,
|
|
QueryStats: stats,
|
|
},
|
|
)
|
|
}
|
|
if err := task.RecordScheduledQueryStats(ctx, host.ID, packStats, time.Now()); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "record host pack stats")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
var software []fleet.Software
|
|
for _, row := range rows {
|
|
name := row["name"]
|
|
version := row["version"]
|
|
source := row["source"]
|
|
bundleIdentifier := row["bundle_identifier"]
|
|
vendor := row["vendor"]
|
|
|
|
if name == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "host reported software with empty name",
|
|
"host", host.Hostname,
|
|
"version", version,
|
|
"source", source,
|
|
)
|
|
continue
|
|
}
|
|
if source == "" {
|
|
level.Debug(logger).Log(
|
|
"msg", "host reported software with empty name",
|
|
"host", host.Hostname,
|
|
"version", version,
|
|
"name", name,
|
|
)
|
|
continue
|
|
}
|
|
|
|
var lastOpenedAt time.Time
|
|
if lastOpenedRaw := row["last_opened_at"]; lastOpenedRaw != "" {
|
|
if lastOpenedEpoch, err := strconv.ParseFloat(lastOpenedRaw, 64); err != nil {
|
|
level.Debug(logger).Log(
|
|
"msg", "host reported software with invalid last opened timestamp",
|
|
"host", host.Hostname,
|
|
"version", version,
|
|
"name", name,
|
|
"last_opened_at", lastOpenedRaw,
|
|
)
|
|
} else if lastOpenedEpoch > 0 {
|
|
lastOpenedAt = time.Unix(int64(lastOpenedEpoch), 0).UTC()
|
|
}
|
|
}
|
|
|
|
// Check whether the vendor is longer than the max allowed width and if so, truncate it.
|
|
if utf8.RuneCountInString(vendor) >= fleet.SoftwareVendorMaxLength {
|
|
vendor = fmt.Sprintf(fleet.SoftwareVendorMaxLengthFmt, vendor)
|
|
}
|
|
|
|
s := fleet.Software{
|
|
Name: name,
|
|
Version: version,
|
|
Source: source,
|
|
BundleIdentifier: bundleIdentifier,
|
|
|
|
Release: row["release"],
|
|
Vendor: vendor,
|
|
Arch: row["arch"],
|
|
}
|
|
if !lastOpenedAt.IsZero() {
|
|
s.LastOpenedAt = &lastOpenedAt
|
|
}
|
|
software = append(software, s)
|
|
}
|
|
|
|
if err := ds.UpdateHostSoftware(ctx, host.ID, software); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update host software")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func directIngestUsers(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
var users []fleet.HostUser
|
|
for _, row := range rows {
|
|
uid, err := strconv.Atoi(row["uid"])
|
|
if err != nil {
|
|
return fmt.Errorf("converting uid %s to int: %w", row["uid"], err)
|
|
}
|
|
username := row["username"]
|
|
type_ := row["type"]
|
|
groupname := row["groupname"]
|
|
shell := row["shell"]
|
|
u := fleet.HostUser{
|
|
Uid: uint(uid),
|
|
Username: username,
|
|
Type: type_,
|
|
GroupName: groupname,
|
|
Shell: shell,
|
|
}
|
|
users = append(users, u)
|
|
}
|
|
if len(users) == 0 {
|
|
return nil
|
|
}
|
|
if err := ds.SaveHostUsers(ctx, host.ID, users); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update host users")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func directIngestMDMMac(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) == 0 {
|
|
// assume the extension is not there
|
|
return nil
|
|
}
|
|
if len(rows) > 1 {
|
|
logger.Log("component", "service", "method", "ingestMDM", "warn",
|
|
fmt.Sprintf("mdm expected single result got %d", len(rows)))
|
|
}
|
|
enrolledVal := rows[0]["enrolled"]
|
|
if enrolledVal == "" {
|
|
return ctxerr.Wrap(ctx, fmt.Errorf("missing mdm.enrolled value: %d", host.ID))
|
|
}
|
|
enrolled, err := strconv.ParseBool(enrolledVal)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "parsing enrolled")
|
|
}
|
|
installedFromDepVal := rows[0]["installed_from_dep"]
|
|
installedFromDep := false
|
|
if installedFromDepVal != "" {
|
|
installedFromDep, err = strconv.ParseBool(installedFromDepVal)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "parsing installed_from_dep")
|
|
}
|
|
}
|
|
|
|
return ds.SetOrUpdateMDMData(ctx,
|
|
host.ID,
|
|
false,
|
|
enrolled,
|
|
rows[0]["server_url"],
|
|
installedFromDep,
|
|
deduceMDMNameMacOS(rows[0]),
|
|
)
|
|
}
|
|
|
|
func deduceMDMNameMacOS(row map[string]string) string {
|
|
// If the PayloadIdentifier is Fleet's MDM then use Fleet as name of the MDM solution.
|
|
// (For Fleet MDM we cannot use the URL because Fleet can be deployed On-Prem.)
|
|
if payloadIdentifier := row["payload_identifier"]; payloadIdentifier == apple_mdm.FleetPayloadIdentifier {
|
|
return fleet.WellKnownMDMFleet
|
|
}
|
|
return fleet.MDMNameFromServerURL(row["server_url"])
|
|
}
|
|
|
|
func deduceMDMNameWindows(data map[string]string) string {
|
|
if name := data["provider_id"]; name != "" {
|
|
return name
|
|
}
|
|
return fleet.MDMNameFromServerURL(data["discovery_service_url"])
|
|
}
|
|
|
|
func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
data := make(map[string]string, len(rows))
|
|
for _, r := range rows {
|
|
data[r["key"]] = r["value"]
|
|
}
|
|
_, autoPilot := data["autopilot"]
|
|
isServer := strings.Contains(strings.ToLower(data["installation_type"]), "server")
|
|
_, enrolled := data["provider_id"]
|
|
return ds.SetOrUpdateMDMData(ctx,
|
|
host.ID,
|
|
isServer,
|
|
enrolled,
|
|
data["discovery_service_url"],
|
|
autoPilot,
|
|
deduceMDMNameWindows(data),
|
|
)
|
|
}
|
|
|
|
func directIngestMunkiInfo(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
if len(rows) == 0 {
|
|
// assume the extension is not there
|
|
return nil
|
|
}
|
|
if len(rows) > 1 {
|
|
logger.Log("component", "service", "method", "ingestMunkiInfo", "warn",
|
|
fmt.Sprintf("munki_info expected single result got %d", len(rows)))
|
|
}
|
|
|
|
errors, warnings := rows[0]["errors"], rows[0]["warnings"]
|
|
errList, warnList := splitCleanSemicolonSeparated(errors), splitCleanSemicolonSeparated(warnings)
|
|
return ds.SetOrUpdateMunkiInfo(ctx, host.ID, rows[0]["version"], errList, warnList)
|
|
}
|
|
|
|
func directIngestDiskEncryptionLinux(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
encrypted := false
|
|
for _, row := range rows {
|
|
if row["path"] == "/" && row["encrypted"] == "1" {
|
|
encrypted = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, encrypted)
|
|
}
|
|
|
|
func directIngestDiskEncryption(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
|
|
encrypted := len(rows) > 0
|
|
return ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, encrypted)
|
|
}
|
|
|
|
func directIngestDiskEncryptionKeyDarwin(
|
|
ctx context.Context,
|
|
logger log.Logger,
|
|
host *fleet.Host,
|
|
ds fleet.Datastore,
|
|
rows []map[string]string,
|
|
) error {
|
|
if len(rows) == 0 {
|
|
// assume the extension is not there
|
|
level.Debug(logger).Log(
|
|
"component", "service",
|
|
"method", "directIngestDiskEncryptionKeyDarwin",
|
|
"msg", "no rows or failed",
|
|
"host", host.Hostname,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
if len(rows) > 1 {
|
|
level.Debug(logger).Log(
|
|
"component", "service",
|
|
"method", "directIngestDiskEncryptionKeyDarwin",
|
|
"msg", fmt.Sprintf("/var/db/FileVaultPRK.dat should have a single line, but got %d", len(rows)),
|
|
"host", host.Hostname,
|
|
)
|
|
}
|
|
|
|
return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, rows[0]["filevault_key"])
|
|
}
|
|
|
|
//go:generate go run gen_queries_doc.go ../../../docs/Using-Fleet/Detail-Queries-Summary.md
|
|
|
|
func GetDetailQueries(
|
|
ctx context.Context,
|
|
fleetConfig config.FleetConfig,
|
|
appConfig *fleet.AppConfig,
|
|
features *fleet.Features,
|
|
) map[string]DetailQuery {
|
|
generatedMap := make(map[string]DetailQuery)
|
|
for key, query := range hostDetailQueries {
|
|
generatedMap[key] = query
|
|
}
|
|
for key, query := range extraDetailQueries {
|
|
generatedMap[key] = query
|
|
}
|
|
|
|
if features != nil && features.EnableSoftwareInventory {
|
|
generatedMap["software_macos"] = softwareMacOS
|
|
generatedMap["software_linux"] = softwareLinux
|
|
generatedMap["software_windows"] = softwareWindows
|
|
}
|
|
|
|
if features != nil && features.EnableHostUsers {
|
|
generatedMap["users"] = usersQuery
|
|
}
|
|
|
|
if !fleetConfig.Vulnerabilities.DisableWinOSVulnerabilities {
|
|
generatedMap["windows_update_history"] = windowsUpdateHistory
|
|
}
|
|
|
|
if fleetConfig.App.EnableScheduledQueryStats {
|
|
generatedMap["scheduled_query_stats"] = scheduledQueryStats
|
|
}
|
|
|
|
if appConfig != nil && appConfig.MDM.EnabledAndConfigured {
|
|
for key, query := range mdmQueries {
|
|
generatedMap[key] = query
|
|
}
|
|
}
|
|
|
|
if features != nil {
|
|
var unknownQueries []string
|
|
|
|
for name, override := range features.DetailQueryOverrides {
|
|
query, ok := generatedMap[name]
|
|
if !ok {
|
|
unknownQueries = append(unknownQueries, name)
|
|
continue
|
|
}
|
|
if override == nil {
|
|
delete(generatedMap, name)
|
|
} else {
|
|
query.Query = *override
|
|
generatedMap[name] = query
|
|
}
|
|
}
|
|
|
|
if len(unknownQueries) > 0 {
|
|
logging.WithErr(ctx, ctxerr.New(ctx, fmt.Sprintf("detail_query_overrides: unknown queries: %s", strings.Join(unknownQueries, ","))))
|
|
}
|
|
}
|
|
|
|
return generatedMap
|
|
}
|
|
|
|
func splitCleanSemicolonSeparated(s string) []string {
|
|
parts := strings.Split(s, ";")
|
|
cleaned := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part != "" {
|
|
cleaned = append(cleaned, part)
|
|
}
|
|
}
|
|
return cleaned
|
|
}
|