2016-11-16 13:47:49 +00:00
package mysql
import (
2021-09-14 12:11:07 +00:00
"context"
2016-11-16 13:47:49 +00:00
"database/sql"
2022-01-18 01:52:09 +00:00
"encoding/json"
2021-11-15 14:11:38 +00:00
"errors"
2016-12-01 17:00:00 +00:00
"fmt"
2021-12-01 19:20:54 +00:00
"sort"
2021-05-07 04:05:09 +00:00
"strings"
2016-11-16 13:47:49 +00:00
"time"
2020-12-10 19:04:58 +00:00
"github.com/cenkalti/backoff/v4"
2021-11-12 11:18:25 +00:00
"github.com/doug-martin/goqu/v9"
2022-07-21 01:54:10 +00:00
"github.com/fleetdm/fleet/v4/server/config"
2021-11-09 14:35:36 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2022-07-21 01:54:10 +00:00
"github.com/go-kit/kit/log"
2021-12-03 18:33:33 +00:00
"github.com/go-kit/kit/log/level"
2020-12-10 19:04:58 +00:00
"github.com/jmoiron/sqlx"
2016-11-16 13:47:49 +00:00
)
2021-06-24 00:32:19 +00:00
var hostSearchColumns = [ ] string { "hostname" , "uuid" , "hardware_serial" , "primary_ip" }
2021-02-18 20:52:43 +00:00
2022-01-18 01:52:09 +00:00
// NewHost creates a new host on the datastore.
//
// Currently only used for testing.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) NewHost ( ctx context . Context , host * fleet . Host ) ( * fleet . Host , error ) {
2016-11-16 13:47:49 +00:00
sqlStatement := `
INSERT INTO hosts (
2016-12-06 19:51:11 +00:00
osquery_host_id ,
2021-06-24 00:32:19 +00:00
detail_updated_at ,
label_updated_at ,
2021-09-27 19:27:38 +00:00
policy_updated_at ,
2016-11-16 13:47:49 +00:00
node_key ,
2021-06-24 00:32:19 +00:00
hostname ,
2016-11-16 13:47:49 +00:00
uuid ,
platform ,
osquery_version ,
os_version ,
uptime ,
2021-06-24 00:32:19 +00:00
memory ,
2022-01-18 01:52:09 +00:00
team_id ,
distributed_interval ,
logger_tls_period ,
config_tls_refresh ,
refetch_requested
2016-11-16 13:47:49 +00:00
)
2022-04-15 20:09:47 +00:00
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
2016-11-16 13:47:49 +00:00
`
2022-02-03 17:56:22 +00:00
result , err := ds . writer . ExecContext (
2021-09-14 14:44:02 +00:00
ctx ,
2020-04-07 01:10:20 +00:00
sqlStatement ,
host . OsqueryHostID ,
2021-06-24 00:32:19 +00:00
host . DetailUpdatedAt ,
host . LabelUpdatedAt ,
2021-09-27 19:27:38 +00:00
host . PolicyUpdatedAt ,
2020-04-07 01:10:20 +00:00
host . NodeKey ,
2021-06-24 00:32:19 +00:00
host . Hostname ,
2020-04-07 01:10:20 +00:00
host . UUID ,
host . Platform ,
host . OsqueryVersion ,
host . OSVersion ,
host . Uptime ,
2021-06-24 00:32:19 +00:00
host . Memory ,
2021-05-27 20:18:00 +00:00
host . TeamID ,
2022-01-18 01:52:09 +00:00
host . DistributedInterval ,
host . LoggerTLSPeriod ,
host . ConfigTLSRefresh ,
host . RefetchRequested ,
2020-04-07 01:10:20 +00:00
)
2016-11-16 13:47:49 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "new host" )
2016-11-16 13:47:49 +00:00
}
id , _ := result . LastInsertId ( )
host . ID = uint ( id )
2021-11-08 14:42:37 +00:00
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , ` INSERT INTO host_seen_times (host_id, seen_time) VALUES (?,?) ` , host . ID , host . SeenTime )
2021-11-08 14:42:37 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "new host seen time" )
2021-11-08 14:42:37 +00:00
}
2016-11-16 13:47:49 +00:00
return host , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SerialUpdateHost ( ctx context . Context , host * fleet . Host ) error {
2021-11-08 14:42:37 +00:00
errCh := make ( chan error , 1 )
defer close ( errCh )
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
2022-02-03 17:56:22 +00:00
case ds . writeCh <- itemToWrite {
2021-11-08 14:42:37 +00:00
ctx : ctx ,
errCh : errCh ,
item : host ,
} :
return <- errCh
}
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostPackStats ( ctx context . Context , hostID uint , stats [ ] fleet . PackStats ) error {
return saveHostPackStatsDB ( ctx , ds . writer , hostID , stats )
2022-01-18 01:52:09 +00:00
}
func saveHostPackStatsDB ( ctx context . Context , db sqlx . ExecerContext , hostID uint , stats [ ] fleet . PackStats ) error {
2022-08-10 14:01:05 +00:00
// NOTE: this implementation must be kept in sync with the async/batch version
// in AsyncBatchSaveHostsScheduledQueryStats (in scheduled_queries.go) - that is,
// the behaviour per host must be the same.
2021-09-08 18:43:22 +00:00
var args [ ] interface { }
queryCount := 0
2022-01-18 01:52:09 +00:00
for _ , pack := range stats {
2021-09-08 18:43:22 +00:00
for _ , query := range pack . QueryStats {
queryCount ++
args = append ( args ,
query . PackName ,
query . ScheduledQueryName ,
2022-01-18 01:52:09 +00:00
hostID ,
2021-09-08 18:43:22 +00:00
query . AverageMemory ,
query . Denylisted ,
query . Executions ,
query . Interval ,
query . LastExecuted ,
query . OutputSize ,
query . SystemTime ,
query . UserTime ,
query . WallTime ,
)
2021-05-07 04:05:09 +00:00
}
2021-09-08 18:43:22 +00:00
}
2021-05-07 04:05:09 +00:00
2021-09-08 18:43:22 +00:00
if queryCount == 0 {
return nil
}
2021-05-07 04:05:09 +00:00
2021-09-08 18:43:22 +00:00
values := strings . TrimSuffix ( strings . Repeat ( "((SELECT sq.id FROM scheduled_queries sq JOIN packs p ON (sq.pack_id = p.id) WHERE p.name = ? AND sq.name = ?),?,?,?,?,?,?,?,?,?,?)," , queryCount ) , "," )
sql := fmt . Sprintf ( `
2021-05-07 04:05:09 +00:00
INSERT IGNORE INTO scheduled_query_stats (
scheduled_query_id ,
host_id ,
average_memory ,
denylisted ,
executions ,
schedule_interval ,
last_executed ,
output_size ,
system_time ,
user_time ,
wall_time
)
2021-09-02 20:39:08 +00:00
VALUES % s ON DUPLICATE KEY UPDATE
scheduled_query_id = VALUES ( scheduled_query_id ) ,
host_id = VALUES ( host_id ) ,
average_memory = VALUES ( average_memory ) ,
denylisted = VALUES ( denylisted ) ,
executions = VALUES ( executions ) ,
schedule_interval = VALUES ( schedule_interval ) ,
last_executed = VALUES ( last_executed ) ,
output_size = VALUES ( output_size ) ,
system_time = VALUES ( system_time ) ,
user_time = VALUES ( user_time ) ,
wall_time = VALUES ( wall_time )
2021-05-07 04:05:09 +00:00
` , values )
2021-09-14 14:44:02 +00:00
if _ , err := db . ExecContext ( ctx , sql , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert pack stats" )
2021-05-07 04:05:09 +00:00
}
return nil
}
2021-12-07 15:51:53 +00:00
// MySQL is really particular about using zero values or old values for
// timestamps, so we set a default value that is plenty far in the past, but
// hopefully accepted by most MySQL configurations.
//
// NOTE: #3229 proposes a better fix that uses *time.Time for
// ScheduledQueryStats.LastExecuted.
var pastDate = "2000-01-01T00:00:00Z"
2021-11-12 11:18:25 +00:00
// loadhostPacksStatsDB will load all the pack stats for the given host. The scheduled
// queries that haven't run yet are returned with zero values.
2021-11-17 22:03:30 +00:00
func loadHostPackStatsDB ( ctx context . Context , db sqlx . QueryerContext , hid uint , hostPlatform string ) ( [ ] fleet . PackStats , error ) {
2021-11-12 11:18:25 +00:00
packs , err := listPacksForHost ( ctx , db , hid )
if err != nil {
return nil , ctxerr . Wrapf ( ctx , err , "list packs for host: %d" , hid )
}
if len ( packs ) == 0 {
return nil , nil
}
packIDs := make ( [ ] uint , len ( packs ) )
packTypes := make ( map [ uint ] * string )
for i := range packs {
packIDs [ i ] = packs [ i ] . ID
packTypes [ packs [ i ] . ID ] = packs [ i ] . Type
}
ds := dialect . From ( goqu . I ( "scheduled_queries" ) . As ( "sq" ) ) . Select (
goqu . I ( "sq.name" ) . As ( "scheduled_query_name" ) ,
goqu . I ( "sq.id" ) . As ( "scheduled_query_id" ) ,
goqu . I ( "sq.query_name" ) . As ( "query_name" ) ,
goqu . I ( "q.description" ) . As ( "description" ) ,
goqu . I ( "p.name" ) . As ( "pack_name" ) ,
goqu . I ( "p.id" ) . As ( "pack_id" ) ,
goqu . COALESCE ( goqu . I ( "sqs.average_memory" ) , 0 ) . As ( "average_memory" ) ,
goqu . COALESCE ( goqu . I ( "sqs.denylisted" ) , false ) . As ( "denylisted" ) ,
goqu . COALESCE ( goqu . I ( "sqs.executions" ) , 0 ) . As ( "executions" ) ,
goqu . I ( "sq.interval" ) . As ( "schedule_interval" ) ,
2021-12-07 15:51:53 +00:00
goqu . COALESCE ( goqu . I ( "sqs.last_executed" ) , goqu . L ( "timestamp(?)" , pastDate ) ) . As ( "last_executed" ) ,
2021-11-12 11:18:25 +00:00
goqu . COALESCE ( goqu . I ( "sqs.output_size" ) , 0 ) . As ( "output_size" ) ,
goqu . COALESCE ( goqu . I ( "sqs.system_time" ) , 0 ) . As ( "system_time" ) ,
goqu . COALESCE ( goqu . I ( "sqs.user_time" ) , 0 ) . As ( "user_time" ) ,
goqu . COALESCE ( goqu . I ( "sqs.wall_time" ) , 0 ) . As ( "wall_time" ) ,
) . Join (
dialect . From ( "packs" ) . As ( "p" ) . Select (
goqu . I ( "id" ) ,
goqu . I ( "name" ) ,
) . Where ( goqu . I ( "id" ) . In ( packIDs ) ) ,
goqu . On ( goqu . I ( "sq.pack_id" ) . Eq ( goqu . I ( "p.id" ) ) ) ,
) . Join (
goqu . I ( "queries" ) . As ( "q" ) ,
goqu . On ( goqu . I ( "sq.query_name" ) . Eq ( goqu . I ( "q.name" ) ) ) ,
) . LeftJoin (
2021-11-17 22:03:30 +00:00
dialect . From ( "scheduled_query_stats" ) . As ( "sqs" ) . Where (
goqu . I ( "host_id" ) . Eq ( hid ) ,
) ,
2021-11-12 11:18:25 +00:00
goqu . On ( goqu . I ( "sqs.scheduled_query_id" ) . Eq ( goqu . I ( "sq.id" ) ) ) ,
2021-11-17 22:03:30 +00:00
) . Where (
goqu . Or (
// sq.platform empty or NULL means the scheduled query is set to
// run on all hosts.
goqu . I ( "sq.platform" ) . Eq ( "" ) ,
goqu . I ( "sq.platform" ) . IsNull ( ) ,
// scheduled_queries.platform can be a comma-separated list of
// platforms, e.g. "darwin,windows".
2021-12-03 18:33:33 +00:00
goqu . L ( "FIND_IN_SET(?, sq.platform)" , fleet . PlatformFromHost ( hostPlatform ) ) . Neq ( 0 ) ,
2021-11-17 22:03:30 +00:00
) ,
2021-11-12 11:18:25 +00:00
)
sql , args , err := ds . ToSQL ( )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sql build" )
}
2021-06-06 22:07:29 +00:00
var stats [ ] fleet . ScheduledQueryStats
2021-11-12 11:18:25 +00:00
if err := sqlx . SelectContext ( ctx , db , & stats , sql , args ... ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "load pack stats" )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
packStats := map [ uint ] fleet . PackStats { }
2021-05-07 04:05:09 +00:00
for _ , query := range stats {
2021-11-12 11:18:25 +00:00
pack := packStats [ query . PackID ]
2021-05-07 04:05:09 +00:00
pack . PackName = query . PackName
pack . PackID = query . PackID
2021-11-12 11:18:25 +00:00
pack . Type = getPackTypeFromDBField ( packTypes [ pack . PackID ] )
2021-05-07 04:05:09 +00:00
pack . QueryStats = append ( pack . QueryStats , query )
2021-11-12 11:18:25 +00:00
packStats [ pack . PackID ] = pack
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
var ps [ ] fleet . PackStats
for _ , pack := range packStats {
ps = append ( ps , pack )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
return ps , nil
}
2021-05-07 04:05:09 +00:00
2021-11-12 11:18:25 +00:00
func getPackTypeFromDBField ( t * string ) string {
if t == nil {
return "pack"
}
return * t
2016-11-16 13:47:49 +00:00
}
2022-01-18 01:52:09 +00:00
func loadHostUsersDB ( ctx context . Context , db sqlx . QueryerContext , hostID uint ) ( [ ] fleet . HostUser , error ) {
2021-11-11 17:26:03 +00:00
sql := ` SELECT username, groupname, uid, user_type, shell FROM host_users WHERE host_id = ? and removed_at IS NULL `
2022-01-18 01:52:09 +00:00
var users [ ] fleet . HostUser
if err := sqlx . SelectContext ( ctx , db , & users , sql , hostID ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "load host users" )
2021-07-13 20:15:38 +00:00
}
2022-01-18 01:52:09 +00:00
return users , nil
2021-07-13 20:15:38 +00:00
}
2022-03-23 15:15:05 +00:00
// hostRefs are the tables referenced by hosts.
//
// Defined here for testing purposes.
var hostRefs = [ ] string {
"host_seen_times" ,
"host_software" ,
"host_users" ,
"host_emails" ,
"host_additional" ,
"scheduled_query_stats" ,
"label_membership" ,
"policy_membership" ,
"host_mdm" ,
"host_munki_info" ,
"host_device_auth" ,
2022-06-28 18:11:49 +00:00
"host_batteries" ,
2022-08-12 19:23:25 +00:00
"host_operating_system" ,
2022-03-23 15:15:05 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) DeleteHost ( ctx context . Context , hid uint ) error {
2022-01-12 17:07:51 +00:00
delHostRef := func ( tx sqlx . ExtContext , table string ) error {
_ , err := tx . ExecContext ( ctx , fmt . Sprintf ( ` DELETE FROM %s WHERE host_id=? ` , table ) , hid )
if err != nil {
return ctxerr . Wrapf ( ctx , err , "deleting %s for host %d" , table , hid )
}
return nil
2017-01-20 17:22:33 +00:00
}
2021-11-08 14:42:37 +00:00
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2022-01-12 17:07:51 +00:00
_ , err := tx . ExecContext ( ctx , ` DELETE FROM hosts WHERE id = ? ` , hid )
if err != nil {
return ctxerr . Wrapf ( ctx , err , "delete host" )
}
2021-11-08 14:42:37 +00:00
2022-01-12 17:07:51 +00:00
for _ , table := range hostRefs {
err := delHostRef ( tx , table )
if err != nil {
return err
}
}
2022-01-19 13:28:08 +00:00
2022-04-15 20:09:47 +00:00
_ , err = tx . ExecContext ( ctx , ` DELETE FROM pack_targets WHERE type = ? AND target_id = ? ` , fleet . TargetHost , hid )
2022-01-19 13:28:08 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "deleting pack_targets for host %d" , hid )
}
2022-01-12 17:07:51 +00:00
return nil
} )
2016-11-16 13:47:49 +00:00
}
2022-05-25 16:30:03 +00:00
func ( ds * Datastore ) Host ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
2022-06-01 16:06:57 +00:00
sqlStatement := `
SELECT
h . * ,
COALESCE ( hst . seen_time , h . created_at ) AS seen_time ,
t . name AS team_name ,
(
SELECT
additional
FROM
host_additional
WHERE
host_id = h . id
) AS additional ,
coalesce ( failing_policies . count , 0 ) as failing_policies_count ,
coalesce ( failing_policies . count , 0 ) as total_issues_count
FROM
hosts h
LEFT JOIN teams t ON ( h . team_id = t . id )
LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id )
JOIN (
SELECT
count ( * ) as count
FROM
policy_membership
WHERE
passes = 0
AND host_id = ?
) failing_policies
WHERE
h . id = ?
LIMIT
1
`
2021-11-18 17:36:35 +00:00
args := [ ] interface { } { id , id }
2022-06-01 16:06:57 +00:00
var host fleet . Host
err := sqlx . GetContext ( ctx , ds . reader , & host , sqlStatement , args ... )
2016-11-16 13:47:49 +00:00
if err != nil {
2021-12-14 21:34:11 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithID ( id ) )
}
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host by id" )
2021-05-07 04:05:09 +00:00
}
2021-11-12 11:18:25 +00:00
2022-02-03 17:56:22 +00:00
packStats , err := loadHostPackStatsDB ( ctx , ds . reader , host . ID , host . Platform )
2021-11-12 11:18:25 +00:00
if err != nil {
2021-05-07 04:05:09 +00:00
return nil , err
2016-11-16 13:47:49 +00:00
}
2021-11-12 11:18:25 +00:00
host . PackStats = packStats
2022-02-03 17:56:22 +00:00
users , err := loadHostUsersDB ( ctx , ds . reader , host . ID )
2022-01-18 01:52:09 +00:00
if err != nil {
2021-07-13 20:15:38 +00:00
return nil , err
}
2022-01-18 01:52:09 +00:00
host . Users = users
2016-11-16 13:47:49 +00:00
2022-06-01 16:06:57 +00:00
return & host , nil
2016-11-16 13:47:49 +00:00
}
2022-06-22 20:38:11 +00:00
func amountEnrolledHostsByOSDB ( ctx context . Context , db sqlx . QueryerContext ) ( byOS map [ string ] [ ] fleet . HostsCountByOSVersion , totalCount int , err error ) {
var hostsByOS [ ] struct {
Platform string ` db:"platform" `
OSVersion string ` db:"os_version" `
NumHosts int ` db:"num_hosts" `
}
const stmt = `
SELECT platform , os_version , count ( * ) as num_hosts
FROM hosts
GROUP BY platform , os_version
`
if err := sqlx . SelectContext ( ctx , db , & hostsByOS , stmt ) ; err != nil {
return nil , 0 , err
}
byOS = make ( map [ string ] [ ] fleet . HostsCountByOSVersion )
for _ , h := range hostsByOS {
totalCount += h . NumHosts
byVersion := byOS [ h . Platform ]
byVersion = append ( byVersion , fleet . HostsCountByOSVersion {
Version : h . OSVersion ,
NumEnrolled : h . NumHosts ,
} )
byOS [ h . Platform ] = byVersion
2021-07-20 21:39:50 +00:00
}
2022-06-22 20:38:11 +00:00
return byOS , totalCount , nil
2021-07-20 21:39:50 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListHosts ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
2021-05-18 00:52:59 +00:00
sql := ` SELECT
2021-07-02 15:59:42 +00:00
h . * ,
2021-12-01 12:05:23 +00:00
COALESCE ( hst . seen_time , h . created_at ) AS seen_time ,
2021-11-29 21:04:33 +00:00
t . name AS team_name
2022-05-02 21:34:14 +00:00
`
if opt . DeviceMapping {
2022-05-10 15:29:17 +00:00
sql += ` ,
2022-05-05 18:13:53 +00:00
COALESCE ( dm . device_mapping , ' null ' ) as device_mapping
2021-11-29 21:04:33 +00:00
`
2022-05-02 21:34:14 +00:00
}
2021-11-29 21:04:33 +00:00
failingPoliciesSelect := ` ,
2021-10-15 10:34:30 +00:00
coalesce ( failing_policies . count , 0 ) as failing_policies_count ,
coalesce ( failing_policies . count , 0 ) as total_issues_count
2022-05-02 21:34:14 +00:00
`
2021-11-29 21:04:33 +00:00
if opt . DisableFailingPolicies {
failingPoliciesSelect = ""
}
sql += failingPoliciesSelect
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
2020-03-30 02:19:54 +00:00
var params [ ] interface { }
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
2021-05-26 23:24:12 +00:00
// Only include "additional" if filter provided.
if len ( opt . AdditionalFilters ) == 1 && opt . AdditionalFilters [ 0 ] == "*" {
// All info requested.
sql += `
, ( SELECT additional FROM host_additional WHERE host_id = h . id ) AS additional
`
} else if len ( opt . AdditionalFilters ) > 0 {
// Filter specific columns.
sql += ` , ( SELECT JSON_OBJECT (
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
`
for _ , field := range opt . AdditionalFilters {
2021-05-17 17:29:50 +00:00
sql += ` ?, JSON_EXTRACT(additional, ?), `
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
params = append ( params , field , fmt . Sprintf ( ` $."%s" ` , field ) )
}
sql = sql [ : len ( sql ) - 2 ]
sql += `
2021-05-26 23:24:12 +00:00
) FROM host_additional WHERE host_id = h . id ) AS additional
Add host additional info filters (#28)
This change adds the ability to filter additional host info via the list hosts endpoint; a continuation from [here](https://github.com/kolide/fleet/pull/2330), but now filtering is accomplished via SQL.
Additional object without filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"time": [
{
"day": "13",
"hour": "3",
"year": "2020",
"month": "10",
"minutes": "43",
"seconds": "11",
"weekday": "Tuesday",
"datetime": "2020-10-13T03:43:11Z",
"iso_8601": "2020-10-13T03:43:11Z",
"timezone": "GMT",
"timestamp": "Tue Oct 13 03:43:11 2020 UTC",
"unix_time": "1602560591",
"local_time": "1602560591",
"local_timezone": "UTC"
}
},
...
```
Additional object with filter:
```
curl 'https://localhost:8080/api/v1/kolide/hosts?additional_info_filters=macs,notreal'
...
"additional": {
"macs": [
{
"mac": "00:00:00:00:00:00"
},
{
"mac": "02:42:c0:a8:10:05"
}
],
"notreal": null
},
...
```
2020-11-14 00:33:25 +00:00
`
}
2022-02-03 17:56:22 +00:00
sql , params = ds . applyHostFilters ( opt , sql , filter , params )
2021-10-07 11:25:35 +00:00
hosts := [ ] * fleet . Host { }
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hosts , sql , params ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "list hosts" )
2021-10-07 11:25:35 +00:00
}
return hosts , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) applyHostFilters ( opt fleet . HostListOptions , sql string , filter fleet . TeamFilter , params [ ] interface { } ) ( string , [ ] interface { } ) {
2022-05-02 21:34:14 +00:00
deviceMappingJoin := ` LEFT JOIN (
SELECT
host_id ,
CONCAT ( '[' , GROUP_CONCAT ( JSON_OBJECT ( ' email ' , email , ' source ' , source ) ) , ']' ) AS device_mapping
FROM
host_emails
GROUP BY
host_id ) dm ON dm . host_id = h . id `
if ! opt . DeviceMapping {
deviceMappingJoin = ""
}
2022-04-15 20:09:47 +00:00
policyMembershipJoin := "JOIN policy_membership pm ON (h.id = pm.host_id)"
2021-08-24 20:24:52 +00:00
if opt . PolicyIDFilter == nil {
policyMembershipJoin = ""
} else if opt . PolicyResponseFilter == nil {
policyMembershipJoin = "LEFT " + policyMembershipJoin
}
2021-10-12 14:38:12 +00:00
softwareFilter := "TRUE"
if opt . SoftwareIDFilter != nil {
2022-04-15 20:09:47 +00:00
softwareFilter = "EXISTS (SELECT 1 FROM host_software hs WHERE hs.host_id = h.id AND hs.software_id = ?)"
2021-10-12 14:38:12 +00:00
params = append ( params , opt . SoftwareIDFilter )
}
2021-11-29 21:04:33 +00:00
failingPoliciesJoin := ` LEFT JOIN (
2022-04-15 20:09:47 +00:00
SELECT host_id , count ( * ) as count FROM policy_membership WHERE passes = 0
2021-11-29 21:04:33 +00:00
GROUP BY host_id
) as failing_policies ON ( h . id = failing_policies . host_id ) `
if opt . DisableFailingPolicies {
failingPoliciesJoin = ""
}
2022-08-10 19:15:01 +00:00
mdmJoin := ` JOIN host_mdm hmdm ON h.id = hmdm.host_id `
if opt . MDMIDFilter == nil && opt . MDMEnrollmentStatusFilter == "" {
mdmJoin = ""
}
2022-08-12 19:23:25 +00:00
operatingSystemJoin := ` JOIN host_operating_system hos ON h.id = hos.host_id `
if opt . OperatingSystemIDFilter == nil {
operatingSystemJoin = ""
}
2021-11-08 14:42:37 +00:00
sql += fmt . Sprintf ( ` FROM hosts h
2022-04-15 20:09:47 +00:00
LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id )
2021-11-08 14:42:37 +00:00
LEFT JOIN teams t ON ( h . team_id = t . id )
2021-11-29 21:04:33 +00:00
% s
2021-08-24 20:24:52 +00:00
% s
2022-05-02 21:34:14 +00:00
% s
2022-08-10 19:15:01 +00:00
% s
2022-08-12 19:23:25 +00:00
% s
2021-10-12 14:38:12 +00:00
WHERE TRUE AND % s AND % s
2022-08-12 19:23:25 +00:00
` , deviceMappingJoin , policyMembershipJoin , failingPoliciesJoin , mdmJoin , operatingSystemJoin , ds . whereFilterHostsByTeams ( filter , "h" ) , softwareFilter ,
2021-06-04 01:53:43 +00:00
)
2021-08-05 17:56:29 +00:00
sql , params = filterHostsByStatus ( sql , opt , params )
2021-08-11 14:40:56 +00:00
sql , params = filterHostsByTeam ( sql , opt , params )
2021-08-24 20:24:52 +00:00
sql , params = filterHostsByPolicy ( sql , opt , params )
2022-08-10 19:15:01 +00:00
sql , params = filterHostsByMDM ( sql , opt , params )
2022-08-12 19:23:25 +00:00
sql , params = filterHostsByOsID ( sql , opt , params )
2021-12-21 20:36:19 +00:00
sql , params = hostSearchLike ( sql , params , opt . MatchQuery , hostSearchColumns ... )
2021-11-29 18:06:00 +00:00
sql , params = appendListOptionsWithCursorToSQL ( sql , params , opt . ListOptions )
2021-08-05 17:56:29 +00:00
2021-10-07 11:25:35 +00:00
return sql , params
2021-08-05 17:56:29 +00:00
}
2021-08-11 14:40:56 +00:00
func filterHostsByTeam ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . TeamFilter != nil {
sql += ` AND h.team_id = ? `
params = append ( params , * opt . TeamFilter )
}
return sql , params
}
2022-08-10 19:15:01 +00:00
func filterHostsByMDM ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . MDMIDFilter != nil {
sql += ` AND hmdm.mdm_id = ? `
params = append ( params , * opt . MDMIDFilter )
}
if opt . MDMEnrollmentStatusFilter != "" {
switch opt . MDMEnrollmentStatusFilter {
case fleet . MDMEnrollStatusAutomatic :
sql += ` AND hmdm.enrolled = 1 AND hmdm.installed_from_dep = 1 `
case fleet . MDMEnrollStatusManual :
sql += ` AND hmdm.enrolled = 1 AND hmdm.installed_from_dep = 0 `
case fleet . MDMEnrollStatusUnenrolled :
sql += ` AND hmdm.enrolled = 0 `
}
}
return sql , params
}
2022-08-12 19:23:25 +00:00
func filterHostsByOsID ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . OperatingSystemIDFilter != nil {
sql += ` AND hos.os_id = ? `
params = append ( params , * opt . OperatingSystemIDFilter )
}
return sql , params
}
2021-08-24 20:24:52 +00:00
func filterHostsByPolicy ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
if opt . PolicyIDFilter != nil && opt . PolicyResponseFilter != nil {
sql += ` AND pm.policy_id = ? AND pm.passes = ? `
params = append ( params , * opt . PolicyIDFilter , * opt . PolicyResponseFilter )
} else if opt . PolicyIDFilter != nil && opt . PolicyResponseFilter == nil {
sql += ` AND (pm.policy_id = ? OR pm.policy_id IS NULL) AND pm.passes IS NULL `
params = append ( params , * opt . PolicyIDFilter )
}
return sql , params
}
2021-08-05 17:56:29 +00:00
func filterHostsByStatus ( sql string , opt fleet . HostListOptions , params [ ] interface { } ) ( string , [ ] interface { } ) {
2020-03-30 02:19:54 +00:00
switch opt . StatusFilter {
case "new" :
2021-05-18 00:52:59 +00:00
sql += "AND DATE_ADD(h.created_at, INTERVAL 1 DAY) >= ?"
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
case "online" :
2021-12-01 12:05:23 +00:00
sql += fmt . Sprintf ( "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND) > ?" , fleet . OnlineIntervalBuffer )
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
case "offline" :
2022-05-23 19:11:02 +00:00
sql += fmt . Sprintf ( "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL LEAST(h.distributed_interval, h.config_tls_refresh) + %d SECOND) <= ?" , fleet . OnlineIntervalBuffer )
params = append ( params , time . Now ( ) )
2020-03-30 02:19:54 +00:00
case "mia" :
2021-12-01 12:05:23 +00:00
sql += "AND DATE_ADD(COALESCE(hst.seen_time, h.created_at), INTERVAL 30 DAY) <= ?"
2020-03-30 02:19:54 +00:00
params = append ( params , time . Now ( ) )
}
2021-08-05 17:56:29 +00:00
return sql , params
2016-12-06 19:51:11 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) CountHosts ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( int , error ) {
2021-10-07 11:25:35 +00:00
sql := ` SELECT count(*) `
2021-10-12 14:38:12 +00:00
// ignore pagination in count
opt . Page = 0
opt . PerPage = 0
2021-10-07 11:25:35 +00:00
var params [ ] interface { }
2022-02-03 17:56:22 +00:00
sql , params = ds . applyHostFilters ( opt , sql , filter , params )
2021-10-07 11:25:35 +00:00
var count int
2022-02-03 17:56:22 +00:00
if err := sqlx . GetContext ( ctx , ds . reader , & count , sql , params ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return 0 , ctxerr . Wrap ( ctx , err , "count hosts" )
2021-10-07 11:25:35 +00:00
}
return count , nil
}
2022-06-13 20:29:32 +00:00
func ( ds * Datastore ) CleanupIncomingHosts ( ctx context . Context , now time . Time ) ( [ ] uint , error ) {
var ids [ ] uint
selectIDs := `
SELECT
id
FROM
hosts
WHERE
hostname = ' ' AND
osquery_version = ' ' AND
created_at < ( ? - INTERVAL 5 MINUTE ) `
if err := ds . writer . SelectContext ( ctx , & ids , selectIDs , now ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "load incoming hosts to cleanup" )
}
cleanupHosts := `
2019-04-09 18:11:11 +00:00
DELETE FROM hosts
2021-06-24 00:32:19 +00:00
WHERE hostname = ' ' AND osquery_version = ' '
2019-04-09 18:11:11 +00:00
AND created_at < ( ? - INTERVAL 5 MINUTE )
`
2022-06-13 20:29:32 +00:00
if _ , err := ds . writer . ExecContext ( ctx , cleanupHosts , now ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "cleanup incoming hosts" )
2019-04-09 18:11:11 +00:00
}
2022-06-13 20:29:32 +00:00
return ids , nil
2019-04-09 18:11:11 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GenerateHostStatusStatistics ( ctx context . Context , filter fleet . TeamFilter , now time . Time , platform * string ) ( * fleet . HostSummary , error ) {
2017-04-18 17:39:50 +00:00
// The logic in this function should remain synchronized with
2021-11-09 14:35:36 +00:00
// host.Status and CountHostsInTargets - that is, the intervals associated
// with each status must be the same.
2017-04-18 17:39:50 +00:00
2022-05-23 19:11:02 +00:00
args := [ ] interface { } { now , now , now , now }
2022-02-03 17:56:22 +00:00
whereClause := ds . whereFilterHostsByTeams ( filter , "h" )
2022-01-24 17:49:21 +00:00
if platform != nil {
2022-02-07 16:50:36 +00:00
whereClause += " AND h.platform IN (?) "
args = append ( args , fleet . ExpandPlatform ( * platform ) )
2022-01-24 17:49:21 +00:00
}
2017-04-18 17:39:50 +00:00
sqlStatement := fmt . Sprintf ( `
2021-06-04 01:53:43 +00:00
SELECT
2021-11-09 14:35:36 +00:00
COUNT ( * ) total ,
2021-12-01 12:05:23 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL 30 DAY ) <= ? THEN 1 ELSE 0 END ) , 0 ) mia ,
2022-05-23 19:11:02 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL LEAST ( distributed_interval , config_tls_refresh ) + % d SECOND ) <= ? THEN 1 ELSE 0 END ) , 0 ) offline ,
2021-12-01 12:05:23 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( COALESCE ( hst . seen_time , h . created_at ) , INTERVAL LEAST ( distributed_interval , config_tls_refresh ) + % d SECOND ) > ? THEN 1 ELSE 0 END ) , 0 ) online ,
2021-06-04 01:53:43 +00:00
COALESCE ( SUM ( CASE WHEN DATE_ADD ( created_at , INTERVAL 1 DAY ) >= ? THEN 1 ELSE 0 END ) , 0 ) new
2022-04-15 20:09:47 +00:00
FROM hosts h LEFT JOIN host_seen_times hst ON ( h . id = hst . host_id ) WHERE % s
2021-06-04 01:53:43 +00:00
LIMIT 1 ;
2021-11-09 14:35:36 +00:00
` , fleet . OnlineIntervalBuffer , fleet . OnlineIntervalBuffer , whereClause )
2017-01-04 21:16:17 +00:00
2022-02-07 16:50:36 +00:00
stmt , args , err := sqlx . In ( sqlStatement , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host statistics statement" )
}
2021-11-15 14:56:13 +00:00
summary := fleet . HostSummary { TeamID : filter . TeamID }
2022-02-07 16:50:36 +00:00
err = sqlx . GetContext ( ctx , ds . reader , & summary , stmt , args ... )
2017-01-16 19:52:03 +00:00
if err != nil && err != sql . ErrNoRows {
2021-11-09 14:35:36 +00:00
return nil , ctxerr . Wrap ( ctx , err , "generating host statistics" )
}
// get the counts per platform, the `h` alias for hosts is required so that
// reusing the whereClause is ok.
2022-01-24 17:49:21 +00:00
args = [ ] interface { } { }
if platform != nil {
2022-02-07 16:50:36 +00:00
args = append ( args , fleet . ExpandPlatform ( * platform ) )
2022-01-24 17:49:21 +00:00
}
2021-11-09 14:35:36 +00:00
sqlStatement = fmt . Sprintf ( `
SELECT
COUNT ( * ) total ,
h . platform
FROM hosts h
WHERE % s
GROUP BY h . platform
` , whereClause )
var platforms [ ] * fleet . HostSummaryPlatform
2022-02-07 16:50:36 +00:00
stmt , args , err = sqlx . In ( sqlStatement , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host platforms statement" )
}
err = sqlx . SelectContext ( ctx , ds . reader , & platforms , stmt , args ... )
2021-11-09 14:35:36 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "generating host platforms statistics" )
2017-01-04 21:16:17 +00:00
}
2021-11-09 14:35:36 +00:00
summary . Platforms = platforms
2017-01-04 21:16:17 +00:00
2021-11-09 14:35:36 +00:00
return & summary , nil
2017-01-04 21:16:17 +00:00
}
2022-05-30 13:30:15 +00:00
func shouldCleanTeamPolicies ( currentTeamID , newTeamID * uint ) bool {
// if the host is global, then there should be nothing to clean up
if currentTeamID == nil {
return false
}
// if the host is switching from a team to global, we should clean up
if newTeamID == nil {
return true
}
// clean up if the host is switching to a different team
return * currentTeamID != * newTeamID
}
2016-11-16 13:47:49 +00:00
// EnrollHost enrolls a host
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) EnrollHost ( ctx context . Context , osqueryHostID , nodeKey string , teamID * uint , cooldown time . Duration ) ( * fleet . Host , error ) {
2016-12-06 19:51:11 +00:00
if osqueryHostID == "" {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . New ( ctx , "missing osquery host identifier" )
2016-11-16 13:47:49 +00:00
}
2016-12-06 19:51:11 +00:00
2021-06-06 22:07:29 +00:00
var host fleet . Host
2022-02-03 17:56:22 +00:00
err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2020-12-10 19:04:58 +00:00
zeroTime := time . Unix ( 0 , 0 ) . Add ( 24 * time . Hour )
2016-11-16 13:47:49 +00:00
2021-12-01 12:05:23 +00:00
var hostID int64
2022-05-30 13:30:15 +00:00
err := sqlx . GetContext ( ctx , tx , & host , ` SELECT id, last_enrolled_at, team_id FROM hosts WHERE osquery_host_id = ? ` , osqueryHostID )
2021-01-19 22:45:58 +00:00
switch {
case err != nil && ! errors . Is ( err , sql . ErrNoRows ) :
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "check existing" )
2021-01-19 22:45:58 +00:00
case errors . Is ( err , sql . ErrNoRows ) :
2022-08-16 12:33:15 +00:00
// Create new host record. We always create newly enrolled hosts with refetch_requested = true
// so that the frontend automatically starts background checks to update the page whenever
// the refetch is completed.
2020-12-10 19:04:58 +00:00
sqlInsert := `
INSERT INTO hosts (
2021-06-24 00:32:19 +00:00
detail_updated_at ,
label_updated_at ,
2021-09-27 19:27:38 +00:00
policy_updated_at ,
2020-12-10 19:04:58 +00:00
osquery_host_id ,
node_key ,
2022-08-16 12:33:15 +00:00
team_id ,
refetch_requested
) VALUES ( ? , ? , ? , ? , ? , ? , 1 )
2020-12-10 19:04:58 +00:00
`
2021-11-08 14:42:37 +00:00
result , err := tx . ExecContext ( ctx , sqlInsert , zeroTime , zeroTime , zeroTime , osqueryHostID , nodeKey , teamID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert host" )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
hostID , _ = result . LastInsertId ( )
2021-01-19 22:45:58 +00:00
default :
2020-12-10 19:04:58 +00:00
// Prevent hosts from enrolling too often with the same identifier.
// Prior to adding this we saw many hosts (probably VMs) with the
// same identifier competing for enrollment and causing perf issues.
2021-06-24 00:32:19 +00:00
if cooldown > 0 && time . Since ( host . LastEnrolledAt ) < cooldown {
2021-11-15 14:11:38 +00:00
return backoff . Permanent ( ctxerr . Errorf ( ctx , "host identified by %s enrolling too often" , osqueryHostID ) )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
hostID = int64 ( host . ID )
2022-05-30 13:30:15 +00:00
if shouldCleanTeamPolicies ( host . TeamID , teamID ) {
if err := cleanupPolicyMembershipOnTeamChange ( ctx , tx , [ ] uint { host . ID } ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "EnrollHost delete policy membership" )
}
}
2020-12-10 19:04:58 +00:00
// Update existing host record
sqlUpdate := `
UPDATE hosts
SET node_key = ? ,
2021-05-31 16:02:05 +00:00
team_id = ? ,
2021-06-24 00:32:19 +00:00
last_enrolled_at = NOW ( )
2020-12-10 19:04:58 +00:00
WHERE osquery_host_id = ?
`
2021-09-14 14:44:02 +00:00
_ , err := tx . ExecContext ( ctx , sqlUpdate , nodeKey , teamID , osqueryHostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "update host" )
2020-12-10 19:04:58 +00:00
}
}
2021-12-01 12:05:23 +00:00
_ , err = tx . ExecContext ( ctx , `
2022-04-15 20:09:47 +00:00
INSERT INTO host_seen_times ( host_id , seen_time ) VALUES ( ? , ? )
2021-12-01 12:05:23 +00:00
ON DUPLICATE KEY UPDATE seen_time = VALUES ( seen_time ) ` ,
hostID , time . Now ( ) . UTC ( ) )
if err != nil {
return ctxerr . Wrap ( ctx , err , "new host seen time" )
}
2020-12-10 19:04:58 +00:00
sqlSelect := `
SELECT * FROM hosts WHERE id = ? LIMIT 1
`
2021-12-01 12:05:23 +00:00
err = sqlx . GetContext ( ctx , tx , & host , sqlSelect , hostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "getting the host to return" )
2020-12-10 19:04:58 +00:00
}
2021-12-01 12:05:23 +00:00
_ , err = tx . ExecContext ( ctx , ` INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, (SELECT id FROM labels WHERE name = 'All Hosts' AND label_type = 1)) ` , hostID )
2020-12-10 19:04:58 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert new host into all hosts label" )
2020-12-10 19:04:58 +00:00
}
return nil
} )
if err != nil {
return nil , err
}
return & host , nil
2016-11-16 13:47:49 +00:00
}
2022-03-09 21:13:56 +00:00
// getContextTryStmt will attempt to run sqlx.GetContext on a cached statement if available, resorting to ds.reader.
func ( ds * Datastore ) getContextTryStmt ( ctx context . Context , dest interface { } , query string , args ... interface { } ) error {
2022-02-14 15:13:38 +00:00
var err error
//nolint the statements are closed in Datastore.Close.
if stmt := ds . loadOrPrepareStmt ( ctx , query ) ; stmt != nil {
err = stmt . GetContext ( ctx , dest , args ... )
} else {
err = sqlx . GetContext ( ctx , ds . reader , dest , query , args ... )
}
return err
}
2022-01-18 01:52:09 +00:00
// LoadHostByNodeKey loads the whole host identified by the node key.
// If the node key is invalid it returns a NotFoundError.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) LoadHostByNodeKey ( ctx context . Context , nodeKey string ) ( * fleet . Host , error ) {
2022-02-14 15:13:38 +00:00
query := ` SELECT * FROM hosts WHERE node_key = ? `
2022-01-18 01:52:09 +00:00
var host fleet . Host
2022-03-09 21:13:56 +00:00
switch err := ds . getContextTryStmt ( ctx , & host , query , nodeKey ) ; {
case err == nil :
return & host , nil
case errors . Is ( err , sql . ErrNoRows ) :
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) )
default :
return nil , ctxerr . Wrap ( ctx , err , "find host" )
}
}
// LoadHostByDeviceAuthToken loads the whole host identified by the device auth token.
// If the token is invalid it returns a NotFoundError.
func ( ds * Datastore ) LoadHostByDeviceAuthToken ( ctx context . Context , authToken string ) ( * fleet . Host , error ) {
const query = `
SELECT
h . *
FROM
host_device_auth hda
INNER JOIN
hosts h
ON
hda . host_id = h . id
WHERE hda . token = ? `
var host fleet . Host
switch err := sqlx . GetContext ( ctx , ds . reader , & host , query , authToken ) ; {
2022-01-18 01:52:09 +00:00
case err == nil :
return & host , nil
case errors . Is ( err , sql . ErrNoRows ) :
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) )
default :
return nil , ctxerr . Wrap ( ctx , err , "find host" )
2016-11-16 13:47:49 +00:00
}
}
2022-03-15 19:51:00 +00:00
// SetOrUpdateDeviceAuthToken inserts or updates the auth token for a host.
func ( ds * Datastore ) SetOrUpdateDeviceAuthToken ( ctx context . Context , hostID uint , authToken string ) error {
return ds . updateOrInsert (
ctx ,
2022-04-15 20:09:47 +00:00
` UPDATE host_device_auth SET token = ? WHERE host_id = ? ` ,
` INSERT INTO host_device_auth (token, host_id) VALUES (?, ?) ` ,
2022-03-15 19:51:00 +00:00
authToken , hostID ,
)
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) MarkHostsSeen ( ctx context . Context , hostIDs [ ] uint , t time . Time ) error {
2021-04-12 23:22:22 +00:00
if len ( hostIDs ) == 0 {
return nil
}
2021-12-01 19:20:54 +00:00
// Sort by host id to prevent deadlocks:
// https://percona.community/blog/2018/09/24/minimize-mysql-deadlocks-3-steps/
// https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks-handling.html
sort . Slice ( hostIDs , func ( i , j int ) bool { return hostIDs [ i ] < hostIDs [ j ] } )
2022-02-03 17:56:22 +00:00
if err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2021-12-01 12:05:23 +00:00
var insertArgs [ ] interface { }
for _ , hostID := range hostIDs {
insertArgs = append ( insertArgs , hostID , t )
2021-04-12 23:22:22 +00:00
}
2021-12-01 12:05:23 +00:00
insertValues := strings . TrimSuffix ( strings . Repeat ( "(?, ?)," , len ( hostIDs ) ) , "," )
query := fmt . Sprintf ( `
INSERT INTO host_seen_times ( host_id , seen_time ) VALUES % s
ON DUPLICATE KEY UPDATE seen_time = VALUES ( seen_time ) ` ,
insertValues ,
)
if _ , err := tx . ExecContext ( ctx , query , insertArgs ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "exec update" )
2021-04-12 23:22:22 +00:00
}
return nil
} ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "MarkHostsSeen transaction" )
2021-04-12 23:22:22 +00:00
}
return nil
}
2021-10-21 20:46:21 +00:00
// SearchHosts performs a search on the hosts table using the following criteria:
// - Use the provided team filter.
2022-02-22 13:19:51 +00:00
// - Search hostname, uuid, hardware_serial, and primary_ip using LIKE (mimics ListHosts behavior)
2021-10-21 20:46:21 +00:00
// - An optional list of IDs to omit from the search.
2022-02-22 13:19:51 +00:00
func ( ds * Datastore ) SearchHosts ( ctx context . Context , filter fleet . TeamFilter , matchQuery string , omit ... uint ) ( [ ] * fleet . Host , error ) {
query := ` SELECT
2021-12-01 12:05:23 +00:00
h . * ,
COALESCE ( hst . seen_time , h . created_at ) AS seen_time
FROM hosts h
LEFT JOIN host_seen_times hst
2022-04-15 20:09:47 +00:00
ON ( h . id = hst . host_id ) WHERE TRUE `
2016-12-01 17:00:00 +00:00
2021-10-21 20:46:21 +00:00
var args [ ] interface { }
2022-02-22 13:19:51 +00:00
if len ( matchQuery ) > 0 {
query , args = hostSearchLike ( query , args , matchQuery , hostSearchColumns ... )
2016-11-16 13:47:49 +00:00
}
2017-01-17 14:51:04 +00:00
var in interface { }
2021-10-21 20:46:21 +00:00
// use -1 if there are no values to omit.
// Avoids empty args error for `sqlx.In`
in = omit
if len ( omit ) == 0 {
in = - 1
}
args = append ( args , in )
2022-02-22 13:19:51 +00:00
query += " AND id NOT IN (?) AND "
query += ds . whereFilterHostsByTeams ( filter , "h" )
query += ` ORDER BY h.id DESC LIMIT 10 `
2021-10-21 20:46:21 +00:00
2022-02-22 13:19:51 +00:00
query , args , err := sqlx . In ( query , args ... )
2017-01-17 14:51:04 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "searching default hosts" )
2017-01-17 14:51:04 +00:00
}
2022-02-22 13:19:51 +00:00
query = ds . reader . Rebind ( query )
2021-06-06 22:07:29 +00:00
hosts := [ ] * fleet . Host { }
2022-02-22 13:19:51 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hosts , query , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "searching hosts" )
2016-11-16 13:47:49 +00:00
}
return hosts , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostIDsByName ( ctx context . Context , filter fleet . TeamFilter , hostnames [ ] string ) ( [ ] uint , error ) {
2020-01-24 05:27:20 +00:00
if len ( hostnames ) == 0 {
return [ ] uint { } , nil
}
2021-06-04 01:53:43 +00:00
sqlStatement := fmt . Sprintf ( `
SELECT id FROM hosts
2021-06-24 00:32:19 +00:00
WHERE hostname IN ( ? ) AND % s
2022-02-03 17:56:22 +00:00
` , ds . whereFilterHostsByTeams ( filter , "hosts" ) ,
2021-06-04 01:53:43 +00:00
)
2018-05-17 22:54:34 +00:00
sql , args , err := sqlx . In ( sqlStatement , hostnames )
if err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "building query to get host IDs" )
2018-05-17 22:54:34 +00:00
}
var hostIDs [ ] uint
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & hostIDs , sql , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host IDs" )
2018-05-17 22:54:34 +00:00
}
return hostIDs , nil
}
2020-04-22 20:54:32 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostByIdentifier ( ctx context . Context , identifier string ) ( * fleet . Host , error ) {
2021-12-14 21:34:11 +00:00
stmt := `
2022-07-05 11:08:43 +00:00
SELECT h . * , COALESCE ( hst . seen_time , h . created_at ) AS seen_time
FROM hosts h
LEFT JOIN host_seen_times hst
ON ( h . id = hst . host_id )
WHERE ? IN ( h . hostname , h . osquery_host_id , h . node_key , h . uuid )
2020-04-22 20:54:32 +00:00
LIMIT 1
`
2021-06-06 22:07:29 +00:00
host := & fleet . Host { }
2022-02-03 17:56:22 +00:00
err := sqlx . GetContext ( ctx , ds . reader , host , stmt , identifier )
2020-04-22 20:54:32 +00:00
if err != nil {
2021-12-14 21:34:11 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithName ( identifier ) )
}
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host by identifier" )
2020-04-22 20:54:32 +00:00
}
2022-02-03 17:56:22 +00:00
packStats , err := loadHostPackStatsDB ( ctx , ds . reader , host . ID , host . Platform )
2021-11-12 11:18:25 +00:00
if err != nil {
2021-05-07 04:05:09 +00:00
return nil , err
}
2021-11-12 11:18:25 +00:00
host . PackStats = packStats
2021-05-07 04:05:09 +00:00
2020-04-22 20:54:32 +00:00
return host , nil
}
2021-05-17 19:23:21 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) AddHostsToTeam ( ctx context . Context , teamID * uint , hostIDs [ ] uint ) error {
2021-05-17 19:23:21 +00:00
if len ( hostIDs ) == 0 {
return nil
}
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2022-05-30 13:30:15 +00:00
if err := cleanupPolicyMembershipOnTeamChange ( ctx , tx , hostIDs ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "AddHostsToTeam delete policy membership" )
2021-10-05 18:48:26 +00:00
}
2021-05-17 19:23:21 +00:00
2022-05-30 13:30:15 +00:00
query , args , err := sqlx . In ( ` UPDATE hosts SET team_id = ? WHERE id IN (?) ` , teamID , hostIDs )
2021-10-05 18:48:26 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "sqlx.In AddHostsToTeam" )
2021-10-05 18:48:26 +00:00
}
2021-05-17 19:23:21 +00:00
2021-10-05 18:48:26 +00:00
if _ , err := tx . ExecContext ( ctx , query , args ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "exec AddHostsToTeam" )
2021-10-05 18:48:26 +00:00
}
return nil
} )
2021-05-17 19:23:21 +00:00
}
2021-05-31 17:56:50 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostAdditional ( ctx context . Context , hostID uint , additional * json . RawMessage ) error {
return saveHostAdditionalDB ( ctx , ds . writer , hostID , additional )
2022-01-18 01:52:09 +00:00
}
func saveHostAdditionalDB ( ctx context . Context , exec sqlx . ExecerContext , hostID uint , additional * json . RawMessage ) error {
2021-05-26 23:24:12 +00:00
sql := `
INSERT INTO host_additional ( host_id , additional )
VALUES ( ? , ? )
ON DUPLICATE KEY UPDATE additional = VALUES ( additional )
`
2022-01-18 01:52:09 +00:00
if _ , err := exec . ExecContext ( ctx , sql , hostID , additional ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert additional" )
2021-05-26 23:24:12 +00:00
}
return nil
}
2021-07-13 20:15:38 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SaveHostUsers ( ctx context . Context , hostID uint , users [ ] fleet . HostUser ) error {
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2022-01-18 01:52:09 +00:00
return saveHostUsersDB ( ctx , tx , hostID , users )
} )
}
func saveHostUsersDB ( ctx context . Context , tx sqlx . ExtContext , hostID uint , users [ ] fleet . HostUser ) error {
currentHostUsers , err := loadHostUsersDB ( ctx , tx , hostID )
if err != nil {
2021-07-13 20:15:38 +00:00
return err
}
2021-09-07 14:02:35 +00:00
keyForUser := func ( u * fleet . HostUser ) string { return fmt . Sprintf ( "%d\x00%s" , u . Uid , u . Username ) }
incomingUsers := make ( map [ string ] bool )
var insertArgs [ ] interface { }
2022-01-18 01:52:09 +00:00
for _ , u := range users {
insertArgs = append ( insertArgs , hostID , u . Uid , u . Username , u . Type , u . GroupName , u . Shell )
2021-09-07 14:02:35 +00:00
incomingUsers [ keyForUser ( & u ) ] = true
2021-07-13 20:15:38 +00:00
}
var removedArgs [ ] interface { }
2022-01-18 01:52:09 +00:00
for _ , u := range currentHostUsers {
2021-09-07 14:02:35 +00:00
if _ , ok := incomingUsers [ keyForUser ( & u ) ] ; ! ok {
removedArgs = append ( removedArgs , u . Username )
2021-07-13 20:15:38 +00:00
}
}
2022-01-18 01:52:09 +00:00
insertValues := strings . TrimSuffix ( strings . Repeat ( "(?, ?, ?, ?, ?, ?)," , len ( users ) ) , "," )
2021-09-07 14:02:35 +00:00
insertSql := fmt . Sprintf (
2021-12-14 21:34:11 +00:00
` INSERT INTO host_users ( host_id , uid , username , user_type , groupname , shell )
VALUES % s
2021-11-23 13:23:12 +00:00
ON DUPLICATE KEY UPDATE
user_type = VALUES ( user_type ) ,
groupname = VALUES ( groupname ) ,
shell = VALUES ( shell ) ,
2022-04-15 20:09:47 +00:00
removed_at = NULL ` ,
2021-09-07 14:02:35 +00:00
insertValues ,
)
2021-09-14 14:44:02 +00:00
if _ , err := tx . ExecContext ( ctx , insertSql , insertArgs ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "insert users" )
2021-09-07 14:02:35 +00:00
}
2021-07-13 20:15:38 +00:00
if len ( removedArgs ) == 0 {
return nil
}
removedValues := strings . TrimSuffix ( strings . Repeat ( "?," , len ( removedArgs ) ) , "," )
removedSql := fmt . Sprintf (
2021-09-07 14:02:35 +00:00
` UPDATE host_users SET removed_at = CURRENT_TIMESTAMP WHERE host_id = ? and username IN (%s) ` ,
2021-07-13 20:15:38 +00:00
removedValues ,
)
2022-01-18 01:52:09 +00:00
if _ , err := tx . ExecContext ( ctx , removedSql , append ( [ ] interface { } { hostID } , removedArgs ... ) ... ) ; err != nil {
2021-11-15 14:11:38 +00:00
return ctxerr . Wrap ( ctx , err , "mark users as removed" )
2021-07-13 20:15:38 +00:00
}
return nil
}
2021-08-27 14:15:36 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) TotalAndUnseenHostsSince ( ctx context . Context , daysCount int ) ( total int , unseen int , err error ) {
2021-12-01 12:05:23 +00:00
var counts struct {
Total int ` db:"total" `
Unseen int ` db:"unseen" `
2021-08-27 14:15:36 +00:00
}
2022-05-25 15:54:56 +00:00
// convert daysCount to integer number of seconds for more precision in sql query
unseenSeconds := daysCount * 24 * 60 * 60
2022-02-03 17:56:22 +00:00
err = sqlx . GetContext ( ctx , ds . reader , & counts ,
2021-12-01 12:05:23 +00:00
` SELECT
COUNT ( * ) as total ,
2022-05-25 15:54:56 +00:00
SUM ( IF ( TIMESTAMPDIFF ( SECOND , COALESCE ( hst . seen_time , h . created_at ) , CURRENT_TIMESTAMP ) >= ? , 1 , 0 ) ) as unseen
2021-12-01 12:05:23 +00:00
FROM hosts h
LEFT JOIN host_seen_times hst
ON h . id = hst . host_id ` ,
2022-05-25 15:54:56 +00:00
unseenSeconds ,
2021-09-01 19:50:52 +00:00
)
2022-05-25 15:54:56 +00:00
2021-08-27 14:15:36 +00:00
if err != nil {
2021-12-01 12:05:23 +00:00
return 0 , 0 , ctxerr . Wrap ( ctx , err , "getting total and unseen host counts" )
2021-08-27 14:15:36 +00:00
}
2021-12-01 12:05:23 +00:00
return counts . Total , counts . Unseen , nil
2021-08-27 14:15:36 +00:00
}
2021-09-29 16:13:23 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) DeleteHosts ( ctx context . Context , ids [ ] uint ) error {
2022-03-23 15:15:05 +00:00
for _ , id := range ids {
if err := ds . DeleteHost ( ctx , id ) ; err != nil {
return ctxerr . Wrapf ( ctx , err , "delete host %d" , id )
}
2022-01-19 13:28:08 +00:00
}
2021-09-29 16:13:23 +00:00
return nil
}
2021-10-07 11:11:10 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListPoliciesForHost ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
2021-12-03 18:33:33 +00:00
if host . FleetPlatform ( ) == "" {
// We log to help troubleshooting in case this happens.
2022-02-03 17:56:22 +00:00
level . Error ( ds . logger ) . Log ( "err" , fmt . Sprintf ( "host %d with empty platform" , host . ID ) )
2021-12-03 18:33:33 +00:00
}
2021-11-24 17:16:42 +00:00
query := ` SELECT p . * ,
COALESCE ( u . name , ' < deleted > ' ) AS author_name ,
COALESCE ( u . email , ' ' ) AS author_email ,
2021-10-07 11:11:10 +00:00
CASE
2021-11-09 14:35:36 +00:00
WHEN pm . passes = 1 THEN ' pass '
WHEN pm . passes = 0 THEN ' fail '
ELSE ' '
2021-10-20 15:07:16 +00:00
END AS response ,
2021-10-21 18:53:23 +00:00
coalesce ( p . resolution , ' ' ) as resolution
2021-11-11 11:40:32 +00:00
FROM policies p
2021-12-03 16:10:11 +00:00
LEFT JOIN policy_membership pm ON ( p . id = pm . policy_id AND host_id = ? )
2021-11-24 21:17:44 +00:00
LEFT JOIN users u ON p . author_id = u . id
2021-12-03 18:33:33 +00:00
WHERE ( p . team_id IS NULL OR p . team_id = ( select team_id from hosts WHERE id = ? ) )
2022-04-11 18:47:50 +00:00
AND ( p . platforms IS NULL OR p . platforms = ' ' OR FIND_IN_SET ( ? , p . platforms ) != 0 ) `
2021-10-07 11:11:10 +00:00
var policies [ ] * fleet . HostPolicy
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & policies , query , host . ID , host . ID , host . FleetPlatform ( ) ) ; err != nil {
2021-11-15 14:11:38 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get host policies" )
2021-10-07 11:11:10 +00:00
}
return policies , nil
}
2021-10-19 20:47:37 +00:00
2022-06-13 20:29:32 +00:00
func ( ds * Datastore ) CleanupExpiredHosts ( ctx context . Context ) ( [ ] uint , error ) {
2022-02-03 17:56:22 +00:00
ac , err := appConfigDB ( ctx , ds . reader )
2021-10-19 20:47:37 +00:00
if err != nil {
2022-06-13 20:29:32 +00:00
return nil , ctxerr . Wrap ( ctx , err , "getting app config" )
2021-10-19 20:47:37 +00:00
}
if ! ac . HostExpirySettings . HostExpiryEnabled {
2022-06-13 20:29:32 +00:00
return nil , nil
2021-10-19 20:47:37 +00:00
}
2021-11-08 14:42:37 +00:00
// Usual clean up queries used to be like this:
// DELETE FROM hosts WHERE id in (SELECT host_id FROM host_seen_times WHERE seen_time < DATE_SUB(NOW(), INTERVAL ? DAY))
// This means a full table scan for hosts, and for big deployments, that's not ideal
// so instead, we get the ids one by one and delete things one by one
// it might take longer, but it should lock only the row we need
2022-06-13 20:29:32 +00:00
var ids [ ] uint
err = ds . writer . SelectContext (
2021-11-08 14:42:37 +00:00
ctx ,
2022-06-13 20:29:32 +00:00
& ids ,
2021-12-01 12:05:23 +00:00
` SELECT h . id FROM hosts h
LEFT JOIN host_seen_times hst
ON h . id = hst . host_id
WHERE COALESCE ( hst . seen_time , h . created_at ) < DATE_SUB ( NOW ( ) , INTERVAL ? DAY ) ` ,
2021-11-08 14:42:37 +00:00
ac . HostExpirySettings . HostExpiryWindow ,
)
if err != nil {
2022-06-13 20:29:32 +00:00
return nil , ctxerr . Wrap ( ctx , err , "getting expired host ids" )
2021-11-08 14:42:37 +00:00
}
2022-06-13 20:29:32 +00:00
for _ , id := range ids {
2022-02-03 17:56:22 +00:00
err = ds . DeleteHost ( ctx , id )
2021-11-08 14:42:37 +00:00
if err != nil {
2022-06-13 20:29:32 +00:00
return nil , err
2021-12-21 20:36:19 +00:00
}
2021-11-08 14:42:37 +00:00
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , ` DELETE FROM host_seen_times WHERE seen_time < DATE_SUB(NOW(), INTERVAL ? DAY) ` , ac . HostExpirySettings . HostExpiryWindow )
2021-10-19 20:47:37 +00:00
if err != nil {
2022-06-13 20:29:32 +00:00
return nil , ctxerr . Wrap ( ctx , err , "deleting expired host seen times" )
2021-10-19 20:47:37 +00:00
}
2022-06-13 20:29:32 +00:00
return ids , nil
2021-10-19 20:47:37 +00:00
}
2021-12-21 12:37:58 +00:00
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ListHostDeviceMapping ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
2021-12-21 20:36:19 +00:00
stmt := `
SELECT
id ,
host_id ,
email ,
source
FROM
host_emails
WHERE
host_id = ?
ORDER BY
email , source `
var mappings [ ] * fleet . HostDeviceMapping
2022-02-03 17:56:22 +00:00
err := sqlx . SelectContext ( ctx , ds . reader , & mappings , stmt , id )
2021-12-21 20:36:19 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "select host emails by host id" )
}
return mappings , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) ReplaceHostDeviceMapping ( ctx context . Context , hid uint , mappings [ ] * fleet . HostDeviceMapping ) error {
2021-12-21 20:36:19 +00:00
for _ , m := range mappings {
if hid != m . HostID {
return ctxerr . Errorf ( ctx , "host device mapping are not all for the provided host id %d, found %d" , hid , m . HostID )
}
}
// the following SQL statements assume a small number of emails reported
// per host.
const (
selStmt = `
SELECT
id ,
email ,
source
FROM
host_emails
WHERE
host_id = ? `
delStmt = `
DELETE FROM
host_emails
WHERE
id IN ( ? ) `
insStmt = `
INSERT INTO
host_emails ( host_id , email , source )
VALUES `
insPart = ` (?, ?, ?), `
)
// index the mappings by email and source, to quickly check which ones
// need to be deleted and inserted
toIns := make ( map [ string ] * fleet . HostDeviceMapping )
for _ , m := range mappings {
toIns [ m . Email + "\n" + m . Source ] = m
}
2022-02-03 17:56:22 +00:00
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2021-12-21 20:36:19 +00:00
var prevMappings [ ] * fleet . HostDeviceMapping
if err := sqlx . SelectContext ( ctx , tx , & prevMappings , selStmt , hid ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "select previous host emails" )
}
var delIDs [ ] uint
for _ , pm := range prevMappings {
key := pm . Email + "\n" + pm . Source
if _ , ok := toIns [ key ] ; ok {
// already exists, no need to insert
delete ( toIns , key )
} else {
// does not exist anymore, must be deleted
delIDs = append ( delIDs , pm . ID )
}
}
if len ( delIDs ) > 0 {
stmt , args , err := sqlx . In ( delStmt , delIDs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "prepare delete statement" )
}
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete host emails" )
}
}
if len ( toIns ) > 0 {
var args [ ] interface { }
for _ , m := range toIns {
args = append ( args , hid , m . Email , m . Source )
}
stmt := insStmt + strings . TrimSuffix ( strings . Repeat ( insPart , len ( toIns ) ) , "," )
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "insert host emails" )
}
}
return nil
} )
}
2022-06-28 18:11:49 +00:00
func ( ds * Datastore ) ReplaceHostBatteries ( ctx context . Context , hid uint , mappings [ ] * fleet . HostBattery ) error {
const (
replaceStmt = `
INSERT INTO
host_batteries (
host_id ,
serial_number ,
cycle_count ,
health
)
VALUES
% s
ON DUPLICATE KEY UPDATE
cycle_count = VALUES ( cycle_count ) ,
health = VALUES ( health ) ,
updated_at = CURRENT_TIMESTAMP
`
valuesPart = ` (?, ?, ?, ?), `
deleteExceptStmt = `
DELETE FROM
host_batteries
WHERE
host_id = ? AND
serial_number NOT IN ( ? )
`
deleteAllStmt = `
DELETE FROM
host_batteries
WHERE
host_id = ?
`
)
replaceArgs := make ( [ ] interface { } , 0 , len ( mappings ) * 4 )
deleteNotIn := make ( [ ] string , 0 , len ( mappings ) )
for _ , hb := range mappings {
deleteNotIn = append ( deleteNotIn , hb . SerialNumber )
replaceArgs = append ( replaceArgs , hid , hb . SerialNumber , hb . CycleCount , hb . Health )
}
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
// first, insert the new batteries or update the existing ones
if len ( replaceArgs ) > 0 {
if _ , err := tx . ExecContext ( ctx , fmt . Sprintf ( replaceStmt , strings . TrimSuffix ( strings . Repeat ( valuesPart , len ( mappings ) ) , "," ) ) , replaceArgs ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "upsert host batteries" )
}
}
// then, delete the old ones
if len ( deleteNotIn ) > 0 {
delStmt , args , err := sqlx . In ( deleteExceptStmt , hid , deleteNotIn )
if err != nil {
return ctxerr . Wrap ( ctx , err , "generating host batteries delete NOT IN statement" )
}
if _ , err := tx . ExecContext ( ctx , delStmt , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete host batteries" )
}
} else if _ , err := tx . ExecContext ( ctx , deleteAllStmt , hid ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "delete all host batteries" )
}
return nil
} )
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) updateOrInsert ( ctx context . Context , updateQuery string , insertQuery string , args ... interface { } ) error {
res , err := ds . writer . ExecContext ( ctx , updateQuery , args ... )
2021-12-21 12:37:58 +00:00
if err != nil {
2022-08-10 19:15:01 +00:00
return ctxerr . Wrap ( ctx , err , "update" )
2021-12-21 12:37:58 +00:00
}
affected , err := res . RowsAffected ( )
if err != nil {
2022-08-10 19:15:01 +00:00
return ctxerr . Wrap ( ctx , err , "rows affected by update" )
2021-12-21 12:37:58 +00:00
}
if affected == 0 {
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx , insertQuery , args ... )
2021-12-21 12:37:58 +00:00
}
2022-08-10 19:15:01 +00:00
return ctxerr . Wrap ( ctx , err , "insert" )
2021-12-21 12:37:58 +00:00
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SetOrUpdateMunkiVersion ( ctx context . Context , hostID uint , version string ) error {
2022-02-15 19:29:14 +00:00
if version == "" {
// Only update deleted_at if there wasn't any deleted at for this host
2022-04-15 20:09:47 +00:00
updateQuery := ` UPDATE host_munki_info SET deleted_at = NOW() WHERE host_id = ? AND deleted_at is NULL `
2022-02-15 19:29:14 +00:00
_ , err := ds . writer . ExecContext ( ctx , updateQuery , hostID )
if err != nil {
return ctxerr . Wrap ( ctx , err )
}
return nil
}
2022-02-03 17:56:22 +00:00
return ds . updateOrInsert (
2021-12-21 12:37:58 +00:00
ctx ,
2022-04-15 20:09:47 +00:00
` UPDATE host_munki_info SET version = ? WHERE host_id = ? ` ,
` INSERT INTO host_munki_info (version, host_id) VALUES (?, ?) ` ,
2021-12-21 12:37:58 +00:00
version , hostID ,
)
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) SetOrUpdateMDMData ( ctx context . Context , hostID uint , enrolled bool , serverURL string , installedFromDep bool ) error {
2022-08-10 19:15:01 +00:00
mdmID , err := ds . getOrInsertMDMSolution ( ctx , serverURL )
if err != nil {
return err
}
2022-02-03 17:56:22 +00:00
return ds . updateOrInsert (
2021-12-21 12:37:58 +00:00
ctx ,
2022-08-10 19:15:01 +00:00
` UPDATE host_mdm SET enrolled = ?, server_url = ?, installed_from_dep = ?, mdm_id = ? WHERE host_id = ? ` ,
` INSERT INTO host_mdm (enrolled, server_url, installed_from_dep, mdm_id, host_id) VALUES (?, ?, ?, ?, ?) ` ,
enrolled , serverURL , installedFromDep , mdmID , hostID ,
2021-12-21 12:37:58 +00:00
)
}
2022-08-10 19:15:01 +00:00
func ( ds * Datastore ) getOrInsertMDMSolution ( ctx context . Context , serverURL string ) ( mdmID uint , err error ) {
mdmName := fleet . MDMNameFromServerURL ( serverURL )
readID := func ( q sqlx . QueryerContext ) ( uint , error ) {
var id uint
err := sqlx . GetContext ( ctx , q , & id ,
` SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ? ` , mdmName , serverURL )
return id , err
}
// optimistic approach, as mdm solutions will already exist the vast majority of the time
id , err := readID ( ds . reader )
if err != nil {
if errors . Is ( err , sql . ErrNoRows ) {
// this mdm solution does not exist yet, try to insert it
res , err := ds . writer . ExecContext ( ctx , ` INSERT INTO mobile_device_management_solutions (name, server_url) VALUES (?, ?) ` , mdmName , serverURL )
if err != nil {
if isDuplicate ( err ) {
// it might've been created between the select and the insert, read
// again this time from the writer database connection.
id , err := readID ( ds . writer )
if err != nil {
return 0 , ctxerr . Wrap ( ctx , err , "get mdm id from writer" )
}
return id , nil
}
return 0 , ctxerr . Wrap ( ctx , err , "insert mdm solution" )
}
id , _ := res . LastInsertId ( )
return uint ( id ) , nil
}
return 0 , ctxerr . Wrap ( ctx , err , "get mdm id from reader" )
}
return id , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GetMunkiVersion ( ctx context . Context , hostID uint ) ( string , error ) {
2021-12-21 12:37:58 +00:00
var version string
2022-04-15 20:09:47 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & version , ` SELECT version FROM host_munki_info WHERE deleted_at is NULL AND host_id = ? ` , hostID )
2021-12-21 12:37:58 +00:00
if err != nil {
if err == sql . ErrNoRows {
return "" , ctxerr . Wrap ( ctx , notFound ( "MunkiInfo" ) . WithID ( hostID ) )
}
return "" , ctxerr . Wrapf ( ctx , err , "getting data from host_munki_info for host_id %d" , hostID )
}
return version , nil
}
2022-08-10 19:15:01 +00:00
func ( ds * Datastore ) GetMDM ( ctx context . Context , hostID uint ) ( * fleet . HostMDM , error ) {
var hmdm fleet . HostMDM
err := sqlx . GetContext ( ctx , ds . reader , & hmdm , `
SELECT
hm . host_id , hm . enrolled , hm . server_url , hm . installed_from_dep , hm . mdm_id , COALESCE ( mdms . name , ? ) AS name
FROM
host_mdm hm
LEFT OUTER JOIN
mobile_device_management_solutions mdms
ON hm . mdm_id = mdms . id
WHERE hm . host_id = ? ` , fleet . UnknownMDMName , hostID )
2021-12-21 12:37:58 +00:00
if err != nil {
if err == sql . ErrNoRows {
2022-08-10 19:15:01 +00:00
return nil , ctxerr . Wrap ( ctx , notFound ( "MDM" ) . WithID ( hostID ) )
2021-12-21 12:37:58 +00:00
}
2022-08-10 19:15:01 +00:00
return nil , ctxerr . Wrapf ( ctx , err , "getting data from host_mdm for host_id %d" , hostID )
2021-12-21 12:37:58 +00:00
}
2022-08-10 19:15:01 +00:00
return & hmdm , nil
2021-12-21 12:37:58 +00:00
}
2022-02-14 15:13:38 +00:00
2022-02-07 17:53:33 +00:00
func ( ds * Datastore ) AggregatedMunkiVersion ( ctx context . Context , teamID * uint ) ( [ ] fleet . AggregatedMunkiVersion , time . Time , error ) {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
if teamID != nil {
id = * teamID
}
var versions [ ] fleet . AggregatedMunkiVersion
2022-02-07 17:53:33 +00:00
var versionsJson struct {
JsonValue [ ] byte ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
err := sqlx . GetContext (
ctx , ds . reader , & versionsJson ,
2022-04-15 20:09:47 +00:00
` SELECT json_value, updated_at FROM aggregated_stats WHERE id = ? AND type = 'munki_versions' ` ,
2022-02-07 17:53:33 +00:00
id ,
)
2022-01-26 20:55:07 +00:00
if err != nil {
if err == sql . ErrNoRows {
// not having stats is not an error
2022-02-07 17:53:33 +00:00
return nil , time . Time { } , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "selecting munki versions" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
if err := json . Unmarshal ( versionsJson . JsonValue , & versions ) ; err != nil {
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "unmarshaling munki versions" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return versions , versionsJson . UpdatedAt , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
func ( ds * Datastore ) AggregatedMDMStatus ( ctx context . Context , teamID * uint ) ( fleet . AggregatedMDMStatus , time . Time , error ) {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
if teamID != nil {
id = * teamID
}
var status fleet . AggregatedMDMStatus
2022-02-07 17:53:33 +00:00
var statusJson struct {
JsonValue [ ] byte ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
err := sqlx . GetContext (
ctx , ds . reader , & statusJson ,
2022-04-15 20:09:47 +00:00
` select json_value, updated_at from aggregated_stats where id = ? and type = 'mdm_status' ` ,
2022-02-07 17:53:33 +00:00
id ,
)
2022-01-26 20:55:07 +00:00
if err != nil {
if err == sql . ErrNoRows {
// not having stats is not an error
2022-02-07 17:53:33 +00:00
return fleet . AggregatedMDMStatus { } , time . Time { } , nil
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return fleet . AggregatedMDMStatus { } , time . Time { } , ctxerr . Wrap ( ctx , err , "selecting mdm status" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
if err := json . Unmarshal ( statusJson . JsonValue , & status ) ; err != nil {
return fleet . AggregatedMDMStatus { } , time . Time { } , ctxerr . Wrap ( ctx , err , "unmarshaling mdm status" )
2022-01-26 20:55:07 +00:00
}
2022-02-07 17:53:33 +00:00
return status , statusJson . UpdatedAt , nil
2022-01-26 20:55:07 +00:00
}
2022-08-10 19:15:01 +00:00
func ( ds * Datastore ) AggregatedMDMSolutions ( ctx context . Context , teamID * uint ) ( [ ] fleet . AggregatedMDMSolutions , time . Time , error ) {
id := uint ( 0 )
if teamID != nil {
id = * teamID
}
var result [ ] fleet . AggregatedMDMSolutions
var resultJSON struct {
JsonValue [ ] byte ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
err := sqlx . GetContext (
ctx , ds . reader , & resultJSON ,
` SELECT json_value, updated_at FROM aggregated_stats WHERE id = ? AND type = 'mdm_solutions' ` ,
id ,
)
if err != nil {
if err == sql . ErrNoRows {
// not having stats is not an error
return nil , time . Time { } , nil
}
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "selecting mdm solutions" )
}
if err := json . Unmarshal ( resultJSON . JsonValue , & result ) ; err != nil {
return nil , time . Time { } , ctxerr . Wrap ( ctx , err , "unmarshaling mdm solutions" )
}
return result , resultJSON . UpdatedAt , nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) GenerateAggregatedMunkiAndMDM ( ctx context . Context ) error {
2022-01-26 20:55:07 +00:00
var ids [ ] uint
2022-02-03 17:56:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader , & ids , ` SELECT id FROM teams ` ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "list teams" )
}
for _ , id := range ids {
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMunkiVersion ( ctx , & id ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated munki version" )
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMDMStatus ( ctx , & id ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm status" )
}
2022-08-10 19:15:01 +00:00
if err := ds . generateAggregatedMDMSolutions ( ctx , & id ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm solutions" )
}
2022-01-26 20:55:07 +00:00
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMunkiVersion ( ctx , nil ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated munki version" )
}
2022-02-03 17:56:22 +00:00
if err := ds . generateAggregatedMDMStatus ( ctx , nil ) ; err != nil {
2022-01-26 20:55:07 +00:00
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm status" )
}
2022-08-10 19:15:01 +00:00
if err := ds . generateAggregatedMDMSolutions ( ctx , nil ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "generating aggregated mdm solutions" )
}
2022-01-26 20:55:07 +00:00
return nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) generateAggregatedMunkiVersion ( ctx context . Context , teamID * uint ) error {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
var versions [ ] fleet . AggregatedMunkiVersion
query := ` SELECT count(*) as hosts_count, hm.version FROM host_munki_info hm `
args := [ ] interface { } { }
if teamID != nil {
args = append ( args , * teamID )
2022-04-15 20:09:47 +00:00
query += ` JOIN hosts h ON (h.id = hm.host_id) WHERE h.team_id = ? AND `
2022-01-26 20:55:07 +00:00
id = * teamID
2022-02-15 19:29:14 +00:00
} else {
query += ` WHERE `
2022-01-26 20:55:07 +00:00
}
2022-04-15 20:09:47 +00:00
query += ` hm.deleted_at IS NULL GROUP BY hm.version `
2022-02-03 17:56:22 +00:00
err := sqlx . SelectContext ( ctx , ds . reader , & versions , query , args ... )
2022-01-26 20:55:07 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "getting aggregated data from host_munki" )
}
versionsJson , err := json . Marshal ( versions )
if err != nil {
return ctxerr . Wrap ( ctx , err , "marshaling stats" )
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx ,
2022-04-15 20:09:47 +00:00
`
INSERT INTO aggregated_stats ( id , type , json_value )
VALUES ( ? , ? , ? )
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
` ,
2022-01-26 20:55:07 +00:00
id , "munki_versions" , versionsJson ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting stats for munki_versions id %d" , id )
}
return nil
}
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) generateAggregatedMDMStatus ( ctx context . Context , teamID * uint ) error {
2022-01-26 20:55:07 +00:00
id := uint ( 0 )
var status fleet . AggregatedMDMStatus
query := ` SELECT
COUNT ( DISTINCT host_id ) as hosts_count ,
COALESCE ( SUM ( CASE WHEN NOT enrolled THEN 1 ELSE 0 END ) , 0 ) as unenrolled_hosts_count ,
COALESCE ( SUM ( CASE WHEN enrolled AND installed_from_dep THEN 1 ELSE 0 END ) , 0 ) as enrolled_automated_hosts_count ,
COALESCE ( SUM ( CASE WHEN enrolled AND NOT installed_from_dep THEN 1 ELSE 0 END ) , 0 ) as enrolled_manual_hosts_count
FROM host_mdm hm
`
args := [ ] interface { } { }
if teamID != nil {
args = append ( args , * teamID )
2022-04-15 20:09:47 +00:00
query += ` JOIN hosts h ON (h.id = hm.host_id) WHERE h.team_id = ? `
2022-01-26 20:55:07 +00:00
id = * teamID
}
2022-02-03 17:56:22 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & status , query , args ... )
2022-01-26 20:55:07 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "getting aggregated data from host_mdm" )
}
statusJson , err := json . Marshal ( status )
if err != nil {
return ctxerr . Wrap ( ctx , err , "marshaling stats" )
}
2022-02-03 17:56:22 +00:00
_ , err = ds . writer . ExecContext ( ctx ,
2022-04-15 20:09:47 +00:00
`
INSERT INTO aggregated_stats ( id , type , json_value )
VALUES ( ? , ? , ? )
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
` ,
2022-01-26 20:55:07 +00:00
id , "mdm_status" , statusJson ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting stats for mdm_status id %d" , id )
}
return nil
}
2022-01-18 01:52:09 +00:00
2022-08-10 19:15:01 +00:00
func ( ds * Datastore ) generateAggregatedMDMSolutions ( ctx context . Context , teamID * uint ) error {
id := uint ( 0 )
var results [ ] fleet . AggregatedMDMSolutions
query := ` SELECT
mdms . id ,
mdms . server_url ,
mdms . name ,
COUNT ( DISTINCT hm . host_id ) as hosts_count
FROM mobile_device_management_solutions mdms
INNER JOIN host_mdm hm
ON hm . mdm_id = mdms . id
`
args := [ ] interface { } { }
if teamID != nil {
args = append ( args , * teamID )
query += ` JOIN hosts h ON (h.id = hm.host_id) WHERE h.team_id = ? `
id = * teamID
}
query += ` GROUP BY id, server_url, name `
err := sqlx . SelectContext ( ctx , ds . reader , & results , query , args ... )
if err != nil {
return ctxerr . Wrapf ( ctx , err , "getting aggregated data from host_mdm" )
}
resultsJSON , err := json . Marshal ( results )
if err != nil {
return ctxerr . Wrap ( ctx , err , "marshaling stats" )
}
_ , err = ds . writer . ExecContext ( ctx ,
`
INSERT INTO aggregated_stats ( id , type , json_value )
VALUES ( ? , ? , ? )
ON DUPLICATE KEY UPDATE
json_value = VALUES ( json_value ) ,
updated_at = CURRENT_TIMESTAMP
` ,
id , "mdm_solutions" , resultsJSON ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "inserting stats for mdm_solutions id %d" , id )
}
return nil
}
2022-01-18 01:52:09 +00:00
// HostLite will load the primary data of the host with the given id.
// We define "primary data" as all host information except the
// details (like cpu, memory, gigs_disk_space_available, etc.).
//
// If the host doesn't exist, a NotFoundError is returned.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) HostLite ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
query , args , err := dialect . From ( goqu . I ( "hosts" ) ) . Select (
2022-01-18 01:52:09 +00:00
"id" ,
"created_at" ,
"updated_at" ,
"osquery_host_id" ,
"node_key" ,
"hostname" ,
"uuid" ,
"platform" ,
"team_id" ,
"distributed_interval" ,
"logger_tls_period" ,
"config_tls_refresh" ,
"detail_updated_at" ,
"label_updated_at" ,
"last_enrolled_at" ,
"policy_updated_at" ,
"refetch_requested" ,
2022-02-03 17:56:22 +00:00
) . Where ( goqu . I ( "id" ) . Eq ( id ) ) . ToSQL ( )
2022-01-18 01:52:09 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sql build" )
}
var host fleet . Host
2022-02-03 17:56:22 +00:00
if err := sqlx . GetContext ( ctx , ds . reader , & host , query , args ... ) ; err != nil {
2022-01-18 01:52:09 +00:00
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "Host" ) . WithID ( id ) )
}
return nil , ctxerr . Wrapf ( ctx , err , "load host %d" , id )
}
return & host , nil
}
// UpdateHostOsqueryIntervals updates the osquery intervals of a host.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHostOsqueryIntervals ( ctx context . Context , id uint , intervals fleet . HostOsqueryIntervals ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := `
UPDATE hosts SET
distributed_interval = ? ,
config_tls_refresh = ? ,
logger_tls_period = ?
WHERE id = ?
`
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement ,
2022-01-18 01:52:09 +00:00
intervals . DistributedInterval ,
intervals . ConfigTLSRefresh ,
intervals . LoggerTLSPeriod ,
id ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "update host %d osquery intervals" , id )
}
return nil
}
// UpdateHostRefetchRequested updates a host's refetch requested field.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHostRefetchRequested ( ctx context . Context , id uint , value bool ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := ` UPDATE hosts SET refetch_requested = ? WHERE id = ? `
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement , value , id )
2022-01-18 01:52:09 +00:00
if err != nil {
return ctxerr . Wrapf ( ctx , err , "update host %d refetch_requested" , id )
}
return nil
}
// UpdateHost updates a host.
//
// UpdateHost updates all columns of the `hosts` table.
// It only updates `hosts` table, other additional host information is ignored.
2022-02-03 17:56:22 +00:00
func ( ds * Datastore ) UpdateHost ( ctx context . Context , host * fleet . Host ) error {
2022-01-18 01:52:09 +00:00
sqlStatement := `
UPDATE hosts SET
detail_updated_at = ? ,
label_updated_at = ? ,
policy_updated_at = ? ,
node_key = ? ,
hostname = ? ,
uuid = ? ,
platform = ? ,
osquery_version = ? ,
os_version = ? ,
uptime = ? ,
memory = ? ,
cpu_type = ? ,
cpu_subtype = ? ,
cpu_brand = ? ,
cpu_physical_cores = ? ,
hardware_vendor = ? ,
hardware_model = ? ,
hardware_version = ? ,
hardware_serial = ? ,
computer_name = ? ,
build = ? ,
platform_like = ? ,
code_name = ? ,
cpu_logical_cores = ? ,
distributed_interval = ? ,
config_tls_refresh = ? ,
logger_tls_period = ? ,
team_id = ? ,
primary_ip = ? ,
primary_mac = ? ,
2022-03-21 16:29:52 +00:00
public_ip = ? ,
2022-01-18 01:52:09 +00:00
refetch_requested = ? ,
gigs_disk_space_available = ? ,
percent_disk_space_available = ?
WHERE id = ?
`
2022-02-03 17:56:22 +00:00
_ , err := ds . writer . ExecContext ( ctx , sqlStatement ,
2022-01-18 01:52:09 +00:00
host . DetailUpdatedAt ,
host . LabelUpdatedAt ,
host . PolicyUpdatedAt ,
host . NodeKey ,
host . Hostname ,
host . UUID ,
host . Platform ,
host . OsqueryVersion ,
host . OSVersion ,
host . Uptime ,
host . Memory ,
host . CPUType ,
host . CPUSubtype ,
host . CPUBrand ,
host . CPUPhysicalCores ,
host . HardwareVendor ,
host . HardwareModel ,
host . HardwareVersion ,
host . HardwareSerial ,
host . ComputerName ,
host . Build ,
host . PlatformLike ,
host . CodeName ,
host . CPULogicalCores ,
host . DistributedInterval ,
host . ConfigTLSRefresh ,
host . LoggerTLSPeriod ,
host . TeamID ,
host . PrimaryIP ,
host . PrimaryMac ,
2022-03-21 16:29:52 +00:00
host . PublicIP ,
2022-01-18 01:52:09 +00:00
host . RefetchRequested ,
host . GigsDiskSpaceAvailable ,
host . PercentDiskSpaceAvailable ,
host . ID ,
)
if err != nil {
return ctxerr . Wrapf ( ctx , err , "save host with id %d" , host . ID )
}
return nil
}
2022-03-28 15:15:45 +00:00
// OSVersions gets the aggregated os version host counts. If a non-nil teamID is passed, it will filter hosts by team.
2022-08-12 19:23:25 +00:00
func ( ds * Datastore ) OSVersions ( ctx context . Context , teamID * uint , platform * string , osID * uint ) ( * fleet . OSVersions , error ) {
2022-03-28 15:15:45 +00:00
query := `
SELECT
json_value ,
updated_at
FROM aggregated_stats
WHERE
id = ? AND
type = ' os_versions '
`
var row struct {
JSONValue * json . RawMessage ` db:"json_value" `
UpdatedAt time . Time ` db:"updated_at" `
}
var args [ ] interface { }
if teamID == nil { // all hosts
args = append ( args , 0 )
} else {
args = append ( args , * teamID )
}
2022-04-18 21:19:58 +00:00
err := sqlx . GetContext ( ctx , ds . reader , & row , query , args ... )
if err != nil {
if err == sql . ErrNoRows {
return nil , ctxerr . Wrap ( ctx , notFound ( "OSVersions" ) )
}
2022-03-28 15:15:45 +00:00
return nil , err
}
osVersions := & fleet . OSVersions {
CountsUpdatedAt : row . UpdatedAt ,
}
if row . JSONValue != nil {
err := json . Unmarshal ( * row . JSONValue , & osVersions . OSVersions )
if err != nil {
return nil , err
}
}
// filter by os versions by platform
if platform != nil {
var filtered [ ] fleet . OSVersion
for _ , osVersion := range osVersions . OSVersions {
if * platform == osVersion . Platform {
filtered = append ( filtered , osVersion )
}
}
osVersions . OSVersions = filtered
}
2022-08-12 19:23:25 +00:00
// filter by os versions by os id
if osID != nil {
var filtered [ ] fleet . OSVersion
for _ , osVersion := range osVersions . OSVersions {
if * osID == osVersion . ID {
filtered = append ( filtered , osVersion )
}
}
osVersions . OSVersions = filtered
}
2022-03-28 15:15:45 +00:00
// Sort by os versions. We can't control the order when using json_arrayagg
// See https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_json-arrayagg.
sort . Slice ( osVersions . OSVersions , func ( i , j int ) bool { return osVersions . OSVersions [ i ] . Name < osVersions . OSVersions [ j ] . Name } )
return osVersions , nil
}
2022-04-18 21:19:58 +00:00
// Aggregated stats for os versions are stored by team id with 0 representing the global case
// If existing team has no hosts, we explicity set the json value as an empty array
2022-03-28 15:15:45 +00:00
func ( ds * Datastore ) UpdateOSVersions ( ctx context . Context ) error {
2022-08-12 19:23:25 +00:00
selectStmt := `
2022-08-16 12:33:15 +00:00
SELECT
COUNT ( * ) hosts_count ,
h . team_id ,
2022-08-12 19:23:25 +00:00
os . id ,
2022-08-16 12:33:15 +00:00
os . name ,
os . version ,
2022-08-12 19:23:25 +00:00
os . platform
FROM hosts h
2022-08-16 12:33:15 +00:00
JOIN host_operating_system hos ON h . id = hos . host_id
JOIN operating_systems os ON hos . os_id = os . id
2022-08-12 19:23:25 +00:00
GROUP BY team_id , os_id
`
2022-04-18 21:19:58 +00:00
2022-08-12 19:23:25 +00:00
var rows [ ] struct {
HostsCount int ` db:"hosts_count" `
Name string ` db:"name" `
Version string ` db:"version" `
Platform string ` db:"platform" `
ID uint ` db:"id" `
TeamID * uint ` db:"team_id" `
}
if err := sqlx . SelectContext ( ctx , ds . reader , & rows , selectStmt ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "update os versions" )
}
// each team has a slice of stats with team host counts per os version
statsByTeamID := make ( map [ uint ] [ ] fleet . OSVersion )
// stats are also aggregated globally per os version
globalStats := make ( map [ uint ] fleet . OSVersion )
for _ , r := range rows {
os := fleet . OSVersion {
HostsCount : r . HostsCount ,
Name : fmt . Sprintf ( "%s %s" , r . Name , r . Version ) ,
NameOnly : r . Name ,
Version : r . Version ,
Platform : r . Platform ,
ID : r . ID ,
}
// increment global stats
if _ , ok := globalStats [ os . ID ] ; ! ok {
globalStats [ os . ID ] = os
} else {
newStats := globalStats [ os . ID ]
newStats . HostsCount += r . HostsCount
globalStats [ os . ID ] = newStats
}
// push to team stats if applicable
if r . TeamID != nil {
statsByTeamID [ * r . TeamID ] = append ( statsByTeamID [ * r . TeamID ] , os )
}
}
// if an existing team has no hosts assigned, we still want to store empty stats
var teamIDs [ ] uint
if err := sqlx . SelectContext ( ctx , ds . reader , & teamIDs , "SELECT id FROM teams" ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "update os versions" )
}
for _ , id := range teamIDs {
if _ , ok := statsByTeamID [ id ] ; ! ok {
statsByTeamID [ id ] = [ ] fleet . OSVersion { }
}
}
// global stats are stored under id 0
for _ , os := range globalStats {
statsByTeamID [ 0 ] = append ( statsByTeamID [ 0 ] , os )
}
// nothing to do so return early
if len ( statsByTeamID ) < 1 {
// log to help troubleshooting in case this happens
level . Info ( ds . logger ) . Log ( "msg" , "Cannot update aggregated stats for os versions: Check for records in operating_systems and host_perating_systems." )
return nil
}
// assemble values as arguments for insert statement
args := make ( [ ] interface { } , 0 , len ( statsByTeamID ) * 3 )
for id , stats := range statsByTeamID {
jsonValue , err := json . Marshal ( stats )
if err != nil {
return ctxerr . Wrap ( ctx , err , "marshal os version stats" )
}
args = append ( args , id , "os_versions" , jsonValue )
}
insertStmt := "INSERT INTO aggregated_stats (id, type, json_value) VALUES "
insertStmt += strings . TrimSuffix ( strings . Repeat ( "(?,?,?)," , len ( statsByTeamID ) ) , "," )
insertStmt += " ON DUPLICATE KEY UPDATE json_value = VALUES(json_value), updated_at = CURRENT_TIMESTAMP"
if _ , err := ds . writer . ExecContext ( ctx , insertStmt , args ... ) ; err != nil {
return ctxerr . Wrapf ( ctx , err , "insert os versions into aggregated stats" )
2022-03-28 15:15:45 +00:00
}
return nil
}
2022-06-08 01:09:47 +00:00
2022-06-13 20:29:32 +00:00
// EnrolledHostIDs returns the complete list of host IDs.
func ( ds * Datastore ) EnrolledHostIDs ( ctx context . Context ) ( [ ] uint , error ) {
const stmt = ` SELECT id FROM hosts `
var ids [ ] uint
if err := sqlx . SelectContext ( ctx , ds . reader , & ids , stmt ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get enrolled host IDs" )
}
return ids , nil
}
// CountEnrolledHosts returns the current number of enrolled hosts.
func ( ds * Datastore ) CountEnrolledHosts ( ctx context . Context ) ( int , error ) {
const stmt = ` SELECT count(*) FROM hosts `
var count int
if err := sqlx . SelectContext ( ctx , ds . reader , & count , stmt ) ; err != nil {
return 0 , ctxerr . Wrap ( ctx , err , "count enrolled host" )
}
return count , nil
}
2022-06-08 01:09:47 +00:00
func ( ds * Datastore ) HostIDsByOSVersion (
ctx context . Context ,
osVersion fleet . OSVersion ,
offset int ,
limit int ,
) ( [ ] uint , error ) {
var ids [ ] uint
stmt := dialect . From ( "hosts" ) .
Select ( "id" ) .
Where (
goqu . C ( "platform" ) . Eq ( osVersion . Platform ) ,
goqu . C ( "os_version" ) . Eq ( osVersion . Name ) ) .
Order ( goqu . I ( "id" ) . Desc ( ) ) .
Offset ( uint ( offset ) ) .
Limit ( uint ( limit ) )
sql , args , err := stmt . ToSQL ( )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get host IDs" )
}
if err := sqlx . SelectContext ( ctx , ds . reader , & ids , sql , args ... ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get host IDs" )
}
return ids , nil
}
2022-06-28 18:11:49 +00:00
2022-07-21 02:16:03 +00:00
// ListHostBatteries returns battery information as reported by osquery for the identified host.
//
// Note: Because of a known osquery issue with M1 Macs, we are ignoring the stored `health` value
// in the db and replacing it at the service layer with custom a value determined by the cycle
// count. See https://github.com/fleetdm/fleet/pull/6782#discussion_r926103758.
// TODO: Update once the underlying osquery issue has been resolved.
2022-06-28 18:11:49 +00:00
func ( ds * Datastore ) ListHostBatteries ( ctx context . Context , hid uint ) ( [ ] * fleet . HostBattery , error ) {
const stmt = `
SELECT
host_id ,
serial_number ,
cycle_count ,
health
FROM
host_batteries
WHERE
host_id = ?
`
var batteries [ ] * fleet . HostBattery
if err := sqlx . SelectContext ( ctx , ds . reader , & batteries , stmt , hid ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "select host batteries" )
}
return batteries , nil
}
2022-07-21 01:54:10 +00:00
// countHostNotResponding counts the hosts that haven't been submitting results for sent queries.
//
// Notes:
// - We use `2 * interval`, because of the artificial jitter added to the intervals in Fleet.
// - Default values for:
// - host.DistributedInterval is usually 10s.
// - svc.config.Osquery.DetailUpdateInterval is usually 1h.
// - Count only includes hosts seen during the last 7 days.
func countHostsNotRespondingDB ( ctx context . Context , db sqlx . QueryerContext , logger log . Logger , config config . FleetConfig ) ( int , error ,
) {
interval := config . Osquery . DetailUpdateInterval . Seconds ( )
// The primary `WHERE` clause is intended to capture where Fleet hasn't received a distributed write
// from the host during the interval since the host was last seen. Thus we assume the host
// is having some issue in executing distributed queries or sending the results.
// The subquery `WHERE` clause excludes from the count any hosts that were inactive during the
// current seven-day statistics reporting period.
sql := `
SELECT h . host_id FROM (
SELECT * FROM hosts JOIN host_seen_times hst ON hosts . id = hst . host_id
WHERE hst . seen_time >= DATE_SUB ( NOW ( ) , INTERVAL 7 DAY )
) h
WHERE
TIME_TO_SEC ( TIMEDIFF ( h . seen_time , h . detail_updated_at ) ) >= ( GREATEST ( h . distributed_interval , ? ) * 2 )
`
var ids [ ] int
if err := sqlx . SelectContext ( ctx , db , & ids , sql , interval ) ; err != nil {
return len ( ids ) , ctxerr . Wrap ( ctx , err , "count hosts not responding" )
}
if len ( ids ) > 0 {
// We log to help troubleshooting in case this happens.
level . Info ( logger ) . Log ( "err" , fmt . Sprintf ( "hosts detected that are not responding distributed queries %v" , ids ) )
}
return len ( ids ) , nil
}